本文仅按照业务系统开发角度描述异常的一些处理看法.不涉及java的异常基础知识,可以自行查阅 《Java核心技术 卷I》 和 《java编程思想》 可以得到更多的基础信息.
在开发业务系统中,我们目前绝大多数采用MVC模式,但是往往有人把service跟controller紧紧的耦合在一起,甚至直接使用Threadlocal来隐式传值,并且复杂的逻辑几乎只能使用service中存储的全局对象来传递处理结果,包括异常.
这样一来首先有违MVC模式,二来逻辑十分不清晰,难以维护.本文结合工作经验,给出一些异常使用建议,使用spring来实战异常为我们带来的好处.
常常,我们读罢了各种java的书,异常的各种机制,特性都很清楚,但是始终还是不知道如何使用,甚至背下了概念,却不知道如何致用.
我们开发的业务系统,或者是产品,常常面临着这样的问题:
经常看到一些项目,在全局定义一个 AppException,然后所有地方都只抛出这个异常,并且把捕获的异常case到这个AppException中.会有如下问题:
我不会把书上的东西直接复制下来,这里说一下容易记住的,并且适合业务开发的.
考虑如下场景: 系统提供一个API,用于修改用户信息,服务器端采用json数据交互.首先我们定义ServiceException,用来表示业务逻辑受理失败, 它仅表示我们处理业务的时候发现无法继续执行下去.
/** * 业务受理失败异常 */ public class ServiceException extends RuntimeException { //接收reason参数用来描述业务失败原因. public ServiceException(String reason) { super(reason); } }
接下来看下Controller层.
// UserController.java /** * 修改用户信息 * @param userID 用户ID * @param user 修改用户信息表单数据 */ @PutMapping("{userID}") public JSONResult updateUser(@PathVariable("userID") Integer userID, @RequestBody UpdateUserForm userForm) { User user = new User(); //准备业务逻辑层使用的领域模型 BeanUtils.copyProperties(userForm, user); //拷贝要修改的值 user.setUserId(userID); //设置主键到用户数据中 userService.updateUser(user); //调用更新业务逻辑 JSONResult json = new JSONResult(); //准备要响应的数据 json.put("user", user); //把修改后的用户数据还给页面 return json; // -- }
关于上述Controller写法乍一看会有一些冗余,如果无法理解,请仔细研读MVC设计模式. 先不管service,我们来考虑下. 一个业务系统不可能不对用户提交的数据进行验证,验证包括两方面 : 有效性 和 合法性 ,
有效性检查,可以交给java的校验框架执行,比如JSR303. 假设用户提交的数据经过验证都合法,还是有一些情况是不能调用修改逻辑的.
对于前3种,我们认为是 有效性检查 失败,第4种属与我们无法处理的异常,第5种就是程序员bug.
显然前2种方法都不可取,因为MVC不设计模式告诉我们,controller是用来接收页面参数,并且调用逻辑处理,最后组织页面响应的地方.我们不可以在controller进行逻辑处理,controller只应该负责用户API入口和响应的处理(如若不然,思考一下如果有一天service的代码打包成jar放到另一个平台,没有controller了,该怎么办?)
状态码机制是个不错的选择,可是如此一来,用户保存逻辑变了,比如增加一个情况,不允许修改已经离职的用户,那么我们还需要修改controller的代码,代码量增加,维护成本增高,并且还耦合了service,不符合MVC设计模式.
那么怎么办呢?现在我们来看下service代码如何编写
/** * 修改用户信息 * @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 保存用户数据 ... }
这样一来只要我们检查到不允许保存的项目,我们就可以直接throw 一个新的异常,异常机制会帮助我们中断代码执行.
接下来有2种选择:
第1种方式是不可取的,注意我们抛出的ServiceException,它仅仅逻辑处理异常,并且我们的方法前面没有声明throws ServiceException,这表示他是一个非受查异常.controller也没有关心会发生什么异常.
为什么不定义成受查异常呢?如果是一个受查异常,那么意味着controller必须要处理你的异常.并且如果有一天你的业务逻辑变了,可能多一种检查项,就需要增加一个异常,反之需要删除一个异常,那么你的方法签名也需要改变,controller也随之要改变,这又变成了紧耦合,这和用状态码123表示处理结果没有什么不同.
我们可以为每一种检查项定义一个异常吗?可以,但是那样显得太多余了.因为业务逻辑处理失败的时候,根据我们需求,我们只需要通知用户失败的原因(通常应该是一段字符串),以及服务器受理失败的一个状态码(有时可能不需要状态码,这要看你的设计了),这样这需要一个包含原因属性的异常即可满足我们需求.
最后我们决定这个异常继承自RuntimeException.并且包含一个接受一个错误原因的构造器,这样controller层也不需要知道异常,只要全局捕获到ServiceException做统一的处理即可,这无论是在struct1,2时代,还是springMVC中,甚至servlet年代,都是极为容易的!
异常不提供无参构造器,因为绝对不允许你抛出一个逻辑处理异常,但是不指明原因,想想看,你是必须要告诉用户为什么受理失败的!
如此一来,我们只需要全局统一处理下 ServiceException 就可以了,很好,spring为我们提供了ControllerAdvice机制,有关ControllerAdvice,可以查阅springMVC使用文档,下面是一个简单的示例:
@ControllerAdvice(basePackages = { "com.xxx.xxx.bussiness.xxx" }) public class ModuleControllerAdvice { private static final Logger LOGGER = LoggerFactory.getLogger(ModuleControllerAdvice.class); private static final Logger SERVICE_LOGGER = LoggerFactory.getLogger(ServiceException.class); /** * 业务受理失败 */ @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(ServiceException.class) private JSONResult handleServiceException(ServiceException exception) { String message = "业务受理失败,原因:" + exception.getLocalizedMessage(); SERVICE_LOGGER.info(message); JSONResult json = new JSONResult(); json.serCode(500001); // 500000表示系统异常,500001表示业务逻辑异常 json.setMessage(message); return json; } }
在这个时候,我们就可以很轻松的处理各种情况了.
注意一点,在这个类中,我们定义了2个log对象,分别指向 ServiceException.class 和 ModuleControllerAdvice.class . 并且处理 ServiceException的时候使用了info级别的日志输出,这是很有用的.
接下来你可以在修改用户的时候想客户端响应这样的JSON
{ code: 200001, message: "业务受理失败,原因:用户名称不存在!" }
如此一来没有任何地方需要关心异常,或者业务逻辑校验失败的情况.用户也可以得到很友好的错误提示.
异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多.
上面这句话出自<java编程思想>,但是我们思考如下几点:
UnknownHostException,表示找不到这样的主机,这个异常和NoUserException有什么区别么?换言之,没有这样的主机是异常,没有这样的用户不是异常了么? 所以一定要弄明白什么是用异常来控制逻辑,什么是定义程序异常.
书中所示的例子,是在循环中大量使用try-catch进行检查,但是业务系统,用户发起请求的次数与该场景天壤地别.淘宝的11`11是个很好的反例.但是请你的系统上到这个级别再考虑这种问题.