服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

SpringMVC:注解@ControllerAdvice的工作原理

日期: 来源:码匠笔记收集编辑:安迪源文


来源:blog.csdn.net/andy_zhang2007/

article/details/100041219

Spring MVC中,通过组合使用注解@ControllerAdvice和其他一些注解,我们可以为开发人员实现的控制器类做一些全局性的定制,具体来讲,可作如下定制 :

  • 结合@ExceptionHandler使用 ==> 添加统一的异常处理控制器方法

  • 结合@ModelAttribute使用 ==> 使用共用方法添加渲染视图的数据模型属性

  • 结合@InitBinder使用 ==> 使用共用方法初始化控制器方法调用使用的数据绑定器

    数据绑定器涉及到哪些参数/属性需要/不需要绑定,设置数据类型转换时使用的PropertyEditor,Formatter等。

那么,@ControllerAdvice的工作原理又是怎样的呢 ?这篇文章,我们就一探究竟。

1. 注解@ControllerAdvice是如何被发现的 ?

首先,容器启动时,会定义类型为RequestMappingHandlerAdapterbean组件,这是DispatcherServlet用于执行控制器方法的HandlerAdapter,它实现了接口InitializingBean,所以自身在初始化时其方法#afterPropertiesSet会被调用执行。

@Override
public void afterPropertiesSet() {
 // Do this first, it may add ResponseBody advice beans
 initControllerAdviceCache();

   // 省略掉无关代码 
 // ...
}

从以上代码可以看出,RequestMappingHandlerAdapter bean组件在自身初始化时调用了#initControllerAdviceCache,从这个方法的名字上就可以看出,这是一个ControllerAdvice相关的初始化函数,而#initControllerAdviceCache具体又做了什么呢?我们继续来看 :

private void initControllerAdviceCache() {
 if (getApplicationContext() == null) {
  return;
 }

   // 找到所有使用了注解 @ControllerAdvice 的bean组件 
 List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
    // 排序
 AnnotationAwareOrderComparator.sort(adviceBeans);


   // this. requestResponseBodyAdvice : 
   //   用于记录所有 @ControllerAdvice + RequestBodyAdvice/ResponseBodyAdvice bean 
   // this.modelAttributeAdviceCache : 
   //   用于记录所有 @ControllerAdvice bean组件中的 @ModuleAttribute 方法
   // this.initBinderAdviceCache : 
   //  用于记录所有 @ControllerAdvice bean组件中的 @InitBinder 方法
   // 用于临时记录所有 @ControllerAdvice + RequestResponseBodyAdvice bean
 List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();

   // 遍历每个使用了注解 @ControllerAdvice 的 bean 组件
 for (ControllerAdviceBean adviceBean : adviceBeans) {
  Class<?> beanType = adviceBean.getBeanType();
  if (beanType == null) {
   throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
  }
       // 获取当前  ControllerAdviceBean 中所有使用了 @ModelAttribute 注解的方法
  Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
  if (!attrMethods.isEmpty()) {
   this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
  }
        
       // 获取当前 ControllerAdviceBean 中所有使用了 @InitMethod 注解的方法
  Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
  if (!binderMethods.isEmpty()) {
   this.initBinderAdviceCache.put(adviceBean, binderMethods);
  }
       // 如果当前 ControllerAdviceBean 继承自 RequestBodyAdvice,将其登记到 requestResponseBodyAdviceBeans
  if (RequestBodyAdvice.class.isAssignableFrom(beanType)) {
   requestResponseBodyAdviceBeans.add(adviceBean);
  }
        
       // 如果当前 ControllerAdviceBean 继承自 ResponseBodyAdvice,将其登记到 requestResponseBodyAdviceBeans  
  if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
   requestResponseBodyAdviceBeans.add(adviceBean);
  }
 }
    
 if (!requestResponseBodyAdviceBeans.isEmpty()) {
  this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
 }

 if (logger.isDebugEnabled()) {
  int modelSize = this.modelAttributeAdviceCache.size();
  int binderSize = this.initBinderAdviceCache.size();
  int reqCount = getBodyAdviceCount(RequestBodyAdvice.class);
  int resCount = getBodyAdviceCount(ResponseBodyAdvice.class);
  if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount == 0) {
   logger.debug("ControllerAdvice beans: none");
  }
  else {
   logger.debug("ControllerAdvice beans: " + modelSize + " @ModelAttribute, " + binderSize +
     " @InitBinder, " + reqCount + " RequestBodyAdvice, " + resCount + " ResponseBodyAdvice");
  }
 }
}

从以上#initControllerAdviceCache方法的实现逻辑来看,它将容器中所有使用了注解@ControllerAdvicebean或者其方法都分门别类做了统计,记录到了RequestMappingHandlerAdapter实例的三个属性中 :

  • requestResponseBodyAdvice

  • 用于记录所有@ControllerAdvice + RequestBodyAdvice/ResponseBodyAdvice bean组件

  • modelAttributeAdviceCache

  • 用于记录所有 @ControllerAdvice bean组件中的 @ModuleAttribute 方法

  • initBinderAdviceCache

  • 用于记录所有@ControllerAdvice bean组件中的 @InitBinder 方法

到此为止,我们知道,使用注解@ControllerAdvicebean中的信息被提取出来了,但是,这些信息又是怎么使用的呢 ?我们继续来看。

2. @ControllerAdvice 定义信息的使用

1. requestResponseBodyAdvice的使用

/**
 * Return the list of argument resolvers to use including built-in resolvers
 * and custom resolvers provided via {@link #setCustomArgumentResolvers}.
 */
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
 List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();

 // ... 省略无关代码        
 resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), 
  this.requestResponseBodyAdvice));
    // ... 省略无关代码
 resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), 
  this.requestResponseBodyAdvice));  
    // ... 省略无关代码
 resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), 
  this.requestResponseBodyAdvice));
 // ... 省略无关代码

 return resolvers;
}

#getDefaultArgumentResolvers方法用于准备RequestMappingHandlerAdapter执行控制器方法过程中缺省使用的HandlerMethodArgumentResolver,从上面代码可见,requestResponseBodyAdvice会被传递给RequestResponseBodyMethodProcessor/RequestPartMethodArgumentResolver/HttpEntityMethodProcessor这三个参数解析器,不难猜测,它们在工作时会使用到该requestResponseBodyAdvice,但具体怎么使用,为避免过深细节影响理解,本文我们不继续展开。

推荐:超级全面的 SpringBoot 注解介绍

方法#getDefaultArgumentResolvers也在RequestMappingHandlerAdapter初始化方法中被调用执行,如下所示 :

@Override
public void afterPropertiesSet() {
 // Do this first, it may add ResponseBody advice beans
 initControllerAdviceCache();

 if (this.argumentResolvers == null) {
  List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); // <==
  this.argumentResolvers = new HandlerMethodArgumentResolverComposite()
          .addResolvers(resolvers);
 }

    // 省略无关代码
}

2. modelAttributeAdviceCache的使用

private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
 SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);
 Class<?> handlerType = handlerMethod.getBeanType();
 Set<Method> methods = this.modelAttributeCache.get(handlerType);
 if (methods == null) {
  // 获取当前控制器类中使用了 @ModelAttribute 的方法
  methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
  this.modelAttributeCache.put(handlerType, methods);
 }
 List<InvocableHandlerMethod> attrMethods = new ArrayList<>();
 // Global methods first
 // 遍历@ControllerAdvice bean中所有使用了 @ModelAttribute 的方法,
 // 将其包装成 InvocableHandlerMethod 放到 attrMethods
 // ********* 这里就是 modelAttributeAdviceCache 被使用到的地方了 ************
 this.modelAttributeAdviceCache.forEach((clazz, methodSet) -> {
  if (clazz.isApplicableToBeanType(handlerType)) {
   Object bean = clazz.resolveBean();
   for (Method method : methodSet) {
    attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
   }
  }
 });
    
 // 遍历当前控制器类中中所有使用了 @ModelAttribute 的方法,
 // 也将其包装成 InvocableHandlerMethod 放到 attrMethods        
 for (Method method : methods) {
  Object bean = handlerMethod.getBean();
  attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
 }
    
 // 此时  attrMethods 包含了两类 InvocableHandlerMethod, 分别来自于 :
 // 1. @ControllerAdvice bean 中所有使用了 @ModelAttribute 的方法
 // 2. 当前控制器类中中所有使用了 @ModelAttribute 的方法
 return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
}

/ 从指定 bean 的方法 method ,其实是一个使用了注解 @ModelAttribute 的方法,
/ 构造一个 InvocableHandlerMethod 对象
private InvocableHandlerMethod createModelAttributeMethod(WebDataBinderFactory factory, 
  Object bean, Method method) {
 InvocableHandlerMethod attrMethod = new InvocableHandlerMethod(bean, method);
 if (this.argumentResolvers != null) {
  attrMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
 }
 attrMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
 attrMethod.setDataBinderFactory(factory);
 return attrMethod;

从此方法可以看到,#getModelFactory方法使用到了modelAttributeAdviceCache,它会根据其中每个元素构造成一个InvocableHandlerMethod,最终传递给要创建的ModelFactory对象。而#getModelFactory又在什么时候被使用呢 ? 它会在RequestMappingHandlerAdapter执行一个控制器方法的准备过程中被调用,如下所示 :

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
  HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

 ServletWebRequest webRequest = new ServletWebRequest(request, response);
 try {
        // 构造调用 handlerMethod 所要使用的数据绑定器工厂  
  WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        // 构造调用 handlerMethod 所要使用的数据模型工厂  
  ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
      // 省略无关代码 ...            
}

3. initBinderAdviceCache的使用

private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
 Class<?> handlerType = handlerMethod.getBeanType();
 Set<Method> methods = this.initBinderCache.get(handlerType);
 if (methods == null) {
      // 获取当前控制器类中使用了 @InitBinder 的方法  
  methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
  this.initBinderCache.put(handlerType, methods);
 }
 List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
 // Global methods first
 // 遍历@ControllerAdvice bean中所有使用了 @InitBinder 的方法,
 // 将其包装成 InvocableHandlerMethod 放到 initBinderMethods
 // ********* 这里就是 initBinderAdviceCache 被使用到的地方了 ************        
 this.initBinderAdviceCache.forEach((clazz, methodSet) -> {
  if (clazz.isApplicableToBeanType(handlerType)) {
   Object bean = clazz.resolveBean();
   for (Method method : methodSet) {
    initBinderMethods.add(createInitBinderMethod(bean, method));
   }
  }
 });
    
 // 遍历当前控制器类中所有使用了 @InitBinder 的方法,
 // 将其包装成 InvocableHandlerMethod 放到 initBinderMethods        
 for (Method method : methods) {
  Object bean = handlerMethod.getBean();
  initBinderMethods.add(createInitBinderMethod(bean, method));
 }
    
 // 此时  initBinderMethods 包含了两类 InvocableHandlerMethod, 分别来自于 :
 // 1. @ControllerAdvice bean 中所有使用了 @InitBinder 的方法
 // 2. 当前控制器类中中所有使用了 @InitBinder 的方法        
 return createDataBinderFactory(initBinderMethods);
}

/ 从指定 bean 的方法 method ,其实是一个使用了注解 @InitBinder 的方法,
/ 构造一个 InvocableHandlerMethod 对象
private InvocableHandlerMethod createInitBinderMethod(Object bean, Method method) {
 InvocableHandlerMethod binderMethod = new InvocableHandlerMethod(bean, method);
 if (this.initBinderArgumentResolvers != null) {
  binderMethod.setHandlerMethodArgumentResolvers(this.initBinderArgumentResolvers);
 }
 binderMethod.setDataBinderFactory(new DefaultDataBinderFactory(this.webBindingInitializer));
 binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
 return binderMethod;
}

/**
 * Template method to create a new InitBinderDataBinderFactory instance.
 * <p>The default implementation creates a ServletRequestDataBinderFactory.
 * This can be overridden for custom ServletRequestDataBinder subclasses.
 * @param binderMethods {@code @InitBinder} methods
 * @return the InitBinderDataBinderFactory instance to use
 * @throws Exception in case of invalid state or arguments
 */
protected InitBinderDataBinderFactory createDataBinderFactory(
          List<InvocableHandlerMethod> binderMethods)
  throws Exception {

 return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}

从此方法可以看到,#getDataBinderFactory方法使用到了initBinderAdviceCache,它会根据其中每个元素构造成一个InvocableHandlerMethod,最终传递给要创建的InitBinderDataBinderFactory对象。而#getDataBinderFactory又在什么时候被使用呢 ? 它会在RequestMappingHandlerAdapter执行一个控制器方法的准备过程中被调用,如下所示 :

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
  HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

 ServletWebRequest webRequest = new ServletWebRequest(request, response);
 try {
        // 构造调用 handlerMethod 所要使用的数据绑定器工厂  
  WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        // 构造调用 handlerMethod 所要使用的数据模型工厂  
  ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
      // 省略无关代码 ...            
}

到此为止,我们基本上可以看到,通过@ControllerAdvice注解的bean组件所定义的@ModelAttribute/@InitBinder方法,或者RequestBodyAdvice/ResponseBodyAdvice,是如何被RequestMappingHandlerAdapter提取和使用的了。虽然我们并未深入到更细微的组件研究它们最终的使用,不过结合这些组件命名以及这些更深一层的使用者组件的名称,即便是猜测,相信你也不难理解猜到它们如何被使用了。

不知道你注意到没有,关于@ControllerAdvice@ExceptionHandler这一组合,在上面提到的RequestMappingHandlerAdapter逻辑中,并未涉及到。那如果使用了这种组合,又会是怎样一种工作机制呢 ?事实上,@ControllerAdvice@ExceptionHandler这一组合所做的定义,会被ExceptionHandlerExceptionResolver消费使用。不过关于ExceptionHandlerExceptionResolver我们会另外行文介绍,通过这篇文章中的例子,理解@ControllerAdvide的工作原理已经不是问题了。


如本文让你有所收获,欢迎扫描上方二维码
关注微信公众号,更多内容持续更新中
点击关注公众号后,回复面试,可以领取我熬夜整理的大厂面经.PDF

相关阅读

  • flutter:一个bug的源码分析

  • 由一个bug引发的flutter的widget跟element关系的源码分析bug现象在页面本来有照片数据的(第一张照片数据),点击加号唤起系统拍照功能后,再返回页面A,原来的照片数据丢失了(部分And
  • Android 技术面试如何做好准备?

  • 这是 JsonChao 的第 210 期分享超友们,早上好,中秋节快乐~今天的干货来点轻松一点的,这次的分享是《技术面试如何做好准备?》,主要分为三个部分:第一部分:面试前。第二部分:面试中。
  • 芳芳频道|李艳芳三套卷复盘(数三①)

  • 往期直达数一复盘(已完结)数二复盘(已完结)李艳芳三套卷划重点数三第一套总体作为数三的第一套,自我感觉还是比较恰当的,整体计算量不大,概率题都很常规,较好得分,高数题和线代题不乏
  • 新文速递 - 9.22/9.29

  • 纳米粒子修饰的微型机器人用于治疗急性细菌性肺炎的体内抗生素递送Biohybrid microrobots consisting of nanoparticle-modified microalgae are constructed for active dr
  • 新文速递:11.28-12.2

  • 基于二甲基铵阳离子添加剂的中间相工程用于稳定钙钛矿太阳能电池The stability of halide perovskite solar cells, determined by film morphology, is paramount to their

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • SpringMVC:注解@ControllerAdvice的工作原理

  • 来源:blog.csdn.net/andy_zhang2007/article/details/100041219Spring MVC中,通过组合使用注解@ControllerAdvice和其他一些注解,我们可以为开发人员实现的控制器类做一些全局
  • 探索Android开源框架 - ARouter使用及源码解析

  • 最近对项目进行组件化重构,要进行组件化就少不了组件间通信的路由,于是就再次研究了一下阿里的ARouter,关于组件化可以查看我之前写的组件化架构系列文章,这里就不过多提及了
  • 聊一聊 AS 的一些好用的功能

  • 文章开始前先墨迹几句,好久没写文章了,这段时间公司确实挺忙,也没抽出时间,上一篇文章还是三月初写的,距今已经两个多月啦,不能再这样下去了,虽然我不能像一些大佬那样周更、甚至日
  • 漫画:为什么程序员没有女朋友?

  • 原因一,程序员实在太忙了在很多互联网公司,程序员996是常态,而且就算在业余时间,程序员也需要经常看书看源码,或者是访问某个全球最大的同性交友网站......他们忙着提升自己的技