Spring MVC 中会配置两个容器:一个用于加载 Web 层的类,比如 Controller、HandlerMapping、ViewResolver 等,叫 web 容器;另一个容器用于加载业务逻辑相关的类,比如 service、dao 层的一些类,叫业务容器。这两个容器是父子关系,业务容器是 web 容器的父容器。父容器中的 bean 对子容器可见,而子容器中的 bean 对父容器不可见。在初始化时,父容器会先于子容器初始化,这是因为子容器中的一些 bean 可能会依赖父容器。
注:在 Sping Boot 中不再有父子容器的概念,因此这是 Spring MVC 独有的。因为面试会问到这部分概念,所以在这里总结一下。
一、父容器创建
web 应用程序启动时,servlet 容器会创建一个全局共享的上下文: ServletContext,然后会读取 web.xml 文件,将读取到的1
2
3
4
5
6
7
8
9
10
11
12
13<web-app>
<!-- 省略其他配置 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application.xml</param-value>
</context-param>
<!-- 省略其他配置 -->
</web-app>
tomcat 会在启动时创建 ContextLoaderListener 对象,这个对象会监听 ServletContext 的创建,然后在监听回调函数中创建父容器。我们来看一下它创建父容器的代码:1
2
3
4
5
6
7
8
9
10public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
// 省略部分代码
public void contextInitialized(ServletContextEvent event) {
// 初始化父容器
initWebApplicationContext(event.getServletContext());
}
}
1 | public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { |
进入创建容器的代码:1
2
3
4
5
6
7
8
9protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
// 判断创建什么类型的容器,默认类型为 XmlWebApplicationContext
Class<?> contextClass = determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(...);
}
// 通过反射创建容器
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
1 | protected Class<?> determineContextClass(ServletContext servletContext) { |
一句话就是根据配置文件的内容获取父容器的类型,然后通过反射创建容器。接下来再看看配置并刷新容器的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// 从 ServletContext 中获取用户配置的 contextId 属性
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
// 设置容器 id
wac.setId(idParam);
}
else {
// 用户未配置 contextId,则设置一个默认的容器 id
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
wac.setServletContext(sc);
// 获取 contextConfigLocation 配置
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
customizeContext(sc, wac);
// 刷新容器
wac.refresh();
}
说白了也很简单,就是从配置文件获取容器 id,没有的话就默认设置一个,然后获取配置文件的路径,放入容器中,刷新容器,之后就是大家熟悉的过程了。
总结一下父容器的创建过程:
- 创建 listener 节点中的 ContextLoaderListener 实例
- 创建过程中初始化 webapplicationContext,也即是父容器对象
- 从 ServletContext 中获取 contextConfigLocation 的值,这是父容器配置文件的路径,把路径放入父容器中
- 根据配置信息刷新父容器
- 将父容器保存在 ServletContext 中
二、子容器创建
子容器的加载发生在 DispatcherServlet 初始化的时候,而 DispatcherServlet 的初始化发生在第一个请求到达的时候,因此能确保 Servlet 的初始化发生在 listener 初始化之后。初始化 DispatcherServlet 需要在 web.xml 中添加如下配置:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<web-app>
<!-- 省略其他配置 -->
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 初始化参数,配置springmvc配置文件 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application-web.xml</param-value>
</init-param>
<!-- web容器启动时加载该Servlet -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<!-- 拦截所有请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 省略其他配置 -->
</web-app>
初始化的入口在 DispatcherServlet 的 init 方法中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public final void init() throws ServletException {
if (logger.isDebugEnabled()) {...}
// 获取 ServletConfig 中的配置信息
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
// 为 DispatcherServlet 对象创建一个 BeanWrapper,方便读/写对象属性。
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
// 设置配置信息到目标对象中
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {...}
throw ex;
}
}
// 进行后续的初始化
initServletBean();
if (logger.isDebugEnabled()) {...}
}
上面的源码主要做的事情是将 web.xml 中的配置信息设置到 DispatcherServlet 对象中,容器的初始化在 initServletBean 中:1
2
3
4
5
6
7
8
9protected final void initServletBean() throws ServletException {
// ...
try {
// 初始化容器
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
// ...
}
1 | protected WebApplicationContext initWebApplicationContext() { |
以上就是创建子容器的源码,下面总结一下该容器创建的过程,我们分成 Spring MVC 和 Spring Boot 两条路线:
Spring MVC 路线:
- 从 ServletContext 中获取父容器
- 如果已有外部设置的子容器的话,设置父容器、刷新子容器
- 尝试从 ServletContext 中获取子容器,若子容器不为空,则无需执行步骤4
- 创建子容器,并设置父容器
- 刷新子容器
- 缓存子容器到 ServletContext 中
Spring Boot 路线:
- 从 ServletContext 中获取父容器
- 获取 this.webApplicationContext 的“子”容器,其实就是父容器
- 刷新容器
- 缓存容器到 ServletContext 中
这里子容器的创建分为两条路线,Spring MVC 路线和 Spring Boot 路线。不过最终它们都会调用 onRefresh 刷新容器。接下来我们看一下 onRefresh 方法做了什么:1
2
3protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
1 | protected void initStrategies(ApplicationContext context) { |
我们重点关注一下 initHandlerMappings
和 initHandlerAdapters
两个方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
// 如果配置的是找到所有 HandlerMapping,则找到容器和父容器中所有的 HandlerMapping
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
// 将所有 HandlerMapping 保存在 DispatcherServlet 中
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// 对 HandlerMapping 排序
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
// 如果配置的是只找一个 HandlerMapping,则找到容器中名称为 "handlerMapping" 的 HandlerMapping
HandlerMapping hm = context.getBean("handlerMapping", HandlerMapping.class);
// 将找到的 HandlerMapping 保存在 DispatcherServlet 中
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {...}
}
// 如果容器中没有 HandlerMapping,就创建一个默认的保存在 DispatcherServlet 中
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
}
}
1 | private void initHandlerAdapters(ApplicationContext context) { |
上面的注释我们可以看到,initHandlerMappings
和 initHandlerAdapters
这两个方法会从容器中找出 HandlerMapping 和 HandlerAdapter 然后保存在 DispatcherServlet 中。说明 DispatcherServlet 中的 onRefresh 方法并不像我们熟悉的容器刷新一样,它只是把容器中的在 DispatcherServlet 中要用到的组件设置到 DispatcherServlet 中。而实际的 HandlerMapping 和 HandlerAdapter 和普通 bean 一样在创建的时候就初始化了。
三、小结
我们来总结一下,Sping MVC 中的容器创建分为父容器的创建和子容器的创建。父容器创建的时机在 Servlet 容器上下文创建的时候,通过监听器 ContextLoaderListener 的回调函数创建父容器。子容器创建的时机在 DispatcherServlet 初始化的时候,一般发生在第一个请求到达,由 Servlet 容器初始化。子容器的创建在我们之前熟知的容器创建步骤之后,还会执行 onRefresh 为 DispatcherServlet 设置几个重要组件,这几个组件用于处理和请求相关的工作。
四、参考资料
Spring MVC 原理探秘 - 容器的创建过程
SpringMVC工作原理之二:HandlerMapping和HandlerAdapter
Spring与SpringMVC父子容器的关系与初始化