转:优雅的处理你的Java异常

转载地址:https://my.oschina.net/c5ms/blog/1827907java

做者:叶知泉 程序员

侵删web

本文介绍

本文仅按照业务系统开发角度描述异常的一些处理见解.不涉及java的异常基础知识,能够自行查阅 《Java核心技术 卷I》 和 《java编程思想》 能够获得更多的基础信息.spring

写在前面的话

笔者文笔功力尚浅,言语多有不妥,请慷慨指正,一定感激涕零. 本文提出了几个概念: 处理反馈 业务异常 代码错误 ,请认真思考一下各中区别.数据库

在开发业务系统中,咱们目前绝大多数采用MVC模式,可是每每有人把service跟controller牢牢的耦合在一块儿,甚至直接使用Threadlocal来隐式传值,而且复杂的逻辑几乎只能使用service中存储的全局对象来传递处理结果,包括异常.编程

这样一来首先有违MVC模式,二来逻辑十分不清晰,难以维护.本文结合工做经验,给出一些异常使用建议,使用spring来实战异常为咱们带来的好处.json

经常,咱们读罢了各类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. 假设用户提交的数据通过验证都合法,仍是有一些状况是不能调用修改逻辑的.

  1. 要修改的用户ID不存在.
  2. 用户被锁定,不容许修改.
  3. 乐观锁机制发现用户已经被被人修改过.
  4. 因为某种缘由,咱们的程序没法保存到数据库.
  5. 一些程序员错误的开发了代码,致使保存过程当中出现异常,好比NPE.

对于前3种,咱们认为是有效性检查失败,第4种属与咱们没法处理的异常,第5种就是程序员bug.

如今的问题是,前三种状况咱们如何通知用户呢?

  1. 在ccontroller 调用userService的checkUserExist()方法.
  2. 在controller直接书写业务逻辑.
  3. 在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种选择:

  1. 在controller 使用try-catch进行处理.
  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级别的日志输出,这是颇有用的.

  • 首先,ServiceException必定要和其余的代码错误分离,不该该混为一谈.
  • 其次,ServiceException并不必定要记录日志,咱们应该提供独立的log对象,方便开关.

接下来你能够在修改用户的时候想客户端响应这样的JSON

{
    code: 200001,
    message: "业务受理失败,缘由:用户名称不存在!"
}

如此一来没有任何地方须要关心异常,或者业务逻辑校验失败的状况.用户也能够获得很友好的错误提示.

如何对异常进行分类

若是你只须要一句归纳,那么直接定义一个简单的异常,用于中断处理,而且与用户保持友好交互便可.

若是不可能一句话描述清楚,而且包含附加信息,好比须要在日志或者数据库记录消息ID,此时可能专门针对这种重要/复杂业务建立独立异常.

上述两种状况由于web系统,是用户发起请求以后须要等待程序给予响应结果的.

若是是后台做业,或者复杂业务须要追溯性.这种一般用流程判断语句控制,要用异常处理.咱们认为这些流程判断必定在一个原子性处理中.而且检查到(不是遇到)的问题(不是异常)须要记录到用户可友好查看的日志.这种状况属于处理反馈,并不叫异常.

综上,笔者一般分为以下几类:

  1. 逻辑异常,这类异经常使用于描述业务没法按照预期的状况处理下去,属于用户制造的意外.
  2. 代码错误,这类异经常使用于描述开发的代码错误,例如NPE,ILLARG,都属于程序员制造的BUG.
  3. 专有异常,多用于特定业务场景,用于描述指定做业出现意外状况没法预先处理.

各种异常必需要有单独的日志记录,或者分级,分类可管理.有的时候仅仅想给三方运维看到逻辑异常.

写在后面的注意

异常设计的初衷是解决程序运行中的各类意外状况,且异常的处理效率比条件判断方式要低不少.

上面这句话出自<java编程思想>,可是咱们思考以下几点:

业务逻辑检查,也是意外状况

UnknownHostException,表示找不到这样的主机,这个异常和NoUserException有什么区别么?换言之,没有这样的主机是异常,没有这样的用户不是异常了么? 因此必定要弄明白什么是用异常来控制逻辑,什么是定义程序异常.

异常处理效率很低

书中所示的例子,是在循环中大量使用try-catch进行检查,可是业务系统,用户发起请求的次数与该场景天壤地别.淘宝的11`11是个很好的反例.可是请你的系统上到这个级别再考虑这种问题.

  1. 系统有千万并发,不可能还去考虑这些中规中矩的循序渐进的方式,别忘了MVC原本就浪费不少资源,代码量增长不少.
  2. 业务系统也存在不少巨量任务处理的状况.可是那些任务都是原子性的,如今MVC中的controller和service可不是原子性的,否则为何要区分这么多层呢.
  3. 若是那么在意效率,考虑下重写Throwable的fillStackTrace方法.你要知道异常的开销大到底大在什么地方,fillStackTrace是一个native方法,会填充异常类内部的运行轨迹.

不要用异常进行业务逻辑处理

咱们先来看一个例子:

//这是一个很是典型的反例,也是一个误区.
  /**
   * 处理业务消息
   * @param message 要处理的消息
   */
  public void processMessage(Message<String> message) {
    try{
        // 处理消息验证
        // 处理消息解析
        // 处理消息入库
    }catch(ValidateException e ){
        // 验证失败
    }catch(ParseException e ){
        // 解析失败
    }catch(PersistException e ){
        // 入库失败
    }
  }

上述代码就是典型的使用异常来处理业务逻辑.这种方式须要严重的禁止!上述代码最大的问题在于,咱们如何利用异常来自动处理事务呢?

然而这和咱们的异常中断service没有什么冲突.也并非一回事.

  • 咱们提倡在 业务处理 的时候,若是发现没法处理直接抛出异常便可.
  • 而并非在 逻辑处理 的时候,用异常来判断逻辑进行的情况.

改正后的逻辑

/**
   * 处理业务消息
   * @param message 要处理的消息
   */
  public void processMessage(Message<String> message) {
    // 处理消息验证
    if(!message.isValud()){
        MessageLogService.log("消息校验失败"+message.errors())
        return ;
    }
    // 处理消息解析
    if(!message.parse()){
        MessageLogService.log("消息解析失败"+message.errors())
        return ;
    }
     // TODO ....
  }

最后俏皮一句:微服务横行的今天,咱们在action里面直接写业务处理,也无可厚非.

相关文章
相关标签/搜索