在SpringBoot的开发中,为了进步程序运行的鲁棒性,咱们常常须要对各种程序异样进行解决,然而如果在每个出异样的中央进行独自解决的话,这会引入大量业务不相干的异样解决代码,减少了程序的耦合,同时将来想扭转异样的解决逻辑,也变得比拟艰难。这篇文章带大家理解一下如何优雅的进行全局异样解决。

为了实现全局拦挡,这里应用到了Spring中提供的两个注解,@RestControllerAdvice@ExceptionHandler,联合应用能够拦挡程序中产生的异样,并且依据不同的异样类型别离解决。上面我会先介绍如何利用这两个注解,优雅的实现全局异样的解决,接着解释这背地的原理。

1. 如何实现全局拦挡?

1.1 自定义异样解决类

在上面的例子中,咱们继承了ResponseEntityExceptionHandler并应用@RestControllerAdvice注解了这个类,接着联合@ExceptionHandler针对不同的异样类型,来定义不同的异样解决办法。这里能够看到我解决的异样是自定义异样,后续我会开展介绍。

ResponseEntityExceptionHandler中包装了各种SpringMVC在解决申请时可能抛出的异样的解决,处理结果都是封装成一个ResponseEntity对象。ResponseEntityExceptionHandler是一个抽象类,通常咱们须要定义一个用来解决异样的应用@RestControllerAdvice注解标注的异样解决类来继承自ResponseEntityExceptionHandler。ResponseEntityExceptionHandler中为每个异样的解决都独自定义了一个办法,如果默认的解决不能满足你的需要,则能够重写对某个异样的解决。
@Log4j2  @RestControllerAdvice  public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {        /**       * 定义要捕捉的异样 能够多个 @ExceptionHandler({})     *       * @param request  request       * @param e        exception       * @param response response       * @return 响应后果       */      @ExceptionHandler(AuroraRuntimeException.class)      public GenericResponse customExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {          AuroraRuntimeException exception = (AuroraRuntimeException) e;           if (exception.getCode() == ResponseCode.USER_INPUT_ERROR) {             response.setStatus(HttpStatus.BAD_REQUEST.value());         } else if (exception.getCode() == ResponseCode.FORBIDDEN) {             response.setStatus(HttpStatus.FORBIDDEN.value());         } else {             response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());         }            return new GenericResponse(exception.getCode(), null, exception.getMessage());      }        @ExceptionHandler(NotLoginException.class)      public GenericResponse tokenExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {          log.error("token exception", e);          response.setStatus(HttpStatus.FORBIDDEN.value());          return new GenericResponse(ResponseCode.AUTHENTICATION_NEEDED);      }    }

1.2 定义异样码

这里定义了常见的几种异样码,次要用在抛出自定义异样时,对不同的情景进行辨别。

@Getter  public enum ResponseCode {        SUCCESS(0, "Success"),        INTERNAL_ERROR(1, "服务器外部谬误"),        USER_INPUT_ERROR(2, "用户输出谬误"),        AUTHENTICATION_NEEDED(3, "Token过期或有效"),        FORBIDDEN(4, "禁止拜访"),        TOO_FREQUENT_VISIT(5, "拜访太频繁,请劳动一会儿");        private final int code;        private final String message;        private final Response.Status status;        ResponseCode(int code, String message, Response.Status status) {          this.code = code;          this.message = message;          this.status = status;      }        ResponseCode(int code, String message) {          this(code, message, Response.Status.INTERNAL_SERVER_ERROR);      }    }

1.3 自定义异样类

这里我定义了一个AuroraRuntimeException的异样,就是在下面的异样处理函数中,用到的异样。每个异样实例会有一个对应的异样码,也就是后面刚定义好的。

@Getter  public class AuroraRuntimeException extends RuntimeException {        private final ResponseCode code;        public AuroraRuntimeException() {          super(String.format("%s", ResponseCode.INTERNAL_ERROR.getMessage()));          this.code = ResponseCode.INTERNAL_ERROR;      }        public AuroraRuntimeException(Throwable e) {          super(e);          this.code = ResponseCode.INTERNAL_ERROR;      }        public AuroraRuntimeException(String msg) {          this(ResponseCode.INTERNAL_ERROR, msg);      }        public AuroraRuntimeException(ResponseCode code) {          super(String.format("%s", code.getMessage()));          this.code = code;      }        public AuroraRuntimeException(ResponseCode code, String msg) {          super(msg);          this.code = code;      }    }

1.4 自定义返回类型

为了保障各个接口的返回对立,这里专门定义了一个返回类型。

@Getter  @Setter  public class GenericResponse<T> {        private int code;        private T data;        private String message;        public GenericResponse() {};        public GenericResponse(int code, T data) {          this.code = code;          this.data = data;      }        public GenericResponse(int code, T data, String message) {          this(code, data);          this.message = message;      }        public GenericResponse(ResponseCode responseCode) {          this.code = responseCode.getCode();          this.data = null;          this.message = responseCode.getMessage();      }        public GenericResponse(ResponseCode responseCode, T data) {          this(responseCode);          this.data = data;      }        public GenericResponse(ResponseCode responseCode, T data, String message) {          this(responseCode, data);          this.message = message;      }  }

理论测试异样

上面的例子中,咱们想获取到用户的信息,如果用户的信息不存在,能够间接抛出一个异样,这个异样会被咱们下面定义的全局异样解决办法所捕捉,而后依据不同的异样编码,实现不同的解决和返回。

public User getUserInfo(Long userId) {      // some logic        User user = daoFactory.getExtendedUserMapper().selectByPrimaryKey(userId);      if (user == null) {          throw new AuroraRuntimeException(ResponseCode.USER_INPUT_ERROR, "用户id不存在");      }          // some logic    ....}

以上就实现了整个全局异样的处理过程,接下来重点说说为什么@RestControllerAdvice@ExceptionHandler联合应用能够拦挡程序中产生的异样?

全局拦挡的背地原理?

上面会提到@ControllerAdvice注解,简略地说,@RestControllerAdvice与@ControllerAdvice的区别就和@RestController与@Controller的区别相似,@RestControllerAdvice注解蕴含了@ControllerAdvice注解和@ResponseBody注解。

接下来咱们深刻Spring源码,看看是怎么实现的,首先DispatcherServlet对象在创立时会初始化一系列的对象,这里重点关注函数initHandlerExceptionResolvers(context);.

public class DispatcherServlet extends FrameworkServlet {    // ......    protected void initStrategies(ApplicationContext context) {        initMultipartResolver(context);        initLocaleResolver(context);        initThemeResolver(context);        initHandlerMappings(context);        initHandlerAdapters(context);        // 重点关注        initHandlerExceptionResolvers(context);                initRequestToViewNameTranslator(context);        initViewResolvers(context);        initFlashMapManager(context);    }    // ......}

在initHandlerExceptionResolvers(context)办法中,会获得所有实现了HandlerExceptionResolver接口的bean并保存起来,其中就有一个类型为ExceptionHandlerExceptionResolver的bean,这个bean在利用启动过程中会获取所有被@ControllerAdvice注解标注的bean对象做进一步解决,要害代码在这里:

public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver        implements ApplicationContextAware, InitializingBean {    // ......    private void initExceptionHandlerAdviceCache() {        // ......        List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());        AnnotationAwareOrderComparator.sort(adviceBeans);        for (ControllerAdviceBean adviceBean : adviceBeans) {            ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());            if (resolver.hasExceptionMappings()) {                // 找到所有ExceptionHandler标注的办法并保留成一个ExceptionHandlerMethodResolver类型的对象缓存起来                this.exceptionHandlerAdviceCache.put(adviceBean, resolver);                if (logger.isInfoEnabled()) {                    logger.info("Detected @ExceptionHandler methods in " + adviceBean);                }            }            // ......        }    }    // ......}

当Controller抛出异样时,DispatcherServlet通过ExceptionHandlerExceptionResolver来解析异样,而ExceptionHandlerExceptionResolver又通过ExceptionHandlerMethodResolver 来解析异样, ExceptionHandlerMethodResolver 最终解析异样找到实用的@ExceptionHandler标注的办法是这里:

public class ExceptionHandlerMethodResolver {    // ......    private Method getMappedMethod(Class<? extends Throwable> exceptionType) {        List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();        // 找到所有实用于Controller抛出异样的解决办法,例如Controller抛出的异样        // 是AuroraRuntimeException(继承自RuntimeException),那么@ExceptionHandler(AuroraRuntimeException.class)和        // @ExceptionHandler(Exception.class)标注的办法都实用此异样        for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {            if (mappedException.isAssignableFrom(exceptionType)) {                matches.add(mappedException);            }        }        if (!matches.isEmpty()) {        /* 这里通过排序找到最实用的办法,排序的规定根据抛出异样绝对于申明异样的深度,例如    Controller抛出的异样是是AuroraRuntimeException(继承自RuntimeException),那么AuroraRuntimeException    绝对于@ExceptionHandler(AuroraRuntimeException.class)申明的AuroraRuntimeException.class其深度是0,    绝对于@ExceptionHandler(Exception.class)申明的Exception.class其深度是2,所以    @ExceptionHandler(BizException.class)标注的办法会排在后面 */            Collections.sort(matches, new ExceptionDepthComparator(exceptionType));            return this.mappedMethods.get(matches.get(0));        }        else {            return null;        }    }    // ......}

整个@RestControllerAdvice解决的流程就是这样,联合@ExceptionHandler就实现了对不同异样的灵活处理。


关注公众号【码老思】,第一工夫获取最通俗易懂的原创技术干货。