1
在SpringMVC中,我们如果需要开启注解,即:我们经常使用的@RequestMapping,@Controller注解需要生效,通常使用SpringMVC框架提供的标签"<mvc:annotation-driven/>"来完成。那么这个标签是如何来完成注解驱动让注解生效呢?下面来揭晓一下生效过程!

1、标签干了什么事?

首先要知道标签干了什么事儿,先得找到这个标签咋解析的。关于标签解析,之前文章中有过详细的介绍,可以参考文章:《Spring标签生效可真不容易》,此处我们直接找到对应的标签解析器AnnotationDrivenBeanDefinitionParser这个类,然后找到类中的parse方法,核心操作就是通过new的方式创建了几个BeanDefinition,后放到容器中。其中就有一个比较重要的Definition,名称为RequestMappingHandlerMapping。当然了,还有其他几个BeanDefinition,别着急,我们后面再介绍。

总结: 此处标签可以暂且认为干了一件事,就是创建了一个类型为RequestMappingHandlerMapping的Bean定义,然后把它放到Spring的Bean定义注册中心。

2、完了需要做什么准备工作?

上述第1步完成之后,容器中就有了一个类型为RequestMappingHandlerMapping的Bean定义,那么下一步就肯定是要去通过这个Bean定义造一个Bean出来了呀。怎么造呢?先看看RequestMappingHandlerMapping的类的继承关系图,如下:
image.png

这个看似很复杂的继承关系图,其实很简单,就是实现了一堆Spring提供的扩展点接口而已。可以把Aware结尾的扩展点看一下,里面没有什么特别的操作。再看看在RequestMappingHandlerMapping中有个InitializaingBean扩展点方法的实现,点开之后,代码如下:

@Override
public void afterPropertiesSet() {
 this.config = new RequestMappingInfo.BuilderConfiguration();
 this.config.setUrlPathHelper(getUrlPathHelper());
 this.config.setPathMatcher(getPathMatcher());
 this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
 this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
 this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
 this.config.setContentNegotiationManager(getContentNegotiationManager());
  
  super.afterPropertiesSet();
}

似乎和@RequestMapping并没有啥关系,但是调用了父类的afterPropertiesSet方法,打开父类中的实现之后,会发现些许猫腻,代码如下:

@Override
public void afterPropertiesSet() {
 initHandlerMethods();
}

按照Spring框架中规范的命名习惯,可以猜到,这就是初始化处理器方法的,继续查看这个方法,可以看到如下的代码,按照Bean的名称做了一个过滤。我们之前文章中介绍过,Spring中声明的Bean,你可以指定其bean的名称,也可以不指定,不指定,默认是按照类的全限定名去生成的,肯定不以"scopedTarget."开头,所以会调用processCandicateBean方法!

protected void initHandlerMethods() {
 for (String beanName : getCandidateBeanNames()) {
  // 如果bean的名称不是以"scopedTarget."开头
  if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
   // 处理候选的Bean
   processCandidateBean(beanName);
  }
 }
 handlerMethodsInitialized(getHandlerMethods());
}

在processCandidateBean方法中,首先根据bean名称调用BeanFactory提供的getType方法获取Bean定义的class类型,然后有个很关键的判断,代码如下:

protected void processCandidateBean(String beanName) {
 Class<?> beanType = null;
 try {
  beanType = obtainApplicationContext().getType(beanName);
 }
 catch (Throwable ex) {}
 
 // 如果根据bean名称解析出来的beanType上标注有@Controler或者@RequestMapping注解,则进行请求映射方法的解析操作.
 if (beanType != null && isHandler(beanType)) {
  // 解析请求URL与Controller的映射关系
  detectHandlerMethods(beanName);
 }
}

这个if判断中的isHandler方法实现如下:

@Override
protected boolean isHandler(Class<?> beanType) {
 return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
   AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

可以看到,就是判断这个类上是否标注有@Controller注解和@RequestMapping注解。所以,可以锁定,processCandidateBean这个方法就是用来完成@RequestMapping注解功能的准备工作的。如果class上标了这两个注解的任何一个,都会去执行detectHandlerMethods方法,即:探测处理器方法。不得不佩服写Spring框架的大神们,命名都这么生动贴切!

完了之后,继续查看detectHandlerMethods方法,如下:

protected void detectHandlerMethods(Object handler) {
 // 获取Handler的class类型,即Controller对应的class
 Class<?> handlerType = (handler instanceof String ?
   obtainApplicationContext().getType((String) handler) : handler.getClass());
 if (handlerType != null) {
  // 当前的Class是否为代理类,判断方式是类的名称中是否含有$$符号
  Class<?> userType = ClassUtils.getUserClass(handlerType);
  Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
    (MethodIntrospector.MetadataLookup<T>) method -> {
     try {
      return getMappingForMethod(method, userType);
     }
     catch (Throwable ex) {}
    });
  // mapping为一个RequestMappingInfo对象
  methods.forEach((method, mapping) -> {
   Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
   registerHandlerMethod(handler, invocableMethod, mapping);
  });
 }
}

这个方法看着写的比较骚气,但是只需要确认三点即可:

(1)返回的methods为一个Map类型,key为方法对象,value是什么什么玩意儿?

查看MethodIntrospector的selectMethods方法实现,其实这个value就是这个方法第二个回调函数的返回结果,即:getMappingForMethod的结果。查看这个getMappingForMethod方法,它的实现在RequestMappingHandlerMapping中,代码如下:

@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
 // 将方法上的@RequestMapping注解解析为RequestMappingInfo对象
 RequestMappingInfo info = createRequestMappingInfo(method);
 if (info != null) {
  // 将类上的@RequestMapping注解解析为RequestMappingInfo对象
  RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
  // 如果类上有注解,则会将类上的@RequestMapping和方法上的@RequestMapping中的信息合并,例如:将requestUri合并
  if (typeInfo != null) {
   info = typeInfo.combine(info);
  }
  String prefix = getPathPrefix(handlerType);
  if (prefix != null) {
   info = RequestMappingInfo.paths(prefix).build().combine(info);
  }
 }
 return info;
}

可以看到这个方法的返回值类型是RequestMappingInfo类型,根据名称和这个方法的实现逻辑可以看到,这个对象就是用来封装方法和处理器的映射关系的。

(2)getMappingForMethod是如何解析HandlerMethod的?

它将类上的@RequestMapping注解信息和方法上的@RequestMapping信息做一个合并,通常就是我们的请求uri合并。即:"类上有个@RequestMapping("/api/user"),listUser方法上有个@RequestMapping("/listUser")",通过typeInfo.combine之后,就变成了/api/user/listUser。再次赞一下Spring中的方法命名,太贴切了。

在上述代码中createRequestMapping的时候,底层使用了建造者设计模式,代码看着特别优雅,如下:

protected RequestMappingInfo createRequestMappingInfo(
  RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
 // 创建RequestMappingInfo对象
 RequestMappingInfo.Builder builder = RequestMappingInfo
   .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
   .methods(requestMapping.method())
   .params(requestMapping.params())
   .headers(requestMapping.headers())
   .consumes(requestMapping.consumes())
   .produces(requestMapping.produces())
   .mappingName(requestMapping.name());
 if (customCondition != null) {
  builder.customCondition(customCondition);
 }
 return builder.options(this.config).build();
}

所以就算代码没看明白,学习一下大佬们的代码习惯也挺好!

(3)得到methods之后,循环中的registerHandlerMethod是把这个方法注册到了哪?

打开registerHandlerMethod方法之后,发现注册处理器方法是通过一个映射器注册中心MappingRegistry来完成,代码如下:

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
 this.mappingRegistry.register(mapping, handler, method);
}

查看这个MappingRegistry的register方法,就看到保存@RequestMapping请求路径和处理器方法的真实逻辑了,如下:

public void register(T mapping, Object handler, Method method) {
 // 因为是在Spring启动的时候注册的,每个虚拟机上可能存在着重复注册的可能性,保证线程安全
 this.readWriteLock.writeLock().lock();
 try {
  // 创建HandlerMethod对象
  HandlerMethod handlerMethod = createHandlerMethod(handler, method);
  // 校验HandlerMethod的唯一性
  assertUniqueMethodMapping(handlerMethod, mapping);
  // 注册RequestHandlerMappingInfo和HandlerMethod的对应关系
  this.mappingLookup.put(mapping, handlerMethod);
  // 查找URL中带有匹配符的url
  List<String> directUrls = getDirectUrls(mapping);
  for (String url : directUrls) {
   this.urlLookup.add(url, mapping);
  }
  String name = null;
  // 如果设置了Mapping名称的生成策略,则使用策略生成Mapping的名称
  // 在创建RequestMappingHandlerMapping的时候设置的名称生成策略为:RequestMappingInfoHandlerMethodMappingNamingStrategy
  if (getNamingStrategy() != null) {
   name = getNamingStrategy().getName(handlerMethod, mapping);
   addMappingName(name, handlerMethod);
  }
  // 初始化跨域配置
  CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
  if (corsConfig != null) {
   this.corsLookup.put(handlerMethod, corsConfig);
  }
  this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
 }
 finally {
  this.readWriteLock.writeLock().unlock();
 }
}

在这个方法中也有两个重要的点:
① 首先有个assertUniqueMethodMapping方法,这个方法就是用来校验RequestMapping是否唯一,有时候多个Controller中的@RequestMapping中的path写重复了,启动报错就是被这给校验住了;

② SpringMVC请求路径和处理器的映射关系,并不是直接一个url和method的映射关系,而是保存了好几套的映射关系,包括:a.mapping和HandlerMethod的;b.url和mapping的;c.mapping和MappingRegistration的,这个MappingRegistration中其实是在HandlerMethod的基础上多了一部分信息而已;d.HandlerMethod和corsConfig的,corsConfig是跨域时会使用到;

总结:在Spring进行Bean的创建过程中,通过执行InitializingBean的扩展点方法afterPropertiesSet来完成RequestMapping和HandlerMethod对应关系的解析,并注册到映射器注册中心里面,完成准备工作!

3、准备好了之后,如果执行?

篇幅太长,欲知后事如何,请期待下次分解。

Spring源码类文章列表:https://segmentfault.com/a/11...


夏日寒冰
318 声望86 粉丝

忠实的技术爱好者,追求极致,喜欢总结一些自己用过的技术点,与他人交流分享。