Spring 源码剖析:IoC 容器在 Web 环境(Tomcat)中的启动原理
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
导读
在 Web 应用中,SpringMVC 是建立在 Spring IoC 容器之上的,理解 SpringMVC 的第一步,是搞清楚 IoC 容器是如何被载入 Web 容器(以 Tomcat 为例)并完成初始化的。本文将结合本仓库 IoC容器在Web环境中的启动.md 的主线,逐层剖析web.xml部署描述、ContextLoaderListener监听器、ContextLoader启动器、WebApplicationContext上下文体系以及XmlWebApplicationContext的实现,并穿插 SpringMVC的设计与实现.md 中的源码证据。读完本文,你将掌握 Spring 在 Web 容器中建立「根上下文 + 子上下文」层次化体系的全过程,理解DispatcherServlet与根上下文之间的双亲-子级关系,以及各类配置参数(contextConfigLocation、contextClass等)的底层作用。
1 Web 环境中的 SpringMVC:web.xml 是接口部分
Spring 的 IoC 是一个独立模块,它并不直接在 Web 容器中发挥作用。要在 Web 环境中使用 IoC 容器,Spring 必须设计一个与 Web 容器启动过程集成在一起的启动流程:一方面处理 Web 容器的启动,另一方面通过特定的 Web 容器拦截器(Listener)将 IoC 容器载入 Web 环境并完成初始化。只有这个流程完成后,IoC 容器才能正常工作,SpringMVC 才能在此基础上建立 MVC 运行机制,响应来自 Web 容器的 HTTP 请求。
以 Tomcat 为例,web.xml是应用的部署描述文件,也是 SpringMVC 与 Tomcat 之间的接口部分。典型的配置如下:
<servlet> <servlet-name>sample</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>6</load-on-startup> </servlet> <servlet-mapping> <servlet-name>sample</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>这段部署描述中包含了三部分关键信息:
- Servlet 定义:配置了 SpringMVC 的
DispatcherServlet,它是 MVC 中十分重要的类,起着分发请求的作用。<load-on-startup>6</load-on-startup>指定了 Servlet 的启动顺序与优先级。 - URL 映射:
<url-pattern>/*</url-pattern>限定了该 Servlet 需要处理的 HTTP 请求范围。 - context-param 参数:
contextConfigLocation用于指定 IoC 容器读取 Bean 的 XML 文件的路径,此处为WEB-INF/applicationContext.xml,其中包含 Spring 应用的 Bean 配置。 - 监听器:
ContextLoaderListener被定义为 Spring MVC 的启动类,该监听器与 Web 服务器的生命周期相关联,负责完成 IoC 容器在 Web 环境中的启动工作。
DispatcherServlet和ContextLoaderListener提供了在 Web 容器中对 Spring 的接口,它们与 Web 容器的耦合是通过ServletContext实现的(ServletContext是容器与应用沟通的桥梁,从一定程度上讲,ServletContext就是 Servlet 规范的体现)。ServletContext为 Spring 的 IoC 容器提供了宿主环境,Spring MVC 在其中建立起 IoC 容器体系:先通过ContextLoaderListener的初始化建立容器体系,再把DispatcherServlet作为 Spring MVC 处理 Web 请求的转发器建立起来,从而完成响应 HTTP 请求的准备。
2 IoC 容器启动的基本过程:层次化上下文体系的建立
IoC 容器的启动过程就是建立上下文的过程,该上下文与ServletContext相伴而生,是 IoC 容器在 Web 应用环境中的具体表现之一。
- 由
ContextLoaderListener启动的上下文为根上下文(Root Context); - 在根上下文之上,还有一个与 Web MVC 相关的子上下文,用来保存控制器(
DispatcherServlet)所需的 MVC 对象,作为根上下文的子上下文,构成一个层次化的上下文体系。
在 Web 容器中启动 Spring 应用程序时,首先建立根上下文,然后建立这个上下文体系,该过程由ContextLoader完成,整体时序如下图所示。
ContextLoaderListener是 Spring 提供的类,为在 Web 容器中建立 IoC 容器服务。它实现了 Servlet API 中定义的ServletContextListener接口,该接口提供了与 Servlet 生命周期结合的回调:上下文初始化contextInitialized()方法和上下文销毁contextDestroyed()方法。在 Web 容器中建立WebApplicationContext的过程是在contextInitialized()方法中完成的。另外,ContextLoaderListener还继承了ContextLoader,具体的 IoC 容器载入过程由ContextLoader完成。
在ContextLoader中,完成了两个 IoC 容器建立的基本过程:
- 在 Web 容器中建立起双亲 IoC 容器(根上下文);
- 生成相应的
WebApplicationContext并将其初始化。
3 Web 容器中的上下文设计
3.1 WebApplicationContext 接口:为 Web 环境扩展的上下文
为了方便在 Web 环境中使用 IoC 容器,Spring 为 Web 应用提供了上下文的扩展接口WebApplicationContext,其继承关系如下图所示。
在这个类继承关系中,可以从熟悉的XmlWebApplicationContext入手来了解它的接口实现。接口设计上,最终通过ApplicationContext接口与BeanFactory接口对接;而对于具体的功能实现,很多都封装在其基类AbstractRefreshableWebApplicationContext中完成(该抽象基类提供了refresh()刷新上下文的模板实现,AnnotationConfigWebApplicationContext、GenericWebApplicationContext等同级实现类也共享这套骨架)。
WebApplicationContext接口中定义了一个关键常量ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,用来索引存储在ServletContext中的根上下文;同时定义了getServletContext()方法,通过它可得到当前 Web 容器的 Servlet 上下文环境,相当于提供了一个 Web 容器级别的全局环境:
public interface WebApplicationContext extends ApplicationContext { /** * 该常量用于在 ServletContext 中存取根上下文 */ String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT"; /** * 对于 WebApplicationContext 来说,需要得到 Web容器 的 ServletContext */ ServletContext getServletContext(); }3.2 XmlWebApplicationContext:Web 环境中的默认 IoC 容器
在启动过程中,Spring 会使用默认的WebApplicationContext实现作为 IoC 容器,即XmlWebApplicationContext。它继承了ApplicationContext,在ApplicationContext的基础上增加了对 Web 环境和 XML 配置定义的处理。在XmlWebApplicationContext的初始化过程中,Web 容器中的 IoC 容器被建立起来,从而在 Web 容器中建立起整个 Spring 应用。
与普通 IoC 容器初始化一样,这个过程同样有loadBeanDefinitions()方法对 BeanDefinition 的载入。在 Web 环境中,对定位 BeanDefinition 的 Resource 有特别的要求,这个要求体现在对getDefaultConfigLocations()方法的处理中——Spring 将默认的 BeanDefinition 配置路径作为一个常量定义好了,即/WEB-INF/applicationContext.xml:
public class XmlWebApplicationContext extends AbstractRefreshableWebApplicationContext { /** 若不指定其它文件,Spring 默认从 "/WEB-INF/applicationContext.xml" 目录文件 初始化 IoC容器 */ public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml"; /** 默认的配置文件在 /WEB-INF/ 目录下 */ public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/"; /** 默认的配置文件后缀名为 .xml */ public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml"; /** * 此加载过程在容器 refresh() 时启动 */ @Override protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { // 使用 XmlBeanDefinitionReader 对指定的 BeanFactory 进行解析 XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); // 初始化 beanDefinitionReader 的属性,其中,设置 ResourceLoader 是因为 XmlBeanDefinitionReader // 是 DefaultResource 的子类,所有这里同样会使用 DefaultResourceLoader 来定位 BeanDefinition beanDefinitionReader.setEnvironment(this.getEnvironment()); beanDefinitionReader.setResourceLoader(this); beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); // 该方法是一个空实现 initBeanDefinitionReader(beanDefinitionReader); // 使用初始化完成的 beanDefinitionReader 来加载 BeanDefinitions loadBeanDefinitions(beanDefinitionReader); } protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) { } /** * 获取所有的配置文件,然后一个一个载入 BeanDefinition */ protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException { String[] configLocations = getConfigLocations(); if (configLocations != null) { for (String configLocation : configLocations) { reader.loadBeanDefinitions(configLocation); } } } /** * 获取默认路径 "/WEB-INF/***.xml" 下的配置文件, * 或者获取 "/WEB-INF/applicationContext.xml" 配置文件 */ @Override protected String[] getDefaultConfigLocations() { if (getNamespace() != null) { return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX}; } else { return new String[] {DEFAULT_CONFIG_LOCATION}; } } }从上面的代码可以看到:在XmlWebApplicationContext中,基本的上下文功能都已通过类的继承获得,这里需要处理的核心问题是如何在 Web 容器环境中获取 BeanDefinition 信息。获取到 BeanDefinition 信息之后,后续过程与普通 IoC 容器一致——通过XmlBeanDefinitionReader载入 BeanDefinition 信息,最终完成整个上下文的初始化。关于 BeanDefinition 的定位、解析与注册,可进一步参阅仓库中的 BeanDefinition的资源定位过程.md、将bean解析封装成BeanDefinition.md 与 将BeanDefinition注册进IoC容器.md。
4 ContextLoaderListener 与 ContextLoader:根上下文的启动器
4.1 监听器与 Web 容器生命周期的绑定
对于 Spring 承载的 Web 应用而言,可以指定在 Web 应用程序启动时载入 IoC 容器(即WebApplicationContext)。这个功能由ContextLoaderListener完成:它是在 Web 容器中配置的监听器,监听 Web 容器的启动,然后载入 IoC 容器。ContextLoaderListener通过使用ContextLoader来完成实际的WebApplicationContext(IoC 容器)的初始化工作,ContextLoader就像 Spring 应用程序在 Web 容器中的启动器。
ContextLoaderListener是启动根 IoC 容器并把它载入到 Web 容器的主要功能模块,也是整个 Spring Web 应用加载 IoC 的第一个地方。从加载过程可以看到:首先从 Servlet 事件中得到ServletContext,然后读取配置在web.xml中的各个相关属性值,接着ContextLoader实例化WebApplicationContext,并完成其载入和初始化过程。这个被初始化的第一个上下文作为根上下文存在,载入后被绑定到 Web 应用程序的ServletContext上。任何需要访问根上下文的应用程序代码都可以通过WebApplicationContextUtils类的静态方法得到。
ContextLoaderListener实现的是ServletContextListener接口,这个接口里的函数会结合 Web 容器的生命周期被调用:服务器启动时,ServletContext被创建,触发contextInitialized()回调;服务器关闭时,ServletContext被销毁,触发contextDestroyed()回调。在初始化回调中,ContextLoaderListener创建ContextLoader,并利用它完成 IoC 容器的初始化:
public class ContextLoaderListener extends ContextLoader implements ServletContextListener { private ContextLoader contextLoader; /** * 启动 web应用 的根上下文 */ public void contextInitialized(ServletContextEvent event) { // 由于本类直接继承了 ContextLoader,所以能直接使用 ContextLoader 来初始化 IoC容器 this.contextLoader = createContextLoader(); if (this.contextLoader == null) { this.contextLoader = this; } // 具体的初始化工作交给 ContextLoader 完成 this.contextLoader.initWebApplicationContext(event.getServletContext()); } }4.2 ContextLoader:根上下文的创建与发布
ContextLoader中定义了多个与web.xml初始化参数对应的常量,用于从ServletContext读取配置:
| 常量 | 对应 web.xml 参数 | 作用 |
|---|---|---|
CONTEXT_CLASS_PARAM | contextClass | 指定使用何种WebApplicationContext实现类 |
CONTEXT_ID_PARAM | contextId | 为根上下文指定 ID |
CONTEXT_INITIALIZER_CLASSES_PARAM | contextInitializerClasses | 指定上下文初始化器类 |
CONFIG_LOCATION_PARAM | contextConfigLocation | 指定 Bean 配置文件位置 |
LOCATOR_FACTORY_SELECTOR_PARAM | locatorFactorySelector | 定位父上下文的工厂选择器 |
LOCATOR_FACTORY_KEY_PARAM | parentContextKey | 定位父上下文的 key |
此外,ContextLoader在静态代码块中从ContextLoader.properties加载默认策略(defaultStrategies),该文件中的默认策略目前严格内部使用,不建议应用开发者定制。
initWebApplicationContext(ServletContext)是根上下文初始化的核心方法,其流程如下:
- 唯一性检查:如果
ServletContext中已存在ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE对应的根上下文,则抛出IllegalStateException,提示检查web.xml中是否存在多个ContextLoader*定义。 - 创建根上下文:调用
createWebApplicationContext(servletContext)实例化上下文对象。 - 设置双亲上下文:若上下文未激活且没有父上下文,调用
loadParentContext(servletContext)载入根上下文的双亲上下文(默认为null)。 - 配置并刷新:调用
configureAndRefreshWebApplicationContext(cwac, servletContext),其中refresh()方法就是之前 IoC 容器初始化分析中AbstractApplicationContext的刷新入口。 - 发布根上下文:通过
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context)把根上下文存入ServletContext,此后所有应用都根据ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE常量获取根上下文。 - 记录当前上下文:根据线程上下文类加载器,将根上下文记录到
currentContext或currentContextPerThread(按类加载器隔离)中,供后续检索。
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { // 如果 ServletContext 中已经包含了根上下文,则抛出异常 if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) { throw new IllegalStateException( "Cannot initialize context because there is already a root application context present - " + "check whether you have multiple ContextLoader* definitions in your web.xml!"); } Log logger = LogFactory.getLog(ContextLoader.class); servletContext.log("Initializing Spring root WebApplicationContext"); if (logger.isInfoEnabled()) { logger.info("Root WebApplicationContext: initialization started"); } long startTime = System.currentTimeMillis(); try { if (this.context == null) { // 这里创建在 ServletContext 中存储的根上下文 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); } // 配置并初始化 IoC容器,看到下面方法中的 Refresh单词 应该能想到 // AbstractApplicationContext 中的 refresh()方法,猜到它是前面介绍的 IoC容器 的初始化入口 configureAndRefreshWebApplicationContext(cwac, servletContext); } } // 将上面创建的 WebApplicationContext实例 存到 ServletContext 中,注意同时被存入的常量 // ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,以后的应用都会根据这个属性获取根上下文 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl == ContextLoader.class.getClassLoader()) { currentContext = this.context; } else if (ccl != null) { currentContextPerThread.put(ccl, this.context); } if (logger.isDebugEnabled()) { logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]"); } if (logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms"); } return this.context; } catch (RuntimeException ex) { logger.error("Context initialization failed", ex); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); throw ex; } catch (Error err) { logger.error("Context initialization failed", err); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err); throw err; } }值得注意的细节:当初始化抛出RuntimeException或Error时,异常/错误对象本身也会被存入ServletContext的同一属性中。这正是 SpringMVC的设计与实现.md 中WebApplicationContextUtils.getWebApplicationContext(ServletContext, String)会先检查属性值是否为RuntimeException/Error/Exception并重新抛出或包装的原因——初始化失败信息会被延迟到任何后续访问方读取时暴露出来。
4.3 determineContextClass:如何决定使用哪种 IoC 容器
createWebApplicationContext(ServletContext)通过determineContextClass(sc)判断在 Web 容器中使用什么类作为 IoC 容器,并要求该类必须是ConfigurableWebApplicationContext的子类型,最后通过BeanUtils.instantiateClass(contextClass)直接实例化。
determineContextClass的判定逻辑体现了配置优先级:
- 首先从
ServletContext中读取contextClass初始化参数(即web.xml中context-param的配置);若配置了,则通过ClassUtils.forName加载该自定义上下文类; - 若未配置
contextClass,则从ContextLoader.properties的默认策略中读取WebApplicationContext.class.getName()对应的默认实现类——即XmlWebApplicationContext。
protected Class<?> determineContextClass(ServletContext servletContext) { // 获取 servletContext 中对 CONTEXT_CLASS_PARAM(contextClass)参数 的配置 String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM); if (contextClassName != null) { try { // 获取配置的 contextClassName 对应的 clazz对象 return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader()); } catch (ClassNotFoundException ex) { throw new ApplicationContextException( "Failed to load custom context class [" + contextClassName + "]", ex); } } else { // 如果没有配置 CONTEXT_CLASS_PARAM,则使用默认的 ContextClass contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName()); try { return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader()); } catch (ClassNotFoundException ex) { throw new ApplicationContextException( "Failed to load default context class [" + contextClassName + "]", ex); } } }4.4 configureAndRefreshWebApplicationContext:配置与刷新的入口
configureAndRefreshWebApplicationContext负责为根上下文装配运行环境:
- 生成上下文 ID:若上下文 ID 仍是默认值,则优先使用
contextId初始化参数;否则根据 Servlet 版本生成:Servlet 2.4 及以下使用web.xml中的servlet-context-name,Servlet 2.5+ 使用contextPath,均以ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX为前缀。 - 设置 ServletContext 与配置文件位置:
wac.setServletContext(sc)将当前 Web 容器的ServletContext绑定到上下文;若存在contextConfigLocation初始化参数,则调用wac.setConfigLocation(initParameter)设置 Bean 配置文件路径。 - 自定义上下文:调用
customizeContext(sc, wac)(可被子类覆盖,用于注册ApplicationContextInitializer等)。 - 刷新容器:调用
wac.refresh(),这是 IoC 容器初始化的真正入口,与普通 IoC 容器启动完全一致。
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) { if (ObjectUtils.identityToString(wac).equals(wac.getId())) { // The application context id is still set to its original default value // -> assign a more useful id based on available information String idParam = sc.getInitParameter(CONTEXT_ID_PARAM); if (idParam != null) { wac.setId(idParam); } else { // Generate default id... if (sc.getMajorVersion() == 2 && sc.getMinorVersion() < 5) { // Servlet <= 2.4: resort to name specified in web.xml, if any. wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getServletContextName())); } else { wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath())); } } } // 设置 ServletContext 及配置文件的位置参数 wac.setServletContext(sc); String initParameter = sc.getInitParameter(CONFIG_LOCATION_PARAM); if (initParameter != null) { wac.setConfigLocation(initParameter); } customizeContext(sc, wac); // IoC容器 初始化的入口,想不起来的把前面 IoC容器 初始化的博文再读10遍 wac.refresh(); }5 根上下文如何被 DispatcherServlet 复用:双亲-子级上下文体系
根上下文初始化完成后会被存储到ServletContext中,从而建立了一个全局的、关于整个应用的上下文。在启动 SpringMVC 时,DispatcherServlet进行自己持有的上下文初始化时,会从ServletContext中得到根上下文,并将其设置为 DispatcherServlet 自带上下文的双亲上下文。这一过程发生在FrameworkServlet(DispatcherServlet的父类)中。
FrameworkServlet.initWebApplicationContext()的核心逻辑:
- 通过
WebApplicationContextUtils.getWebApplicationContext(getServletContext())从ServletContext中取出根上下文,作为当前 MVC 上下文的双亲上下文; - 依次尝试三种上下文来源:构造时注入的上下文实例(
this.webApplicationContext)→ 已注册在 Servlet 上下文中的上下文(findWebApplicationContext())→ 新建本地上下文(createWebApplicationContext(rootContext)); - 若未收到刷新事件,则手动触发
onRefresh(wac); - 若
publishContext为true,将当前 Servlet 的上下文以getServletContextAttributeName()(与 Servlet 名称相关的属性名)存入ServletContext,保证该上下文在 Web 环境上下文体系中的唯一性。
protected WebApplicationContext initWebApplicationContext() { // 获取根上下文作为当前 MVC上下文 的双亲上下文,这个根上下文保存在 ServletContext 中 WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; if (this.webApplicationContext != null) { // 可以在本对象被构造时注入一个 webApplicationContext实例 wac = this.webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; if (!cwac.isActive()) { // 上下文尚未刷新 -> 提供诸如设置父上下文、设置应用程序上下文id等服务 if (cwac.getParent() == null) { // 上下文实例在没有显式父实例的情况下被注入 -> // 将根上下文(如果有的话;可以为空)设置为父上下文 cwac.setParent(rootContext); } configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { // 在本对象被构造时没有注入上下文实例 -> // 查看是否已在 servlet上下文 中注册了上下文实例。 // 如果存在一个,则假定父上下文(如果有的话)已经被设置, // 并且用户已经执行了任何初始化,例如设置上下文ID wac = findWebApplicationContext(); } if (wac == null) { // 没有为此 servlet 定义上下文实例 -> 创建本地实例 wac = createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { // 上下文不是支持刷新的 ConfigurableApplicationContext,或者 // 在构造时注入的上下文已经完成刷新 -> 在此处手动触发 onRefresh()方法 onRefresh(wac); } if (this.publishContext) { // 把当前建立的上下文保存到 ServletContext 中,使用的属性名是和 当前servlet名 相关的 String attrName = getServletContextAttributeName(); getServletContext().setAttribute(attrName, wac); if (this.logger.isDebugEnabled()) { this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() + "' as ServletContext attribute with name [" + attrName + "]"); } } return wac; }创建 DispatcherServlet 持有的上下文时,FrameworkServlet.createWebApplicationContext(parent)与ContextLoader.createWebApplicationContext的过程非常相似:默认上下文类同样是XmlWebApplicationContext(通过getContextClass()获取,可通过contextClass参数定制),实例化后设置父上下文、环境、配置文件位置等,最终同样通过refresh()完成初始化。
WebApplicationContextUtils是封装了静态方法的抽象工具类,任何需要访问根上下文的代码都可以通过它获取:
public abstract class WebApplicationContextUtils { /** * 使用了 WebApplicationContext 的 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE属性,获取 * ServletContext 中的根上下文,这个属性代表的根上下文在 ContextLoaderListener 初始化的 * 过程中被建立 */ public static WebApplicationContext getWebApplicationContext(ServletContext sc) { return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); } /** * 查找此 web应用程序 的自定义 WebApplicationContext */ public static WebApplicationContext getWebApplicationContext(ServletContext sc, String attrName) { Assert.notNull(sc, "ServletContext must not be null"); Object attr = sc.getAttribute(attrName); if (attr == null) { return null; } if (attr instanceof RuntimeException) { throw (RuntimeException) attr; } if (attr instanceof Error) { throw (Error) attr; } if (attr instanceof Exception) { throw new IllegalStateException((Exception) attr); } if (!(attr instanceof WebApplicationContext)) { throw new IllegalStateException("Context attribute is not of type WebApplicationContext: " + attr); } return (WebApplicationContext) attr; } }理解了这个双亲-子级上下文体系,就掌握了 Spring Web 应用中 Bean 的共享规则:一个根上下文可以作为多个 Servlet 上下文的双亲上下文;向子上下文getBean()时,IoC 容器会首先向其双亲上下文查询,因此在根上下文中定义的 Bean 可以被各个 Servlet 持有的上下文获取和共享。这也是业务 Bean 放在根上下文(applicationContext.xml)、而 MVC 组件(控制器、视图解析器等)放在 DispatcherServlet 上下文中的经典分工的底层依据。完整的请求分发细节可进一步阅读 SpringMVC的设计与实现.md。
6 总结:Web 环境中 IoC 容器的启动全景
IoC 容器在 Web 容器中的启动过程与应用中启动 IoC 容器的方式相类似,所不同的是需要考虑 Web 容器的环境特点,比如各种参数的设置、IoC 容器与 Web 容器ServletContext的结合等。整体流程可归纳为:
- Tomcat 启动→ 解析
web.xml,实例化ContextLoaderListener(实现了ServletContextListener); - 触发
contextInitialized()→ 创建ContextLoader,调用initWebApplicationContext(servletContext); ContextLoader创建根上下文→determineContextClass决定上下文实现类(默认XmlWebApplicationContext),实例化后设置ServletContext、contextConfigLocation等参数;refresh()完成 IoC 容器初始化→ 通过XmlBeanDefinitionReader载入 BeanDefinition,走完 Bean 解析、注册、依赖注入全流程;- 发布根上下文→ 以
ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE为属性名存入ServletContext,供全局访问; - 初始化 DispatcherServlet→
FrameworkServlet.initWebApplicationContext()从ServletContext取出根上下文作为双亲上下文,创建并刷新自己的 MVC 上下文,将其发布到ServletContext(属性名与 Servlet 名称相关)。
初始化完成后的上下文会被存储到ServletContext中,这样就建立了一个全局的、关于整个应用的上下文。同时,在启动 SpringMVC 时,这个根上下文会被DispatcherServlet在初始化自己持有的上下文时设置为双亲上下文——正是这一设计,让 SpringMVC 得以建立在 IoC 容器之上,构成完整的 Web 应用上下文体系。
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考