Java业务开发最佳实践-异常处理

1,302 阅读3分钟

代码的三层架构

不建议在框架层面进行异常的自动、统一处理,尤其不要随意捕获异常。框架只是做兜底工作。

  • Controller 层负责信息收集、参数校验、转换服务层处理的数据适配前端,轻业务逻辑。
    • 如果下层异常上升到 Controller 层还是无法处理的话,Controller 层往往会给予用户友好提示,或是根据每一个 API 的异常表返回指定的异常类型,同样无法对所有异常一视同仁。
  • Service 层负责核心业务逻辑,包括各种外部服务调用、访问数据库、缓存处理、消息处理等。
    • Service 层往往涉及数据库事务,出现异常同样不适合捕获,否则事务无法自动回滚。
    • Service 层涉及业务逻辑,有些业务逻辑执行中遇到业务异常,可能需要在异常后转入分支业务流程。
    • 如果业务异常都被框架捕获了,业务功能就会不正常。
  • Repository 层负责数据访问实现,一般没有业务逻辑。
    • Repository 层出现异常或许可以忽略,或许可以降级,或许需要转化为一个友好的异常。
    • 如果一律捕获异常仅记录日志,很可能业务逻辑已经出错,而用户和程序本身完全感知不到。

img

最佳实践

  • 记录日志时打印异常栈信息、抛出异常时指定消息

    catch (IOException e) {
        log.error("文件读取错误", e);
        throw new RuntimeException("系统忙请稍后再试");
    }
    
  • 通过日志正确记录异常原始信息。

    • 自定义的业务异常
      1. 以 Warn 级别的日志记录异常以及当前 URL、执行方法等信息
      2. 提取异常中的错误码和消息等信息,转换为合适的 API 包装体返回给 API 调用方
    • 无法处理的系统异常
      1. 以 Error 级别的日志记录异常和上下文信息(比如 URL、参数、用户 ID)
      2. 转换为普适的“服务器忙,请稍后再试”异常信息,以 API 包装体返回给调用方
  • 转换为新的异常抛出。对于新抛出的异常,最好具有特定的分类和明确的异常消息,而不是随便抛一个无关或没有任何信息的异常,并最好通过 cause 关联老异常。

    catch (IOException e) {
        throw new RuntimeException("系统忙请稍后再试", e);
    }
    

    意义:将偏向于底层的异常转换为高层异常,即把偏向于自己才能理解的内部处理异常转换为一个外部可以理解的异常抛出。

  • 重试之前的操作。比如远程调用服务端过载超时的情况,盲目重试会让问题更严重,需要考虑当前情况是否适合重试。

  • 恢复,尝试进行降级处理,或使用默认值替代原始数据。

  • 异常是每次 new 出来的,不能使用一个预先定义的 static 静态变量存放异常,否则会导致异常栈信息错乱。

  • 手动抛出异常时,不建议直接使用 Exception 或 RuntimeException,建议复用 JDK 中的一些标准异常。

  • 确保正确处理了线程池中任务的异常。出现异常会导致线程退出,大量的异常会导致线程重复创建引起性能问题。

    • 任务通过 execute 提交:尽可能确保任务不出异常,同时设置默认的未捕获异常处理程序做兜底。

    • 任务通过 submit 提交:通过拿到的 Future 调用其 get 方法来获得任务运行结果和可能出现的异常,否则异常可能就被生吞了。

JDK标准异常

  • IllegalArgumentException: 入参错误,比如参数类型int输入string。
  • IllegalStateException: 状态错误,比如订单已经支付完成,二次请求支付接口。
  • UnsupportedOperationException: 不支持操作错误,比如对一笔不能退款的订单退款。
  • SecurityException: 权限错误,比如未登陆用户调用修改用户信息接口。