springboot优雅实现异常处理

前言

在平时的 API 开发过程中,总会遇到一些错误异常没有捕捉到的情况。那有的小伙伴可能会想,这还不简单么,我在 API 最外层加一个 try...catch 不就完事了。

哈哈哈,没错。这种方法简单粗暴。指北君曾经也是这么干的,但是你转过来想一想,你会在每一个 API 入口,都去做 try...catch 吗?这样不是代码非常丑陋的。小伙伴开始思考,突然灵光一现,说我们实现一个 AOP 来做这事不就完了。没错,使用 AOP 来实现是最佳的选择。

现在就给大家来介绍介绍 Spring Boot 怎么通过注解来实现全局异常处理的。

主角 @ControllerAdvice@ExceptionHandler

我们先来介绍一下今天的主角,分别是 @ControllerAdvice@ExceptionHandler

  • @ControllerAdvice 相当于 controller 的切面,主要用于 @ExceptionHandler, @InitBinder@ModelAttribute,使注解标注的方法对每一个 controller 都起作用。默认对所有 controller 都起作用,当然也可以通过 @ControllerAdvice 注解中的一些属性选定符合条件的 controller

源码如下:

package org.springframework.web.bind.annotation;
 
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
 
@AliasFor("basePackages")
String[] value() default {};
 
@AliasFor("value")
String[] basePackages() default {};
 
Class<?>[] basePackageClasses() default {};
 
Class<?>[] assignableTypes() default {};
 
Class<? extends Annotation>[] annotations() default {};
}
  • @ExceptionHandler 用于异常处理的注解,可以通过 value 指定处理哪种类型的异常还可以与 @ResponseStatus 搭配使用,处理特定的 http 错误。标记的方法入参与返回值都有很大的灵活性,具体可以看注释也可以在后边的深度探究。

源码如下:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
    Class<? extends Throwable>[] value() default {};
}

案例分析

今天我们就通过几种案例的方式,来给大家分析分析,怎么通过全局异常处理的方式玩转 Spring Boot 的全局异常处理。

案例一

一般的异常处理,所有的API都需要有相同的异常结构

Image

exception1

在这种情况下,实现是非常简单的,我们只需要创建 GeneralExceptionHandler 类,用 @ControllerAdvice 注解来注解它,并创建所需的 @ExceptionHandler ,它将处理所有由应用程序抛出的异常,如果它能找到匹配的 @ExceptionHandler,它将相应地进行转换。

@ControllerAdvice
public class GeneralExceptionHandler {
    @ExceptionHandler(Exception.class)
    protected ResponseEntity<Error> handleException(Exception ex) {
       MyError myError = MyError.builder()
                         .text(ex.getMessage())
                         .code(ex.getErrorCode()).build();
       return new ResponseEntity(myError,
                               HttpStatus.valueOf(ex.getErrorCode()));
    }
}

案例二

我们有一个API,它需要有一个或多个异常以其他格式处理,与其他应用程序的 API 不同。

Image

exception2

我们可以采取两种方式来实现这种情况。我们可以在 OtherController 内部添加 @ExceptionHandler 来处理 OtherException ,或者为 OtherController 创建新的@ControllerAdvice,以备我们也想在其他 API 中处理 OtherException

OtherController 中添加 @ExceptionHandler 来处理 OtherException 的代码示例。

@RestController
@RequestMapping("/other")
public class OtherController {
    @ExceptionHandler(OtherException.class)
    protected ResponseEntity<Error> handleException(OtherException ex) {
      MyOtherError myOtherError = MyOtherError.builder()
                         .message(ex.getMessage())
                         .origin("Other API")
                         .code(ex.getErrorCode()).build();
      return new ResponseEntity(myOtherError,
                               HttpStatus.valueOf(ex.getErrorCode()));
    }
}

只针对 OtherController 控制器的 @ControllerAdvice 的代码示例

@ControllerAdvice(assignableTypes = OtherController.class)
public class OtherExceptionHandler {
    @ExceptionHandler(OtherException.class)
    protected ResponseEntity<Error> handleException(OtherException ex) {
      MyOtherError myOtherError = MyOtherError.builder()
                         .message(ex.getMessage())
                         .origin("Other API")
                         .code(ex.getErrorCode()).build();
      return new ResponseEntity(myOtherError,
                               HttpStatus.valueOf(ex.getErrorCode()));
    }
}

案例三

与案例二类似,我们有一个 API 需要以不同于应用程序中其他 API 的方式对异常进行格式化,但这次所有的异常都需要进行不同的转换。

Image

exception3

为了实现这个案例,我们将不得不使用两个 @ControllerAdvice,并加上 @Order 注解的注意事项。因为现在我们需要告诉 Spring,在处理同一个异常时,哪个 @ControllerAdvice 的优先级更高。如果我们没有指定 @Order,在启动时,其中一个处理程序将自动注册为更高的顺序,我们的异常处理将变得不可预测。例如,我最近看到一个案例,如果你使用 mvn springboot:run 任务启动一个应用程序,OtherExceptionHandler 是主要的,但是当以jar形式启动时,GeneralExceptionHandler 是主要的。

@ControllerAdvice
public class GeneralExceptionHandler {
    @ExceptionHandler(Exception.class)
    protected ResponseEntity<Error> handleException(Exception ex) {
       MyError myError = MyError.builder()
                         .text(ex.getMessage())
                         .code(ex.getErrorCode()).build();
       return new ResponseEntity(myError,
                               HttpStatus.valueOf(ex.getErrorCode()));
    }
}
@ControllerAdvice(assignableTypes = OtherController.class)
@Order(Ordered.HIGHEST_PRECEDENCE)
public class OtherExceptionHandler {
    @ExceptionHandler(Exception.class)
    protected ResponseEntity<Error> handleException(Exception ex) {
       MyError myError = MyError.builder()
                         .message(ex.getMessage())
                         .origin("Other API")
                         .code(ex.getErrorCode()).build();
       return new ResponseEntity(myError,
                               HttpStatus.valueOf(ex.getErrorCode()));
    }
}

@ExceptionHandler 运行原理分析

ExceptionHandler的初始化

上面讲了通过xml配置的方式或者@ExceptionHandler注解的方式可以得到ExceptionHandler。是如何做到的呢?

Spring容器初始化阶段,在初始化ExceptionHandlerExceptionResolver的时候,会执行afterPropertiesSet()方法,这是Bean生命周期中的一步。

img
img

进一步的初始化Bean。在ExceptionHandlerExceptionResolver的afterPropertiesSet()方法中,会调用initExceptionHandlerAdviceCache()代码如下:

private void initExceptionHandlerAdviceCache() {
   if (getApplicationContext() == null) {
      return;
   }
   // 扫描 @ControllerAdvice 注解的Bean,并进行排序
   List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
   // 遍历 ControllerAdviceBean 数组
   for (ControllerAdviceBean adviceBean : adviceBeans) {
      Class<?> beanType = adviceBean.getBeanType();
      if (beanType == null) {
         throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
      }
      // 扫描该 ControllerAdviceBean 对应的类型
      ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
      // 有 @ExceptionHandler 注解,则添加到 exceptionHandlerAdviceCache 中
      if (resolver.hasExceptionMappings()) {
         this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
      }
      // 如果该 beanType 类型是 ResponseBodyAdvice 子类,则添加到 responseBodyAdvice 中
      if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
         this.responseBodyAdvice.add(adviceBean);
      }
   }

   if (logger.isDebugEnabled()) {
      int handlerSize = this.exceptionHandlerAdviceCache.size();
      int adviceSize = this.responseBodyAdvice.size();
      if (handlerSize == 0 && adviceSize == 0) {
         logger.debug("ControllerAdvice beans: none");
      }
      else {
         logger.debug("ControllerAdvice beans: " +
               handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");
      }
   }
}

首先找到加了注解@ControllerAdvice的Bean

遍历找到的所有的Bean,根据Bean类型构建ExceptionHandlerMethodResolver对象

ExceptionHandlerMethodResolver(beanType)方法如下:

public ExceptionHandlerMethodResolver(Class<?> handlerType) {
   // 遍历 @ExceptionHandler 注解的方法
   for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
      // 遍历处理的异常集合
      for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
         // 添加到 mappedMethods 中
         addExceptionMapping(exceptionType, method);
      }
   }
}

逻辑很简单,就是遍历这个类中的所有的方法,找到加了注解@ExceptionHandler的方法。然后遍历这个方法中的异常类型的映射。也就是该方法可以处理的异常类型。将它添加到mappedMethods当中。key是异常类型,value是对应的异常处理的方法。

这样一个类就对应这一个ExceptionHandlerMethodResolver对象,保存在exceptionHandlerAdviceCache缓存当中。就可以做到不仅一个Controller可以使用了。单例池中结果如下:

在这里插入图片描述

之后在DispatcherServlet初始化的时候,会调用initHandlerExceptionResolvers(),该方法从spring容器中找HandlerExceptionResolver类型的Bean,添加到成员变量handlerExceptionResolvers当中。并排序。

对应xml配置的就更为简单了。

容器初始化的时候,扫描xml配置文件,解析Bean标签,构建SimpleMappingExceptionResolver对象。填充xml中配置的属性。就完成了SimpleMappingExceptionResolver的初始化。

ExceptionHandler的触发时机

doDispatch中有一个局部变量Exception dispatchException = null,用于存储catch到的异常。并在调用processDispatchResult的时候,会将这个局部变量传入。processDispatchResult代码如下:


private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
      @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
      @Nullable Exception exception) throws Exception {

   boolean errorView = false;

   if (exception != null) {//异常视图处理
      if (exception instanceof ModelAndViewDefiningException) {
         logger.debug("ModelAndViewDefiningException encountered", exception);
         mv = ((ModelAndViewDefiningException) exception).getModelAndView();
      }
      else {
         Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
         mv = processHandlerException(request, response, handler, exception);//执行异常处理
         errorView = (mv != null);
      }
   }

   // Did the handler return a view to render?
   if (mv != null && !mv.wasCleared()) {
      render(mv, request, response);//解析视图 分发结果
      if (errorView) {
         WebUtils.clearErrorRequestAttributes(request);
      }
   }
   else {
      if (logger.isTraceEnabled()) {
         logger.trace("No view rendering, null ModelAndView returned.");
      }
   }

   if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
      // Concurrent handling started during a forward
      return;
   }

   if (mappedHandler != null) {
      // Exception (if any) is already handled..
      mappedHandler.triggerAfterCompletion(request, response, null);//触发拦截器完成处理
   }
}

调用processHandlerException来调用异常处理器处理异常,代码如下:

public ModelAndView resolveException(
      HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
   // 判断是否可以应用
   if (shouldApplyTo(request, handler)) {
      // 阻止缓存
      prepareResponse(ex, response);
      // 执行解析异常,返回 ModelAndView 对象 由子类实现
      ModelAndView result = doResolveException(request, response, handler, ex);
      // 如果 ModelAndView 对象非空,则进行返回
      if (result != null) {
         // Print debug message when warn logger is not enabled.
         if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
            logger.debug("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
         }
         // Explicitly configured warn logger in logException method.
         // 打印异常日志
         logException(ex, request);
      }
      // 返回 ModelAndView 对象
      return result;
   }
   else {
      return null;
   }
}

真正的异常处理是调用doResolveException,由子类实现,根据不同类型的HandlerExceptionResolver执行不同的逻辑。

根据上面异常处理的时机,可以得出的结论是异常处理拦截的是视图解析之前的逻辑。也就是从getHandler开始到执行了拦截器后置处理的地方。

总结

经过上述的几个案例,指北君觉得大家应该已经能够轻松应对 Spring Boot 中大部分的全局异常处理的情况。

细心的同学也许会觉得为什么不使用 @RestControllerAdvice 呢?如果是用的 @RestControllerAdvice 注解,它会将数据自动转换成JSON格式,不再需要 ResponseEntity 的处理来。这种与 ControllerRestController 类似,本质是一样的,所以我们在使用全局异常处理之后可以进行灵活的选择处理。

参考文档

Spring异常处理@ExceptionHandler

web九大组件之---HandlerExceptionResolver异常处理器使用详解【享学Spring MVC】

ExceptionHandlerExceptionResolver类源码解析

SpringBoot源码解析-ExceptionHandler处理异常的原理

SpringMVC源码解析---ExceptionHandler异常处理实现原理

SpringMVC重要注解(二)@ControllerAdvice

posted @ 2021-08-03 13:36  satire  阅读(497)  评论(0编辑  收藏  举报