如何设计SpringBoot项目的全局异常处理

在项目开发中,我们需要对一些异常进行统一的捕获,SpringBoot中有两个注解支撑全局异常处理:
(1)ControllerAdvice 表示开启了全局异常的捕获
(2)ExceptionHandler 表示指定捕获异常的类型

@ControllerAdvice
public class MyExceptionHandler {

   @ExceptionHandler(value = Exception.class)
   public String exceptionHandler(Exception e){
      System.out.println("未知异常!原因是:" + e);
      return e.getMessage();
   }
}

上面的写法比较低级,它没有考虑到两个点:(1)返回体没有统一;(2)有些异常信息应该脱敏

那么,我们现在开始改良,从UML类图开始吧~

软件设计

技术实现

异常接口类

通常来说,在项目初期设计时我们都会自己定义一个通用异常枚举类,然后在项目开发过程中依据业务特性在这个通用异常枚举类中不断追加异常代码。不断追加异常代码这种写法是比较常见的,但是不符合软件开闭设计原则(就是对扩展开放,对修改关闭)。我们可以通过 一个异常接口类+多个异常枚举类 来改良:

public interface IErrorCode {

    String getCode(); //code 建议使用字符串类型,兼容性更好~

    String getDescription();

}

所有异常枚举类通过实现 IErrorCode 接口,便可被适配为统一返回体 ObjectResult\TableResult 的异常事件入参:

(1)ObjectResult.error(IErrorCode errorCode)
(2)TableResult.error(IErrorCode errorCode)

比如定义:ErrorCodeEnumBlogErrorCodeEnumUserErrorCodeEnum,以 ErrorCodeEnum 为例:

异常枚举类

public enum ErrorCodeEnum implements IErrorCode {

   // 数据操作错误定义
   SUCCESS("200", "成功!"), 
   BODY_NOT_MATCH("400","请求的数据格式不符!"),
   SIGNATURE_NOT_MATCH("401","请求的数字签名不匹配!"),
   NOT_FOUND("404", "未找到该资源!"), 
   INTERNAL_SERVER_ERROR("500", "服务器内部错误!"),
   SERVER_BUSY("503","服务器正忙,请稍后再试!")
   ;

   /** 错误码 */
   private String code;

   /** 错误描述 */
   private String description;

   /**
     * 私有构造方法
     * @param code        编码
     * @param description 描述
     **/
   ErrorCodeEnum(String code, String description) {
      this.code = code;
      this.description = description;
   }

   public String  getCode() {
      return code;
   }

   public String getDescription() {
      return description;
   }

}

异常枚举类值域管理

实际开发中,若是采用微服务架构又或者是分包集成型软件,那么不同的服务都应该有自己的异常代码,这主要是方便出问题后的排查工作。因此,架构者需要做统一规划。比如,我会做如下规划:

框架异常(0-999),可以模仿http协议的状态码,定义

  • 200 正常
  • 400 错误语法
  • 401 身份未认证
  • 404 未找到资源
  • 500 服务器内部错误
  • 503 服务器忙

公共服务异常规划,从1000开始

  • 1000-1999 ⽹关
  • 2000-2999 统⼀⽤户中⼼
  • 3000-3999 统⼀认证中⼼
  • 4000-4999 统⼀权限中⼼
  • 5000-5999 统⼀消息中⼼

业务服务异常规划,从10000开始

  • 10000-10999 xxx服务
  • 11000-11999 yyy服务
  • ……

全局统一返回体

任何一个系统都需要定义一个统一返回体,通常来说设计一个ResultDto就够了。我这里做下微调,针对不同的业务场景从集合类和非集合类角度设计:
(1)TableResult 查询一批业务数据
(2)ObjectResult 增删改及获取单个业务对象

统一返回体 - 基类

@Data
@Builder
public class BaseResult {

   private boolean success = true;
   private String errorCode = ErrorCodeEnum.SUCCESS.getCode();
   private String errorMsg = ErrorCodeEnum.SUCCESS.getDescription();
   private String requestId;

   protected BaseResult() {}

   public static BaseResult success() {
      return BaseResult.builder().build();
   }

   public static BaseResult error() {
      return BaseResult.builder()
               .success(false)
               .errorCode(ErrorCodeEnum.INTERNAL_SERVER_ERROR.getCode())
               .errorMsg(ErrorCodeEnum.INTERNAL_SERVER_ERROR.getDescription())
               .build();
   }

   public static BaseResult error(IErrorCode errorCode) {
      return BaseResult.builder()
               .success(false)
               .errorCode(errorCode.getCode())
               .errorMsg(errorCode.getDescription())
               .build();
   }

   public static BaseResult auto(int serviceCode) {
      if(serviceCode==SERVICE_OK) return success();
      else return error();
   }

}

统一返回体 - 非集合类

public class ObjectResult<T> extends BaseResult {

   private T data;

   /**   getter setter 略   **/

   private ObjectResult() {
      super();
   }

   public static <T> ObjectResult<T> success(T data) {
      ObjectResult<T> objectResult = (ObjectResult<T>) success();
      objectResult.setData(data);
      return objectResult;
   }

}

统一返回体 - 集合类

public class TableResult<T> extends BaseResult {

   private PageBean<T> data;

   /**   getter setter 略   **/

   private TableResult() {
      super();
   }

   public static <T> TableResult<T> success(PageBean<T> data) {
      TableResult<T> objectResult = (TableResult<T>) success();
      objectResult.setData(data);
      return objectResult;
   }

}

异常类

异常基类

public class BaseException extends RuntimeException implements IErrorCode {

   /**
   * 状态码
   **/
   private String code;
   /**
   * 状态描述
   **/
   private String description;

   public BaseException() {}

   public BaseException(String code, String message) {
      super(message);
      this.code = code;
      this.description = message;
   }

   @Override
   public String getCode() {
      return this.code;
   }

   @Override
   public String getDescription() {
      return this.description;
   }
}

业务校验异常类

public class ValidateException extends BaseException {

}

其他异常都和 ValidateException 一样继承 BaseException,不贴代码了

为什么要设计 ValidateException ?它是在业务逻辑校验不通过时向外抛的异常。通常来说,这种校验逻辑不仅发生在 service、manager 两层,在 filter、interceptor 也可能存在,这个时候使用 ServiceException 或 ManagerException 来抛异常就涉及到层与层之间的渗透问题,不是那么优雅。替代方案自然是设计一个与 无关的异常,在 filter、interceptor、service、manager 或其他层中都可以使用

全局异常处理器

一个小细节,除 ServiceException、ManagerException、ValidateException 异常以外,其他异常我都做了脱敏,没有暴露到用户端。由于做了异常打印,因此在异常发生后,开发者可以在服务器日志中追查异常原因

@ControllerAdvice
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

   /**
   * 处理自定义的数据校验异常
   */
   @ExceptionHandler(value = ValidateException.class)
   public BaseResult validateException(HttpServletRequest req, HttpServletResponse response, ValidateException e){
      log.error("ValidateException:" + e.getMessage(), e);
      return BaseResult.error(e);
   }

   /**
   * 处理自定义的业务层异常
   */
   @ExceptionHandler(value = ServiceException.class)  
   public BaseResult serviceException(HttpServletRequest req, HttpServletResponse response, ServiceException e){
      log.error("ServiceException:" + e.getMessage(), e);
      return BaseResult.error(e);
   }

   /**
   * 处理自定义的manager层异常
   */
   @ExceptionHandler(value = ManagerException.class)  
   public BaseResult managerException(HttpServletRequest req, HttpServletResponse response, ManagerException e){
      log.error("ManagerException:" + e.getMessage(), e);
      return BaseResult.error(e);
   }

   /**
   * 处理自定义的DAO层异常
   */
   @ExceptionHandler(value = DaoException.class)  
   public BaseResult daoException(HttpServletRequest req, HttpServletResponse response, DaoException e){
      log.error("DaoException:" + e.getMessage(), e);
      return BaseResult.error(ErrorCodeEnum.INTERNAL_SERVER_ERROR);
   }

   /**
   * 处理空指针的异常
   */
   @ExceptionHandler(value = NullPointerException.class)
   public BaseResult nullPointerException(HttpServletRequest req, HttpServletResponse response, NullPointerException e){
      log.error("NullPointerException:" + e.getMessage(), e);
      return BaseResult.error(ErrorCodeEnum.BODY_NOT_MATCH);
   }

   /**
   * 处理其他异常
   */
   @ExceptionHandler(value = Exception.class)
   public ResultBody exception(HttpServletRequest req, HttpServletResponse response, Exception e){
      log.error("exception:" + e.getMessage(), e);
      return BaseResult.error(ErrorCodeEnum.INTERNAL_SERVER_ERROR);
   }

}

转载请注明来源。 欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。 可以在下面评论区评论,也可以邮件至 sharlot2050@foxmail.com。

文章标题:如何设计SpringBoot项目的全局异常处理

字数:1.6k

本文作者:夏来风

发布时间:2021-05-07, 00:01:20

原始链接:http://www.demo1024.com/blog/java-global-exception/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。