美文网首页
@ControllerAdvice注解作用原理

@ControllerAdvice注解作用原理

作者: YLiuY | 来源:发表于2021-08-15 15:35 被阅读0次

    在Spring里,我们可以使用@ControllerAdvice来声明一些全局性的东西,最常见的是结合@ExceptionHandler注解用于全局异常的处理。

    @ControllerAdvice是在类上声明的注解,其用法主要有三点:

    @ExceptionHandler注解标注的方法:用于捕获Controller中抛出的不同类型的异常,从而达到异常全局处理的目的;
    @InitBinder注解标注的方法:用于请求中注册自定义参数的解析,从而达到自定义请求参数格式的目的;
    @ModelAttribute注解标注的方法:表示此方法会在执行目标Controller方法之前执行 。
    看下具体用法:

    // 这里@RestControllerAdvice等同于@ControllerAdvice + @ResponseBody

    public class GlobalHandler {
        private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class);
        // 这里@ModelAttribute("loginUserInfo")标注的modelAttribute()方法表示会在Controller方法之前
        // 执行,返回当前登录用户的UserDetails对象
        @ModelAttribute("loginUserInfo")
        public UserDetails modelAttribute() {
            return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        }
        // @InitBinder标注的initBinder()方法表示注册一个Date类型的类型转换器,用于将类似这样的2019-06-10
        // 日期格式的字符串转换成Date对象
        @InitBinder
        protected void initBinder(WebDataBinder binder) {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
            dateFormat.setLenient(false);
            binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
        } 
        // 这里表示Controller抛出的MethodArgumentNotValidException异常由这个方法处理
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public Result exceptionHandler(MethodArgumentNotValidException e) {
            Result result = new Result(BizExceptionEnum.INVALID_REQ_PARAM.getErrorCode(),
                    BizExceptionEnum.INVALID_REQ_PARAM.getErrorMsg());
            logger.error("req params error", e);
            return result;
        }
        // 这里表示Controller抛出的BizException异常由这个方法处理
        @ExceptionHandler(BizException.class)
        public Result exceptionHandler(BizException e) {
            BizExceptionEnum exceptionEnum = e.getBizExceptionEnum();
            Result result = new Result(exceptionEnum.getErrorCode(), exceptionEnum.getErrorMsg());
            logger.error("business error", e);
            return result;
        }
        // 这里就是通用的异常处理器了,所有预料之外的Exception异常都由这里处理
        @ExceptionHandler(Exception.class)
        public Result exceptionHandler(Exception e) {
            Result result = new Result(1000, "网络繁忙,请稍后再试");
            logger.error("application error", e);
            return result;
        }
    
    }
    

    在Controller里取出@ModelAttribute标注的方法返回的UserDetails对象:

    RestController

    @Validated
    public class ExamController {
        @Autowired
        private IExamService examService;
        // ......
        @PostMapping("/getExamListByOpInfo")
        public Result<List<GetExamListResVo>> getExamListByOpInfo( @NotNull Date examOpDate,
                                                                  @ModelAttribute("loginUserInfo") UserDetails userDetails) {
            List<GetExamListResVo> resVos = examService.getExamListByOpInfo(examOpDate, userDetails);
            Result<List<GetExamListResVo>> result = new Result(resVos);
            return result;
        }
    
    }
    

    这里当入参为examOpDate=2019-06-10时,Spring会使用我们上面@InitBinder注册的类型转换器将2019-06-10转换examOpDate对象:

    @PostMapping("/getExamListByOpInfo")
    public Result<List<GetExamListResVo>> getExamListByOpInfo(@NotNull Date examOpDate,
                                                              @ModelAttribute("loginUserInfo") UserDetails userDetails) {
        List<GetExamListResVo> resVos = examService.getExamListByOpInfo(examOpDate, userDetails);
        Result<List<GetExamListResVo>> result = new Result(resVos);
        return result;
    }
    

    @ExceptionHandler标注的多个方法分别表示只处理特定的异常。这里需要注意的是当Controller抛出的某个异常多个@ExceptionHandler标注的方法都适用时,Spring会选择最具体的异常处理方法来处理,也就是说@ExceptionHandler(Exception.class)这里标注的方法优先级最低,只有当其它方法都不适用时,才会来到这里处理。

    下面我们看看Spring是怎么实现的,首先前端控制器DispatcherServlet对象在创建时会初始化一系列的对象:

    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);
        }
        // ......
    }
    

    对于@ControllerAdvice 注解,我们重点关注initHandlerAdapters(context)和initHandlerExceptionResolvers(context)这两个方法。

    initHandlerAdapters(context)方法会取得所有实现了HandlerAdapter接口的bean并保存起来,其中就有一个类型为RequestMappingHandlerAdapter的bean,这个bean就是@RequestMapping注解能起作用的关键,这个bean在应用启动过程中会获取所有被@ControllerAdvice注解标注的bean对象做进一步处理,关键代码在这里:

    public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
            implements BeanFactoryAware, InitializingBean {
        // ......
        private void initControllerAdviceCache() {
                    // ......
            List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
            AnnotationAwareOrderComparator.sort(adviceBeans);
    
            List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();
    
            for (ControllerAdviceBean adviceBean : adviceBeans) {
                Class<?> beanType = adviceBean.getBeanType();
                if (beanType == null) {
                    throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
                }
                            // 找到所有ModelAttribute标注的方法并缓存起来
                Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
                if (!attrMethods.isEmpty()) {
                    this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
                    if (logger.isInfoEnabled()) {
                        logger.info("Detected @ModelAttribute methods in " + adviceBean);
                    }
                }
                            // 找到所有InitBinder标注的方法并缓存起来
                Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
                if (!binderMethods.isEmpty()) {
                    this.initBinderAdviceCache.put(adviceBean, binderMethods);
                    if (logger.isInfoEnabled()) {
                        logger.info("Detected @InitBinder methods in " + adviceBean);
                    }
                }
                            // ......
            }
        }
        // ......
    }
    

    经过这里处理之后,@ModelAttribute和@InitBinder就能起作用了,至于DispatcherServlet和RequestMappingHandlerAdapter是如何交互的这就是另一个复杂的话题了,此处不赘述。

    再来看DispatcherServlet的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抛出的异常
            // 是BizException(继承自RuntimeException),那么@ExceptionHandler(BizException.class)和
            // @ExceptionHandler(Exception.class)标注的方法都适用此异常
            for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
                if (mappedException.isAssignableFrom(exceptionType)) {
                    matches.add(mappedException);
                }
            }
            if (!matches.isEmpty()) {
                    // 这里通过排序找到最适用的方法,排序的规则依据抛出异常相对于声明异常的深度,例如
                // Controller抛出的异常是BizException(继承自RuntimeException),那么BizException
                // 相对于@ExceptionHandler(BizException.class)声明的BizException.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;
            }
        }
        // ......
    }
    

    整个@ControllerAdvice处理的流程就是这样,这个设计还是非常灵活的。

    相关文章

      网友评论

          本文标题:@ControllerAdvice注解作用原理

          本文链接:https://www.haomeiwen.com/subject/njyubltx.html