/**
* 修改用户信息
* @param user 要修改的用户数据
*/
public void updateUser(User user) {
User userOrig = userDao.getUserById(user.getUserID());
if (null == userOrig) {
throw new ServiceException("用户不存在");
}
if (userOrig.isLocked()) {
throw new ServiceException("用户被锁定,不允许修改");
}
if (!user.getVersion().equals(userOrig.getVersion())) {
throw new ServiceException("用户已经被别人修改过,请刷新重试");
}
// TODO 保存用户数据 ...
}
https://my.oschina.net/c5ms/blog/1827907?p=1
本文介绍
本文仅按照业务系统开发角度描述异常的一些处理看法.不涉及java的异常基础知识,可以自行查阅 《Java核心技术 卷I》 和 《java编程思想》 可以得到更多的基础信息.
写在前面的话
笔者文笔功力尚浅,言语多有不妥,请慷慨指正,必定感激不尽. 本文提出了几个概念: 处理反馈 业务异常 代码错误 ,请认真思考一下各中区别.
在开发业务系统中,我们目前绝大多数采用
MVC
模式,但是往往有人把service
跟controller
紧紧的耦合在一起,甚至直接使用Threadlocal
来隐式传值,并且复杂的逻辑几乎只能使用service
中存储的全局对象来传递处理结果,包括异常.这样一来首先有违
MVC
模式,二来逻辑十分不清晰,难以维护.本文结合工作经验,给出一些异常使用建议,使用spring
来实战异常为我们带来的好处.常常,我们读罢了各种
java
的书,异常的各种机制,特性都很清楚,但是始终还是不知道如何使用,甚至背下了概念,却不知道如何致用.我们开发的业务系统,或者是产品,常常面临着这样的问题:
什么情况需要自定义异常
经常看到一些项目,在全局定义一个
AppException
,然后所有地方都只抛出这个异常,并且把捕获的异常case
到这个AppException
中.会有如下问题:log
日志存储空间,并且栈顶并不是最接近发生异常的代码位置.什么情况需要手动处理异常
我不会把书上的东西直接复制下来,这里说一下容易记住的,并且适合业务开发的.
考虑如下场景: 系统提供一个API,用于修改用户信息,服务器端采用json数据交互.首先我们定义
ServiceException
,用来表示业务逻辑受理失败,它仅表示我们处理业务的时候发现无法继续执行下去.接下来看下
Controller
层.关于上述
Controller
写法乍一看会有一些冗余,如果无法理解,请仔细研读MVC
设计模式. 先不管service
, 我们来考虑下. 一个业务系统不可能不对用户提交的数据进行验证,验证包括两方面 : 有效性和合法性,有效性检查,可以交给
java
的校验框架执行,比如JSR303
. 假设用户提交的数据经过验证都合法,还是有一些情况是不能调用修改逻辑的.对于前
3
种,我们认为是有效性检查失败,第4
种属与我们无法处理的异常,第5
种就是程序员bug
.现在的问题是,前三种情况我们如何通知用户呢?
controller
调用userService
的checkUserExist()
方法.controller
直接书写业务逻辑.service
响应一个状态码机制,比如1 2 3
表示错误信息,0
表示没有任何错误.显然前
2
种方法都不可取 ,因为MVC
不设计模式告诉我们,controller
是用来接收页面参数,并且调用逻辑处理,最后组织页面响应的地方.我们不可以在controller
进行逻辑处理,controller
只应该负责用户API
入口和响应的处理(如若不然,思考一下如果有一天service
的代码打包成jar放到另一个平台,没有controller
了,该怎么办?)状态码机制是个不错的选择,可是如此一来,用户保存逻辑变了,比如增加一个情况,不允许修改已经离职的用户,那么我们还需要修改
controller
的代码,代码量增加,维护成本增高,并且还耦合了service
,不符合MVC
设计模式.那么怎么办呢?现在我们来看下
service
代码如何编写这样一来只要我们检查到不允许保存的项目,我们就可以直接
throw
一个新的异常,异常机制会帮助我们中断代码执行.接下来有
2
种选择:controller
使用try-catch
进行处理.第
1
种方式是不可取的 ,注意我们抛出的ServiceException
,它仅仅逻辑处理异常,并且我们的方法前面没有声明throws ServiceException
,这表示他是一个非受查异常.controller
也没有关心会发生什么异常.为什么不定义成受查异常呢? 如果是一个受查异常,那么意味着
controller
必须要处理你的异常.并且如果有一天你的业务逻辑变了,可能多一种检查项,就需要增加一个异常,反之需要删除一个异常,那么你的方法签名也需要改变,controller
也随之要改变,这又变成了紧耦合,这和用状态码123
表示处理结果没有什么不同.我们可以为每一种检查项定义一个异常吗? 可以,但是那样显得太多余了.因为业务逻辑处理失败的时候,根据我们需求,我们只需要通知用户失败的原因(通常应该是一段字符串),以及服务器受理失败的一个状态码(有时可能不需要状态码,这要看你的设计了),这样这需要一个包含原因属性的异常即可满足我们需求.
最后我们决定这个异常继承自
RuntimeException
.并且包含一个接受一个错误原因的构造器,这样controller
层也不需要知道异常,只要全局捕获到ServiceException
做统一的处理即可,这无论是在struct1,2
时代,还是springMVC
中,甚至servlet
年代,都是极为容易的!异常不提供无参构造器 ,因为绝对不允许你抛出一个逻辑处理异常,但是不指明原因,想想看,你是必须要告诉用户为什么受理失败的!
如此一来,我们只需要全局统一处理下
ServiceException
就可以了,很好,spring
为我们提供了ControllerAdvice
机制,有关ControllerAdvice
,可以查阅springMVC
使用文档,下面是一个简单的示例:在这个时候,我们就可以很轻松的处理各种情况了.
注意一点,在这个类中,我们定义了
2
个log
对象,分别指向ServiceException.class
和ModuleControllerAdvice.class
. 并且处理ServiceException
的时候使用了info
级别的日志输出,这是很有用的.首先,
ServiceException
一定要和其他的代码错误分离,不应该混为一谈. 其次,ServiceException
并不一定要记录日志,我们应该提供独立的log
对象,方便开关. 接下来你可以在修改用户的时候想客户端响应这样的JSON
如此一来没有任何地方需要关心异常,或者业务逻辑校验失败的情况.用户也可以得到很友好的错误提示.
如何对异常进行分类
如果你只需要一句概括,那么直接定义一个简单的异常,用于中断处理,并且与用户保持友好交互即可.
如果不可能一句话描述清楚,并且包含附加信息,比如需要在日志或者数据库记录消息
ID
,此时可能专门针对这种重要/复杂业务创建独立异常.上述两种情况因为
web
系统,是用户发起请求之后需要等待程序给予响应结果的.如果是后台作业,或者复杂业务需要追溯性.这种通常用流程判断语句控制,要用异常处理.我们认为这些流程判断一定在一个原子性处理中.并且检查到(不是遇到)的问题(不是异常)需要记录到用户可友好查看的日志.这种情况属于处理反馈,并不叫异常.
综上,笔者通常分为如下几类:
NPE
,ILLARG
,都属于程序员制造的BUG
.各类异常必须要有单独的日志记录,或者分级,分类可管理.有的时候仅仅想给三方运维看到逻辑异常.
写在后面的注意
异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多.
上面这句话出自<java编程思想>,但是我们思考如下几点:
业务逻辑检查,也是意外情况
UnknownHostException
,表示找不到这样的主机,这个异常和NoUserException
有什么区别么?换言之,没有这样的主机是异常,没有这样的用户不是异常了么? 所以一定要弄明白什么是用异常来控制逻辑,什么是定义程序异常.异常处理效率很低
书中所示的例子,是在循环中大量使用
try-catch
进行检查,但是业务系统,用户发起请求的次数与该场景天壤地别.淘宝的 11`11是个很好的反例.但是请你的系统上到这个级别再考虑这种问题.系统有千万并发,不可能还去考虑这些中规中矩的按部就班的方式,别忘了
MVC
本来就浪费很多资源,代码量增加很多. 业务系统也存在很多巨量任务处理的情况.但是那些任务都是原子性的,现在MVC
中的controller
和service
可不是原子性的,不然为什么要区分这么多层呢. 如果那么在乎效率,考虑下重写Throwable
的fillStackTrace
方法.你要知道异常的开销大到底大在什么地方,fillStackTrace
是一个native
方法,会填充异常类内部的运行轨迹. 不要用异常进行业务逻辑处理我们先来看一个例子:
上述代码就是典型的使用异常来处理业务逻辑.这种方式需要严重的禁止!上述代码最大的问题在于,我们如何利用异常来自动处理事务呢?
然而这和我们的异常中断
service
没有什么冲突.也并不是一回事.我们提倡在 业务处理 的时候,如果发现无法处理直接抛出异常即可. 而并不是在 逻辑处理 的时候,用异常来判断逻辑进行的状况. 改正后的逻辑
最后俏皮一句:微服务横行的今天,我们在
action
里面直接写业务处理,也无可厚非.