正文
本文介绍
本文仅按照业务系统开发角度描述异常的一些处理看法.不涉及java的异常基础知识,可以自行查阅
《Java核心技术 卷I》
和
《java编程思想》
可以得到更多的基础信息.
写在前面的话
笔者文笔功力尚浅,言语多有不妥,请慷慨指正,必定感激不尽. 本文提出了几个概念:
处理反馈
业务异常
代码错误
,请认真思考一下各中区别.
在开发业务系统中,我们目前绝大多数采用MVC模式,但是往往有人把service跟controller紧紧的耦合在一起,甚至直接使用Threadlocal来隐式传值,并且复杂的逻辑几乎只能使用service中存储的全局对象来传递处理结果,包括异常.
这样一来首先有违MVC模式,二来逻辑十分不清晰,难以维护.本文结合工作经验,给出一些异常使用建议,使用spring来实战异常为我们带来的好处.
常常,我们读罢了各种java的书,异常的各种机制,特性都很清楚,但是始终还是不知道如何使用,甚至背下了概念,却不知道如何致用.
我们开发的业务系统,或者是产品,常常面临着这样的问题:
-
系统运行出错,但是完全不知道错误发生的位置.
-
我们找到了错误的位置,但是完全不知道是因为什么.
-
系统明明出了错误,但是就是看不到错误堆栈信息.
什么情况需要自定义异常
经常看到一些项目,在全局定义一个 AppException,然后所有地方都只抛出这个异常,并且把捕获的异常case到这个AppException中.会有如下问题:
-
浪费log日志存储空间,并且栈顶并不是最接近发生异常的代码位置.
-
只有一种异常类,无法精准区分开异常类型
-
异常类后期难以修改以增加其携带的信息.
什么情况需要手动处理异常
我不会把书上的东西直接复制下来,这里说一下容易记住的,并且适合业务开发的.
-
你有能力处理异常,并且你知道如何处理
-
你有责任处理异常
自定义业务异常
考虑如下场景: 系统提供一个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,我们来考虑下. 一个业务系统不可能不对用户提交的数据进行验证,验证包括两方面 :
有效性
和
合法性
,
-
有效性: 比如用户所在岗位,是否属于数据库有记录的岗位ID,如果不存在,无效.
-
合法性: 比如用户名只允许输入最多12个字符,用户提交了20个字符,不合法.
有效性检查,可以交给java的校验框架执行,比如JSR303. 假设用户提交的数据经过验证都合法,还是有一些情况是不能调用修改逻辑的.
-
要修改的用户ID不存在.
-
用户被锁定,不允许修改.
-
乐观锁机制发现用户已经被被人修改过.
-
由于某种原因,我们的程序无法保存到数据库.
-
一些程序员错误的开发了代码,导致保存过程中出现异常,比如NPE.
对于前3种,我们认为是
有效性检查
失败,第4种属与我们无法处理的异常,第5种就是程序员bug.
现在的问题是,前三种情况我们如何通知用户呢?
-
在ccontroller 调用userService的checkUserExist()方法.
-
在controller直接书写业务逻辑.
-
在service响应一个状态码机制,比如1 2 3表示错误信息,0 表示没有任何错误.
显然前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种选择:
-
在controller 使用try-catch进行处理.
-
直接把异常抛给上层框架统一处理.
第1种方式是不可取的
,注意我们抛出的ServiceException,它仅仅逻辑处理异常,并且我们的方法前面没有声明throws ServiceException,这表示他是一个非受查异常.controller也没有关心会发生什么异常.
为什么不定义成受查异常呢?
如果是一个受查异常,那么意味着controller必须要处理你的异常.并且如果有一天你的业务逻辑变了,可能多一种检查项,就需要增加一个异常,反之需要删除一个异常,那么你的方法签名也需要改变,controller也随之要改变,这又变成了紧耦合,这和用状态码123表示处理结果没有什么不同.