springboot项目中统一异常处理

一.统一异常处理分析

Spring在 3.2 版本增加了一个注解@ControllerAdvice,
可以与@ExceptionHandler、@InitBinder、@ModelAttribute 等注解注解配套使用,
对于这几个注解的作用,这里不做过多赘述,若有不了解的,
可以参考 Spring3.2 新注解 @ControllerAdvice,先大概有个了解。
不过跟异常处理相关的只有注解@ExceptionHandler,从字面上看,就是 异常处理器 的意思,
其实际作用也是:若在某个Controller类定义一个异常处理方法,
并在方法上添加该注解,那么当出现指定的异常时,会执行该处理异常的方法,
其可以使用 springmvc 提供的数据绑定,
比如注入 HttpServletRequest 等,还可以接受一个当前抛出的 Throwable 对象。
但是,这样一来,就必须在每一个Controller类都定义一套这样的异常处理方法,因为异常可以是各种各样。
这样一来,就会造成大量的冗余代码,而且若需要新增一种异常的处理逻辑,
就必须修改所有Controller类了,很不优雅。
当然你可能会说,那就定义个类似BaseController的基类,这样总行了吧。
这种做法虽然没错,但仍不尽善尽美,因为这样的代码有一定的侵入性和耦合性。
简简单单的Controller,我为啥非得继承这样一个类呢,
万一已经继承其他基类了呢。大家都知道Java只能继承一个类。
那有没有一种方案,既不需要跟Controller耦合,
也可以将定义的 异常处理器 应用到所有控制器呢?
所以注解@ControllerAdvice出现了,简单的说,
该注解可以把异常处理器应用到所有控制器,而不是单个控制器。
借助该注解,我们可以实现:在独立的某个地方,比如单独一个类,
定义一套对各种异常的处理机制,然后在类的签名加上注解@ControllerAdvice,
统一对 不同阶段的、不同异常 进行处理。这就是统一异常处理的原理。
注意到上面对异常按阶段进行分类,
大体可以分成:进入Controller前的异常 和 Service 层异常,
不同阶段的异常

在这里插入图片描述

消灭 95% 以上的 try catch 代码块,
以优雅的 Assert(断言) 方式来校验业务的异常情况,
只关注业务逻辑,而不用花费大量精力写冗余的 try catch 代码块。

二.统一异常处理实战

1.用Assert(断言)替换 throw exception

想必 Assert(断言) 大家都很熟悉,
比如 Spring 家族的 org.springframework.util.Assert,
在我们写测试用例的时候经常会用到,
使用断言能让我们编码的时候有一种非一般丝滑的感觉,
比如:
@Test
public void test1() {
	...
	User user = userDao.selectById(userId);
	Assert.notNull(user, "用户不存在.");
	...
}

2.断言类抛出自己定义的异常

org.springframework.util.Assert,也写一个断言类,
不过断言失败后抛出的异常不是IllegalArgumentException 这些内置异常,
而是我们自己定义的异常。
下面让我们来尝试一下

Assert

public interface Assert {
    /**创建异常*/
    BaseException newException(Object... args);
    
    /** 创建异常*/
    BaseException newException(Throwable t, Object... args);
 
    /**
     * 断言对象obj非空。如果对象obj为空,则抛出异常
     */
    default void assertNotNull(Object obj) {
        if (obj == null) { throw newException(obj); }
    }

    /**
     * 断言对象obj非空。如果对象obj为空,则抛出异常
     * 异常信息message支持传递参数方式,避免在判断之前进行字符串拼接操作
     */
    default void assertNotNull(Object obj, Object... args) {
        if (obj == null) {
            throw newException(args);
        }
    }
}
自定义异常BaseException有2个属性,
即code、message,这样一对属性,有没有想到什么类一般也会定义这 2 个属性?
没错,就是枚举类。且看我如何将 Enum 和 Assert 结合起来,相信我一定会让你眼前一亮。
public interface IResponseEnum {
    int getCode();
    String getMessage();
}
/**
 *业务异常
 *业务处理时,出现异常,可以抛出该异常
 */
public class BusinessException extends  BaseException {
    private static final long serialVersionUID = 1L;
    public BusinessException(IResponseEnum responseEnum, Object[] args, String message) {
        super(responseEnum, args, message);
    }
    public BusinessException(IResponseEnum responseEnum, Object[] args, String message, Throwable cause) {
        super(responseEnum, args, message, cause);
    }
}
public interface BusinessExceptionAssert extends IResponseEnum, Assert {
    @Override
    default BaseException newException(Object... args) {
        String msg = MessageFormat.format(this.getMessage(), args);
        return new BusinessException(this, args, msg);
    }
    @Override
    default BaseException newException(Throwable t, Object... args) {
        String msg = MessageFormat.format(this.getMessage(), args);
        return new BusinessException(this, args, msg, t);
    }
}
@Getter
@AllArgsConstructor
public enum ResponseEnum implements BusinessExceptionAssert {
    BAD_LICENCE_TYPE(7001, "Bad licence type."),
    LICENCE_NOT_FOUND(7002, "Licence not found.")
    ;
    /*** 返回码*/
    private int code;
    /*** 返回消息*/
    private String message;
}
代码示例中定义了两个枚举实例:BAD_LICENCE_TYPE、LICENCE_NOT_FOUND,
分别对应了BadLicenceTypeException、LicenceNotFoundException两种异常。
以后每增加一种异常情况,只需增加一个枚举实例即可,再也不用每一种异常都定义一个异常类了。
然后再来看下如何使用,假设LicenceService有校验Licence是否存在的方法,如下:
/**校验{@link Licence}存在*/
private void checkNotNull(Licence licence) {
   ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);
}

定义统一异常处理器类

@Slf4j
@Component
@ControllerAdvice
@ConditionalOnWebApplication
@ConditionalOnMissingBean(UnifiedExceptionHandler.class)
public class UnifiedExceptionHandler {
    /*** 生产环境*/
    private final static String ENV_PROD = "prod";
    @Autowired
    private UnifiedMessageSource unifiedMessageSource;
    /*** 当前环境*/
    @Value("${spring.profiles.active}")
    private String profile;
    
    /*** 获取国际化消息 */
    public String getMessage(BaseException e) {
        String code = "response." + e.getResponseEnum().toString();
        String message = unifiedMessageSource.getMessage(code, e.getArgs());
        if (message == null || message.isEmpty()) {
            return e.getMessage();
        }
        return message;
    }
 
    /** * 业务异常 */
    @ExceptionHandler(value = BusinessException.class)
    @ResponseBody
    public ErrorResponse handleBusinessException(BaseException e) {
        log.error(e.getMessage(), e);
        return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
    }
 
    /*** 自定义异常*/
    @ExceptionHandler(value = BaseException.class)
    @ResponseBody
    public ErrorResponse handleBaseException(BaseException e) {
        log.error(e.getMessage(), e);
        return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
    }
 
    /**Controller上一层相关异常*/
    @ExceptionHandler({
            NoHandlerFoundException.class,
            HttpRequestMethodNotSupportedException.class,
            HttpMediaTypeNotSupportedException.class,
            MissingPathVariableException.class,
            MissingServletRequestParameterException.class,
            TypeMismatchException.class,
            HttpMessageNotReadableException.class,
            HttpMessageNotWritableException.class,
            // BindException.class,
            // MethodArgumentNotValidException.class
            HttpMediaTypeNotAcceptableException.class,
            ServletRequestBindingException.class,
            ConversionNotSupportedException.class,
            MissingServletRequestPartException.class,
            AsyncRequestTimeoutException.class
    })
    @ResponseBody
    public ErrorResponse handleServletException(Exception e) {
        log.error(e.getMessage(), e);
        int code = CommonResponseEnum.SERVER_ERROR.getCode();
        try {
            ServletResponseEnum servletExceptionEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName());
            code = servletExceptionEnum.getCode();
        } catch (IllegalArgumentException e1) {
            log.error("class [{}] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName());
        }
        if (ENV_PROD.equals(profile)) {
            // 当为生产环境, 不适合把具体的异常信息展示给用户, 比如404.
            code = CommonResponseEnum.SERVER_ERROR.getCode();
            BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
            String message = getMessage(baseException);
            return new ErrorResponse(code, message);
        }
        return new ErrorResponse(code, e.getMessage());
    }
 
 
    /**参数绑定异常 */
    @ExceptionHandler(value = BindException.class)
    @ResponseBody
    public ErrorResponse handleBindException(BindException e) {
        log.error("参数绑定校验异常", e);
        return wrapperBindingResult(e.getBindingResult());
    }
 
    /** 参数校验异常,将校验失败的所有异常组合成一条错误信息*/
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    @ResponseBody
    public ErrorResponse handleValidException(MethodArgumentNotValidException e) {
        log.error("参数绑定校验异常", e);
        return wrapperBindingResult(e.getBindingResult());
    }
 
    /** * 包装绑定异常结果*/
    private ErrorResponse wrapperBindingResult(BindingResult bindingResult) {
        StringBuilder msg = new StringBuilder();
        for (ObjectError error : bindingResult.getAllErrors()) {
            msg.append(", ");
            if (error instanceof FieldError) {
                msg.append(((FieldError) error).getField()).append(": ");
            }
            msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage());
        }
        return new ErrorResponse(ArgumentResponseEnum.VALID_ERROR.getCode(), msg.substring(2));
    }
 
    /*** 未定义异常*/
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ErrorResponse handleException(Exception e) {
        log.error(e.getMessage(), e);
        if (ENV_PROD.equals(profile)) {
            // 当为生产环境, 不适合把具体的异常信息展示给用户, 比如数据库异常信息.
            int code = CommonResponseEnum.SERVER_ERROR.getCode();
            BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
            String message = getMessage(baseException);
            return new ErrorResponse(code, message);
        }
        return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
    }
}

使用定义统一异常处理器类:

1.如何让404也抛出异常呢只需在properties文件中加入如下配置即可:

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

2.捕获自定义异常

public LicenceDTO queryDetail(Long licenceId){
  Licence licence = this.getById(licenceId);
  
  //校验非空
  ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence); 
  //断言,非空
  ResponseEnum.BADLICENCETYPE.assertNotNull(licenceTypeEnum); 
  
  OrganizationDTO org = ClientUtil.execute(() -> organizationClient.getOrganization(licence.getOrganizationId()));
  return toLicenceDTO(licence, org);
}

3.Controller 前的异常

捕获 404 异常
@ResponseBody
public ErrorResponse handleServletException(Exception e) {
  int code = CommonResponseEnum,SERVER ERRORgetCode(); 
  //...
  return new ErrorResponse(code,e.getMessage());
}
不支持GET请求异常【到统一异常处理方法中:handleServletException】
@RestController
@RequestMapping(value = "/licence")
public class LicenceController {
  @Autowiredprivate LicenceService licenceService;
  @PostMapping 
  public R<LicenceAddRespData> addLicence(@Validated @RequestBody LicenceAddRequest request) {
     return new R<>(licenceService.addLicence(request));
  }
}
校验异常
@NotBlank(message = "licence type cannot be empty.")
private String licenceType;
校验异常 2:post 请求,这里使用 postman 模拟
@PostMapping
public R<LicenceAddRespData> addLicence(@Validated @RequestBody LicenceAddRequest request){
 //.....
}

总结

使用 断言 和 枚举类 相结合的方式,再配合统一异常处理,基本大部分的异常都能够被捕获。
为什么说大部分异常,因为当引入 spring cloud security 后,还会有认证 / 授权异常,
网关的服务降级异常、跨模块调用异常、远程调用第三方服务异常等,这些异常的捕获方式与本文介绍的不太一样,
不过限于篇幅,这里不做详细说明,以后会有单独的文章介绍。
另外,当需要考虑国际化的时候,捕获异常后的异常信息一般不能直接返回,需要转换成对应的语言,
不过本文已考虑到了这个,获取消息的时候已经做了国际化映射,逻辑如下
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java之书

会持续更新实用好的文章谢谢关注

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值