关于spring:来看看SpringMVC核心零部件的实现原理

1次阅读

共计 7254 个字符,预计需要花费 19 分钟才能阅读完成。

在 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 的类的继承关系图,如下:

这个看似很简单的继承关系图,其实很简略,就是实现了一堆 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…

正文完
 0