Spring Boot 统一异常处理和剖析

文章将说明如何统一处理异常,以及其背后的实现原理 ,老套路,先实现,后说明原理,有了上一篇文章的铺底,相信,理解这篇文章就驾轻就熟了

Spring Boot 统一异常处理和剖析

实现

新建业务异常

新建 BusinessException.class 类表示业务异常, 注意这是一个 Runtime 异常

@Data
@AllArgsConstructor
public final class BusinessException extends RuntimeException {
 private String errorCode;
 private String errorMsg;
 
}

添加统一异常处理静态方法

在 CommonResult 类中添加静态方法 errorResult 用于接收异常码和异常消息:

public static  CommonResult errorResult(String errorCode, String errorMsg){
 CommonResult commonResult = new CommonResult<>();
 commonResult.errorCode = errorCode;
 commonResult.errorMsg = errorMsg;
 commonResult.status = -1;
 return commonResult;
}

配置

同样要用到 @RestControllerAdvice 注解,将统一异常添加到配置中:

@RestControllerAdvice("com.example.unifiedreturn.api")
static class UnifiedExceptionHandler{
 @ExceptionHandler(BusinessException.class)
 public CommonResult handleBusinessException(BusinessException be){
 return CommonResult.errorResult(be.getErrorCode(), be.getErrorMsg());
 }
}

三部搞定,到这里无论是 Controller 还是 Service 中,只要抛出 BusinessException, 我们都会返回给前端一个统一数据格式。

Spring Boot 统一异常处理和剖析

测试

将 UserController 中的方法进行改造,直接抛出异常:

@GetMapping("/{id}")
public UserVo getUserById(@PathVariable Long id){
 throw new BusinessException("1001", "根据ID查询用户异常");
}

浏览器中输入:
http://localhost:8080/users/1

Spring Boot 统一异常处理和剖析

在 Service 中抛出异常:

@Service
public class UserServiceImpl implements UserService {
 /**
 * 根据用户ID查询用户
 *
 * @param id
 * @return
 */
 @Override
 public UserVo getUserById(Long id) {
 throw new BusinessException("1001", "根据ID查询用户异常");
 }
}
 

运行是得到同样的结果,所以我们尽可能的抛出异常吧 (作为一个程序猿这种心理很可拍)

解剖实现过程

解剖这个过程是相当纠结的,为了更好的说(yin)明(wei)问(wo)题(lan),我要说重中之重了,真心希望看该文章的童鞋自己去案发现场发现线索

还是在
WebMvcConfigurationSupport 类中实例化了 HandlerExceptionResolver Bean

@Bean
public HandlerExceptionResolver handlerExceptionResolver() {
 List exceptionResolvers = new ArrayList<>();
 configureHandlerExceptionResolvers(exceptionResolvers);
 if (exceptionResolvers.isEmpty()) {
 addDefaultHandlerExceptionResolvers(exceptionResolvers);
 }
 extendHandlerExceptionResolvers(exceptionResolvers);
 HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
 composite.setOrder(0);
 composite.setExceptionResolvers(exceptionResolvers);
 return composite;
}

和上一篇文章一毛一样的套路,
ExceptionHandlerExceptionResolver 实现了 InitializingBean 接口,重写了 afterPropertiesSet 方法:

@Override
public void afterPropertiesSet() {
 // Do this first, it may add ResponseBodyAdvice beans
 initExceptionHandlerAdviceCache();
 ...
}
private void initExceptionHandlerAdviceCache() {
 if (getApplicationContext() == null) {
 return;
 }
 List adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
 AnnotationAwareOrderComparator.sort(adviceBeans);
 for (ControllerAdviceBean adviceBean : adviceBeans) {
 Class> beanType = adviceBean.getBeanType();
 if (beanType == null) {
 throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
 }
 // 重点看这个构造方法
 ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
 if (resolver.hasExceptionMappings()) {
 this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
 }
 if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
 this.responseBodyAdvice.add(adviceBean);
 }
 }
}

重点看上面我用注释标记的构造方法,代码很好懂,仔细看看吧,其实就是筛选出我们用 @ExceptionHandler 注解标记的方法并放到集合当中,用于后续全局异常捕获的匹配

/**
 * A constructor that finds {@link ExceptionHandler} methods in the given type.
 * @param handlerType the type to introspect
 */
public ExceptionHandlerMethodResolver(Class> handlerType) {
 for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
 for (Class extends Throwable> exceptionType : detectExceptionMappings(method)) {
 addExceptionMapping(exceptionType, method);
 }
 }
}
/**
 * Extract exception mappings from the {@code @ExceptionHandler} annotation first,
 * and then as a fallback from the method signature itself.
 */
@SuppressWarnings("unchecked")
private List> detectExceptionMappings(Method method) {
 List> result = new ArrayList<>();
 detectAnnotationExceptionMappings(method, result);
 if (result.isEmpty()) {
 for (Class> paramType : method.getParameterTypes()) {
 if (Throwable.class.isAssignableFrom(paramType)) {
 result.add((Class extends Throwable>) paramType);
 }
 }
 }
 if (result.isEmpty()) {
 throw new IllegalStateException("No exception types mapped to " + method);
 }
 return result;
}
private void detectAnnotationExceptionMappings(Method method, List> result) {
 ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);
 Assert.state(ann != null, "No ExceptionHandler annotation");
 result.addAll(Arrays.asList(ann.value()));
}

到这里,我们用 @RestControllerAdvice 和 @ExceptionHandler 注解就会被 Spring 扫描到上下文,供我们使用

让我们回到你最熟悉的调用的入口 DispatcherServlet 类的 doDispatch 方法:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
 ...
 try {
 ModelAndView mv = null;
 Exception dispatchException = null;
 try {
 ...
 // 当请求发生异常,该方法会通过 catch 捕获异常
 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
 ...
 
 }
 catch (Exception ex) {
 dispatchException = ex;
 }
 catch (Throwable err) {
 // As of 4.3, we're processing Errors thrown from handler methods as well,
 // making them available for @ExceptionHandler methods and other scenarios.
 dispatchException = new NestedServletException("Handler dispatch failed", err);
 }
 // 调用该方法分析捕获的异常
 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
 }
 ...
}

接下来,我们来看 processDispatchResult 方法,这里只要展示调用栈你就会眼前一亮了,又是为了返回统一格式数据:

Spring Boot 统一异常处理和剖析

总结

上一篇文章的返回统一数据格式是基础,当异常情况发生时,只不过需要将异常信息提取出来。本文主要为了说明问题,剖析原理,好多地方设计方式是不可取,比如我们最好将异常封装在一个 Enum 类,通过 enum 对象抛出异常等。

Spring Boot 统一异常处理和剖析

附加说明

之前看到的一本书对异常的分类让我印象深刻,在此摘录一小段分享给大家:

Spring Boot 统一异常处理和剖析

结合出国旅行的例子说明异常分类:

  • 机场地震,属于不可抗力,对应异常分类中的 Error ,在制订出行计划时,根本不需要把这个部分的异常考虑进去
  • 堵车属于 checked 异常,应对这种异常,我们可以提前出发,或者改签机票。而飞机延误异常,虽然也需要 check,但我们无能为力,只能持续关注航班动态
  • 没有带护照,明显属于可 提前预测的异常 ,只要出发前检查即可避免;去机场路上车子抛锚,这个异常是突发的,虽然难以预料,但是必须处理,属于 需要捕捉的异常 ,可以通过更换交通工具;应对检票机器故障属于 可透出异常 ,交由航空公司处理,我们无须关心


分享到:


相關文章: