接上篇,我们实现了一个简单的SpringMVC小程序。麻雀虽小,五脏俱全,里面包含了一个web工程基本的组件。现在我们开始剖析源码,分为如下几个步骤
- 1.Tomcat如何启动两个容器—Spring容器、SpringMVC容器
- 2.处理@RequestMapping注解,建立<url,controller(中的方法)>关系。
- 3.初始化DispacherServlet
- 4.DispacherServlet如何处理请求(前三步都是为该步骤打下基础,具体内容放到下一篇)
注Spring的版本是5.2.12.RELEASE,不同版本略有差异,但核心逻辑是不变的。
一、Tomcat如何启动两个容器—Spring容器、SpringMVC容器
前面我们提到web项目会启动两个容器,两个容器启动过程中有一个共同点就是,初始化的方法位于AbstractApplicationContext类的refresh方法。 ?
1.先启动刷新Spring容器
该功能由ContextLoader类的initWebApplicationContext方法实现,省略一些次要代码
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
try {
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
return this.context;
}
}
栈调用如下图 
看下刷新完后的spring容器中bean 
2.接着启动刷新SpringMVC容器
该功能主要由FrameworkServlet类的createWebApplicationContext方法实现
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(getEnvironment());
wac.setParent(parent);
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
3.刷新容器
configureAndRefreshWebApplicationContext(wac);
return wac;
}
上面方法的调用过程如下 
刷新完的SpringMVC容器,只包含了UserController的实例,没有UserService的实例。图有点长,就不截了,大家可以自己验证。 ?
3.父子容器描述
1.概括上述步骤
上述步骤可以总结成下图 
简单来说就是初始化listener时初始化Spring容器,初始化Servlet时初始化SpringMVC容器。 ? 需要说明的是,在本例中父子容器其实都是XmlWebApplicationContext的实例,只是两个实例做了一个引用关系
2.这个过程需要注意以下几个点
如果把bean全部放到Spring容器会怎么样?
如果将controller的bean都放入的spring容器中,如果访问该项目会出现404的错误。 因为在解析@RequestMapping过程中,initHandlerMethods()方法只是对SpringMVC容器中的bean进行处理的,并没有找入父容器的bean。此时url和controller中的方法(或者是handler)没有生成对应关系,也就是说没有方法能够处理该请求,出现404的错误。
如果把bean全部放在SpringMVC容器会怎么样?
在使用事务时会导致失效
3.为什么要分父子容器
SpringMVC是从struct发展过来的,也就是说方便两者进行切换,而不用动Spring容器中的bean。
二、处理@RequestMapping注解,建立url和方法的关系
上面提的关系就是<url,controller(中的方法)>Map结构。要注意的是?第二部分是在SpringMVC容器初始化过程中完成。
1.说下过程中涉及的类
- RequestMappingInfo: 这个类是对请求映射的一个抽象,它包含了请求路径,请求方法,请求头等信息。其实可以看做是@RequestMapping的一个对应类。
- HandlerMethod: 这个类封装了处理器实例(Controller bean)和处理方法实例(Method)以及方法参数数组(MethodParameter[])
- HandlerMapping :该接口的实现类用来定义请求和处理器之前的映射关系,其中定义了一个方法getHandler。
- AbstractHandlerMethodMapping :这是HandlerMapping的一个基本实现类,该类定义了请求与HandlerMethod实例的映射关系。
- RequestMappingHandlerMapping: 它将@RequestMapping注解转化为RequestMappingInfo实例,并为父类使用。
2.怎么将上面的类串联起来呢?分为三个步骤
-
1.遍历注解:SpringMVC容器初始化过程中,具体在创建RequestMappingHandlerMapping类实例时,对标注了@Controller或@RequestMapping注解的类中方法进行遍历。 -
2.封装RequestMappingInfo实例:将类和方法上的**@RequestMapping注解值进行合并,封装成一个RequestMappingInfo实例**。以controller中的方法对象为key,对应的RequestMappingInfo实例为value存入map中,遍历map开始注册 -
3.注册到Map中:将这个Controller实例、方法及方法参数信息封装到HandlerMethod中。以RequestMappingInfo为key,HandlerMethod为value存储到map结构。将url(@RequestMapping注解value值)为key,以RequestMappingInfo为value存到map结构中
3.上面步骤实现细节
1.遍历注解
入口开始在RequestMappingHandlerMapping类的afterPropertiesSet方法
public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper());
this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(useSuffixPatternMatch());
this.config.setTrailingSlashMatch(useTrailingSlashMatch());
this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch());
this.config.setContentNegotiationManager(getContentNegotiationManager());
super.afterPropertiesSet();
}
回调父类AbstractHandlerMethodMapping的afterPropertiesSet方法。 下面的方法都是在AbstractHandlerMethodMapping类中,这个要有印象,在下一篇DispacherServlet逻辑处理中要用到
public void afterPropertiesSet() {
initHandlerMethods();
}
protected void initHandlerMethods() {
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}
进入父类processCandidateBean方法,从方法名字可以看出来这个是处理候选bean方法。 ?
怎么处理呢?
在这个方法里只是判断下是不是Handler通过isHandler(beanType),判断的条件就是这个bean上有没有@Controller或者@RequestMapping注解。 如果有则开始从bean中发现url和方法的对应关系,实现这个方法的是detectHandlerMethods
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
2.封装成一个RequestMappingInfo实例。
进入detectHandlerMethods方法,这个方法完成了两件事 1.得到一个Map<Method, T> methods变量,Method就是controller中的方法,T代表的是RequestMappingInfo 2.遍历methods进行注册
protected void detectHandlerMethods(Object handler) {
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
1.完成第一件事儿
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
我们看下methods的存储的数据  有了map结构之后开始注册,直接看registerHandlerMethod方法
3.注册就是向两个Map结构中存储数据
此时进入内部类MappingRegistry,根据类名可以推测出来是注册映射关系的。介绍两个成员变量
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>(); private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
mappingLookup:以RequestMappingInfo为key,HandlerMethod为value的Map结构 urlLookup:以url为key,RequestMapping集合为value的Map结构
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
validateMethodMapping(handlerMethod, mapping);
1.向第一个Map存值
this.mappingLookup.put(mapping, handlerMethod);
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}
String name = null;
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();
}
}
看下mappingLookup存完值的图 
urlLookup存完值的图  现在已经向Map中存储了数据,怎么根据一个请求找到对应的方法呢?就两个步骤 ?
上面的步骤就是下一篇DispacherServlet要写的根据url找到对应Controller中的方法。
三、初始化DispacherServlet
? 上面的过程描述了Tomcat是怎么初始化两个容器的,下面叙述下在初始化完SpringMVC容器后,如何给这个容器添加上web的全局变量,所谓的添加web的全局变量就是给DispacherServlet中的成员变量赋值,这个功能是onRefresh方法实现的。
1.onRefresh方法
**_DispacherServlet类 _**重写FrameworkServlet的onRefresh方法,主要的刷新功能放到了initStrategies来做。
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
initHandlerMappings(context)
这个方法主要就是给DispacherServlet中 List handlerMappings;属性赋值。 怎么赋值呢?
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
}
在IOC容器中找到的bean如下所示  这些bean是在SpringMVC容器初始化时加载当前系统中所有实现了HandlerMapping接口的bean。 ?
initHandlerAdapters(context)
?
该方法和初始化HandlerMapping方法如出一辙,都是从SpringMVC容器中找到 HandlerAdapter类型的bean然后给List handlerAdapters赋值
private void initHandlerAdapters(ApplicationContext context) {
this.handlerAdapters = null;
if (this.detectAllHandlerAdapters) {
Map<String, HandlerAdapter> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerAdapters = new ArrayList<>(matchingBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerAdapters);
}
}
}
看下从SpringMVC容器找到的bean 
2.总结给成员变量赋值方法
仔细看下DispacherServlet中这几个nitxxxxx方法里面有几个共同点 ?
1.代码是有大量重复代码的,不变的是赋值的DispacherServlet成员变量不同,如this.handlerAdapters = null ? 2.而且有一个共同的逻辑就是如果SpringMVC容器没有成员变量对应的bean,那么就会加载DispacherServlet配置文件中的bean
? DispacherServlet配置文件DispacherServlet.properties内容如下
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
org.springframework.web.servlet.function.support.RouterFunctionMapping
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
org.springframework.web.servlet.function.support.HandlerFunctionAdapter
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
什么时候加载这个配置文件呢?在DispacherServlet类加载的时候,位于静态代码块中
static {
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
}
而各个initxxxxx方法中都有一个判断逻辑
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
如果在mvc的配置文件中有如下配置,则不会走上面的判断。
<mvc:annotation-driven></mvc:annotation-driven>
该配置的作用就是将配置文件的各个bean注册到SpringMVC容器中。 ?
四、总结
上面最主要的点就是当一个项目启动时,会初始化两个容器(Spring容器和SpringMVC容器),为了完成web项目处理请求的功能,需要为SpringMVC容器添加web的全局变量,其实就是为DispacherServlet的成员变量赋值。
五、扩展
本来写到上面就可以停笔了,出于好奇验证了下现在SpringBoot还采用父子容器形式了吗?发现不是。 下面源码基于SpringBoot 2.1.1.RELEASE 版本,可能版本间使用的IOC容器不相同,大家可以自行验证。 ?
验证如下 从SpringBoot的main函数进去,一直到SpringApplication类的run方法,该方法中有一行去创建容器
context = createApplicationContext();
在该行代码打上断点运行  此时的容器只有一个就是**AnnotationConfigServletWebServerApplicationContext**,继续向下走,去刷新容器。 ?
查看该容器中的单例池(beanfactory属性中的singletonObjects)是否包含controller和service包下的类实例。 涉及到的对象有点多,只截取重要的信息  由上图可以发现controller和service以及视图解析器都在单例池中。 可以证明SpringBoot只使用了一个容器 ?
参考文章 mvc:annotation-driven的作用 Spring和SpringMVC父子容器概念 SpringMvc在SpringBoot环境和Web环境中上下文的关系 ?
项目git地址及相关链接
项目git地址:https://gitee.com/shang_jun_shu/springmvc-analysis? 下篇:SpringMVC源码分析(三)—DispacherServlet逻辑处理及为什么要用适配器模式
|