SpringBoot系列: Spring项目异常处理最佳实践

===================================
自定义异常类
===================================
稍具规模的项目, 一般都要自定义一组异常类, 这样做的好处是:
1. 可以充分利用异常的中断特性, 简化代码的逻辑控制.
2. 在自定义的异常类, 可以设置 BusinessErrorCode 和 error message, 有了统一的 BusinessErrorCode, 排查和联调沟通就更容易了.

Java 异常的 Root 是 Throwable, 其下有 Error 和 Exception. Error 是 JVM 级的致命错误, 应用系统内部一般不用关心这类错误. Exception 是异常的父类, 其下分为两类, 一类是 Runtime Exception, 一类是 Checked Exception. Checked Exception 是那些编译器能检查到的异常, 如果一个函数中抛出了这类异常, 我们要么 catch 它, 要么在函数签名上继续抛出去, 否则程序将不能编译通过.

Runtime Exception 有, 包括 RuntimeException 和它的子类. 比如 ArrayIndexOutOfBoundsException/ClassCastException/被 0 除等.
Checked Exception 有: Exception 类和所有非 RuntimeException 类的异常都属于 checked exception, 比如 IoException 等.

自定义异常类的基类如何选择?
自定义异常类应该继承自 RuntimeException 类, 原因有:
1. Spring 事务控制只支持 RuntimeException 类的异常.
2. 如果我们想要在函数加 throw Exception 签名, Java 语言已经提供了非常丰富的选择, 总能找到一个很合适的类, 而不需要再自定义一个.


===================================
自定义类的最佳实践:
===================================
1. 先定义一个基类 BusinessException, 继承自 RuntimeException.
2. 定义一套 BusinessErrorCode 枚举类型, 包含 BusinessErrorCode/HttpStatus/BusinessErrorMessage, 这里的 BusinessErrorCode 不同于 HttpStatus, 它是业务上的错误代码 (int 型).
2. 在 BusinessException 基类上, 加上绑定 BusinessErrorCode 枚举类型的机制.
3. 基于 BusinessException 定义一组子类, 比如 UserNotLoginException/PermissionForbiddenException/DataNotFoundException 等等, 并将这些子类加入到一个 BusinessExceptionEnum 枚举中, 方便使用.


===================================
Spring 项目数据验证最佳实践
===================================
1. 针对 UI 输入检查, 如果 js 前端检查有困难, 可以在 Controller 层使用 Pojo validation 手段做检查, 然后前端使用 ajax 拿到校验结果. 检查过程没有触发 UI 完整渲染, 用户体验会很好.
2. Controller 层使用 Pojo validation 进行检查.
3. Service 以下层, 使用 org.springframework.util.Assert 进行数据验证, 比如 Assert.notNull(user, "user is not null.");


===================================
Spring 日志和异常处理的最佳实践
===================================
异常处理的基本思路是: 早抛出, 晚捕获. 日志输出的基本思路是, 详尽但不冗余.
落实到具体的项目中, 在不同分层中, 应采用不同的规则, 一般的分层有: DAO -> Service -> Controller -> 统一异常 Controller 层.

1. DAO 层:
   (1) 尽量不 catch 任何异常, 该向上抛就抛.
   (2) 不用记录 log 日志, 或者仅使用 logger.debug() 记录
2. Service 层的做法:
   (1) @Transactional 注解应该加在 Service 层上.
   (2) 对于一些关键问题, 比如 Checked Exception 或者数据的问题, 应该及时 throw new BusinessException 异常, 以确保事务完整.
   (3) throw new BusinessException 时的日志: 如果是单体应用, 为了避免日志重复, 不需要 log 日志输出, 如果是微服务应用, 应加 logger.warning 日志.
   (4) Service 层一般的日志级别, 对于单体应用, 应该用 logger.debug() 记日志; 对于微服务应用, 应该是 logger.info().
   (5) Service 层函数的返回值应该是 Optional 类型, 方便 Controller 做 null 判断.
3. Controller 层:
   (1) Controller 层负责组装 Service, 在关键步骤上应该加日志输出 (info 级别)
   (2) Controller 层不应再主动 throw 异常.
4. 统一异常 Controller 层:
   (1) 通过 json 或 UI 返回详细的报错信息, 包括 HttpStatus 和详尽的 BusinessErrorCode/BusinessErrorMessage 以及 DetailErrorMessage(来源于 exception.getMessage()), 甚至包括 exception 的 stack trace.
   (2) 对于 Exception 类的异常, 说明这是我们预料之外的报错, 应该使用 logger.error() 级别记录;
   (3) 对于 BusinessException 和子类的异常, 则说明我们的程序已经预料到了, 事物该回滚也已经回滚了, 所以应该以 logger.warn() 或 logger.info() 记录日志.
   (4) 对于 Spring Boot 缺省的 /error 进行定制, 增加一些"系统主页"和"返回"的链接, 改善用户体验.


===================================
参考:
===================================
https://blog.csdn.net/aiyaya_/article/details/78725755
https://blog.csdn.net/aiyaya_/article/details/78989226
https://blog.qinchuan.io/experience/2018/10/11/spring-boot-restful-api-error-handling-in-practice.html
https://zhuanlan.zhihu.com/p/38114882
http://tengj.top/2018/05/16/springboot13/

猜你喜欢

转载自www.cnblogs.com/harrychinese/p/SpringBoot_exception_handler_best_practice.html