共计 15492 个字符,预计需要花费 39 分钟才能阅读完成。
web 应用部署初始化流程
当一个 Web 应用部署到容器内时(eg.tomcat),在 Web 应用开始响应执行用户请求前,以下步骤会被依次执行:
部署描述文件中 (eg.tomcat 的 web.xml) 由 <listener> 元素标记的事件监听器会被创建和初始化
对于所有事件监听器,如果实现了 ServletContextListener 接口,将会执行其实现的 contextInitialized()方法
部署描述文件中由 <filter> 元素标记的过滤器会被创建和初始化,并调用其 init()方法
部署描述文件中由 <servlet> 元素标记的 servlet 会根据 <load-on-startup> 的权值按顺序创建和初始化,并调用其 init()方法
web 初始化流程图如下:
SpringMVC 初始化流程
接下来以一个常见的简单 web.xml 配置进行 Spring MVC 启动过程的分析,web.xml 配置内容如下:
<web-app>
<display-name>Web Application</display-name>
<!– 全局变量配置 –>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext-*.xml</param-value>
</context-param>
<!– 监听器 –>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!– 解决乱码问题的 filter–>
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!–Restful 前端控制器 –>
<servlet>
<servlet-name>springMVC_rest</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!– 配置 servlet 初始化优先级,本质是调用 init()–>
<load-on-startup>1<load-on-startup/>
</servlet>
<!– 静态资源使用 DefaultServletHttpRequestHandler 处理器处理 –>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.css</url-pattern>
</servlet-mapping>
</web-app>
在 web 初始化的时候会去加载 web.xml 文件,会按照配置的内容进行初始化
Listener 监听器初始化
首先定义了 <context-param> 标签,用于配置一个全局变量,<context-param> 标签的内容读取后会被放进 application 中,做为 Web 应用的全局变量使用,加载 bean 的时候作为 application 的 configLocation 参数,因此,Web 应用在容器中部署后,进行初始化时会先读取这个全局变量,之后再进行上述讲解的初始化启动过程。接着定义了一个 ContextLoaderListener 类的 listener。查看 ContextLoaderListener 的类声明源码如下图:
ContextLoaderListener 类继承了 ContextLoader 类并实现了 ServletContextListener 接口,首先看一下前面讲述的 ServletContextListener 接口源码:
该接口只有两个方法 contextInitialized 和 contextDestroyed,这里采用的是观察者模式,也称为为订阅 - 发布模式,实现了该接口的 listener 会向发布者进行订阅,当 Web 应用初始化或销毁时会分别调用上述两个方法。继续看 ContextLoaderListener,该 listener 实现了 ServletContextListener 接口,因此在 Web 应用初始化时会调用该方法,该方法的具体实现如下:
* Initialize the root web application context.*/
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
ContextLoaderListener 的 contextInitialized()方法直接调用了 initWebApplicationContext()方法,这个方法是继承自 ContextLoader 类,通过函数名可以知道,该方法是用于初始化 Web 应用上下文,即 IoC 容器,这里使用的是代理模式,继续查看 ContextLoader 类的 initWebApplicationContext()方法的源码如下:
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
/*
首先通过 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
这个 String 类型的静态变量获取一个根 IoC 容器,根 IoC 容器作为全局变量
存储在 application 对象中,如果存在则有且只能有一个
如果在初始化根 WebApplicationContext 即根 IoC 容器时发现已经存在
则直接抛出异常,因此 web.xml 中只允许存在一个 ContextLoader 类或其子类的对象
*/
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 {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
// 如果当前成员变量中不存在 WebApplicationContext 则创建一个根 WebApplicationContext
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
// 为根 WebApplicationContext 设置一个父容器
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 配置并刷新整个根 IoC 容器,在这里会进行 Bean 的创建和初始化
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
/*
将创建好的 IoC 容器放入到 application 对象中,并设置 key 为 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
因此,在 SpringMVC 开发中可以在 jsp 中通过该 key 在 application 对象中获取到根 IoC 容器,进而获取到相应的 Ben
*/
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;
}
}
initWebApplicationContext()方法如上注解讲述,主要目的就是创建 root WebApplicationContext 对象即根 IoC 容器,其中比较重要的就是,整个 Web 应用如果存在根 IoC 容器则有且只能有一个,根 IoC 容器作为全局变量存储在 ServletContext 即 application 对象中。将根 IoC 容器放入到 application 对象之前进行了 IoC 容器的配置和刷新操作,调用了 configureAndRefreshWebApplicationContext()方法,该方法源码如下:
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…
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
wac.setServletContext(sc);
/*
CONFIG_LOCATION_PARAM = “contextConfigLocation”
获取 web.xml 中 <context-param> 标签配置的全局变量,其中 key 为 CONFIG_LOCATION_PARAM
也就是我们配置的相应 Bean 的 xml 文件名,并将其放入到 WebApplicationContext 中
*/
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
// The wac environment’s #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
customizeContext(sc, wac);
wac.refresh();
}
比较重要的就是获取到了 web.xml 中的 <context-param> 标签配置的全局变量 contextConfigLocation,并最后一行调用了 refresh()方法,ConfigurableWebApplicationContext 是一个接口,通过对常用实现类 ClassPathXmlApplicationContext 逐层查找后可以找到一个抽象类 AbstractApplicationContext 实现了 refresh()方法,其源码如下:
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn(“Exception encountered during context initialization – ” +
“cancelling refresh attempt: ” + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset ‘active’ flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring’s core, since we
// might not ever need metadata for singleton beans anymore…
resetCommonCaches();
}
}
}
该方法主要用于创建并初始化 contextConfigLocation 类配置的 xml 文件中的 Bean,因此,如果我们在配置 Bean 时出错,在 Web 应用启动时就会抛出异常,而不是等到运行时才抛出异常。整个 ContextLoaderListener 类的启动过程到此就结束了,可以发现,创建 ContextLoaderListener 是比较核心的一个步骤,主要工作就是为了创建根 IoC 容器并使用特定的 key 将其放入到 application 对象中,供整个 Web 应用使用,由于在 ContextLoaderListener 类中构造的根 IoC 容器配置的 Bean 是全局共享的,因此,在 <context-param> 标识的 contextConfigLocation 的 xml 配置文件一般包括: 数据库 DataSource、DAO 层、Service 层、事务等相关 Bean。
Filter 的初始化
在监听器 listener 初始化完成后,按照文章开始的讲解,接下来会进行 filter 的初始化操作,filter 的创建和初始化中没有涉及 IoC 容器的相关操作,因此不是本文讲解的重点,本文举例的 filter 是一个用于编码用户请求和响应的过滤器,采用 utf- 8 编码用于适配中文。
Servlet 的初始化
Servlet 的初始化过程可以通过一张图来总结,如下所示:
通过类图和相关初始化函数调用的逻辑来看,DispatcherServlet 类的初始化过程将模板方法使用的淋漓尽致,其父类完成不同的统一的工作,并预留出相关方法用于子类覆盖去完成不同的可变工作。
DispatcherServelt 类的本质是 Servlet,通过文章开始的讲解可知,在 Web 应用部署到容器后进行 Servlet 初始化时会调用相关的 init(ServletConfig)方法,因此,DispatchServlet 类的初始化过程也由该方法开始。上述调用逻辑中比较重要的就是 FrameworkServlet 抽象类中的 initServletBean()方法、initWebApplicationContext()方法以及 DispatcherServlet 类中的 onRefresh()方法,接下来会逐一进行讲解。
首先来看 initServletBean()方法:
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log(“Initializing Spring FrameworkServlet ‘” + getServletName() + “‘”);
if (this.logger.isInfoEnabled()) {
this.logger.info(“FrameworkServlet ‘” + getServletName() + “‘: initialization started”);
}
long startTime = System.currentTimeMillis();
try {
// 这里是重点,用于初始化子 ApplicationContext 对象,主要是用来加载 <servlet/> 对应的 servletName-servlet.xml 文件如:springMVC_rest-servlet.xml
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException ex) {
this.logger.error(“Context initialization failed”, ex);
throw ex;
}
catch (RuntimeException ex) {
this.logger.error(“Context initialization failed”, ex);
throw ex;
}
if (this.logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() – startTime;
this.logger.info(“FrameworkServlet ‘” + getServletName() + “‘: initialization completed in ” +
elapsedTime + ” ms”);
}
}
接下来来着重分析 initWebApplicationContext()方法,
protected WebApplicationContext initWebApplicationContext() {
/*
获取由 ContextLoaderListener 创建的根 IoC 容器
获取根 IoC 容器有两种方法,还可通过 key 直接获取
*/
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
/*
如果当前 Servelt 存在一个 WebApplicationContext 即子 IoC 容器
并且上文获取的根 IoC 容器存在,则将根 IoC 容器作为子 IoC 容器的父容器
*/
cwac.setParent(rootContext);
}
// 配置并刷新当前的子 IoC 容器,功能与前文讲解根 IoC 容器时的配置刷新一致,用于构建相关 Bean
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
// 如果当前 Servlet 不存在一个子 IoC 容器则去查找一下
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
// 如果仍旧没有查找到子 IoC 容器则创建一个子 IoC 容器
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
// 调用子类覆盖的 onRefresh 方法完成“可变”的初始化过程
onRefresh(wac);
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
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;
}
通过函数名不难发现,该方法的主要作用同样是创建一个 WebApplicationContext 对象,即 Ioc 容器,不过前文讲过每个 Web 应用最多只能存在一个根 IoC 容器,这里创建的则是特定 Servlet 拥有的子 IoC 容器,可能有些读者会有疑问,为什么需要多个 Ioc 容器,首先介绍一个父子 IoC 容器的访问特性,有兴趣的读者可以自行实验。
父子 IoC 容器的访问特性
在学习 Spring 时,我们都是从读取 xml 配置文件来构造 IoC 容器,常用的类有 ClassPathXmlApplicationContext 类,该类存在一个初始化方法用于传入 xml 文件路径以及一个父容器,我们可以创建两个不同的 xml 配置文件并实现如下代码:
//applicationContext1.xml 文件中配置一个 id 为 baseBean 的 Bean
ApplicationContext baseContext = new ClassPathXmlApplicationContext(“applicationContext1.xml”);
Object obj1 = baseContext.getBean(“baseBean”);
System.out.println(“baseContext Get Bean ” + obj1);
//applicationContext2.xml 文件中配置一个 id 未 subBean 的 Bean
ApplicationContext subContext = new ClassPathXmlApplicationContext(new String[]{“applicationContext2.xml”}, baseContext);
Object obj2 = subContext.getBean(“baseBean”);
System.out.println(“subContext get baseContext Bean ” + obj2);
Object obj3 = subContext.getBean(“subBean”);
System.out.println(“subContext get subContext Bean ” + obj3);
// 抛出 NoSuchBeanDefinitionException 异常
Object obj4 = baseContext.getBean(“subBean”);
System.out.println(“baseContext get subContext Bean ” + obj4);
首先创建 baseContext 没有为其设置父容器,接着可以成功获取 id 为 baseBean 的 Bean,接着创建 subContext 并将 baseContext 设置为其父容器,subContext 可以成功获取 baseBean 以及 subBean,最后试图使用 baseContext 去获取 subContext 中定义的 subBean,此时会抛出异常 NoSuchBeanDefinitionException,由此可见,父子容器类似于类的继承关系,子类可以访问父类中的成员变量,而父类不可访问子类的成员变量,同样的,子容器可以访问父容器中定义的 Bean,但父容器无法访问子容器定义的 Bean。通过上述实验我们可以理解为何需要创建多个 Ioc 容器,根 IoC 容器做为全局共享的 IoC 容器放入 Web 应用需要共享的 Bean,而子 IoC 容器根据需求的不同,放入不同的 Bean,这样能够做到隔离,保证系统的安全性。
接下来继续讲解 DispatcherServlet 类的子 IoC 容器创建过程,如果当前 Servlet 存在一个 IoC 容器则为其设置根 IoC 容器作为其父类,并配置刷新该容器,用于构造其定义的 Bean,这里的方法与前文讲述的根 IoC 容器类似,同样会读取用户在 web.xml 中配置的 <servlet> 中的 <init-param> 值,用于查找相关的 xml 配置文件用于构造定义的 Bean,这里不再赘述了。如果当前 Servlet 不存在一个子 IoC 容器就去查找一个,如果仍然没有查找到则调用 createWebApplicationContext()方法去创建一个,查看该方法的源码如下图所示:
该方法用于创建一个子 IoC 容器并将根 IoC 容器做为其父容器,接着进行配置和刷新操作用于构造相关的 Bean。至此,根 IoC 容器以及相关 Servlet 的子 IoC 容器已经配置完成,子容器中管理的 Bean 一般只被该 Servlet 使用,因此,其中管理的 Bean 一般是“局部”的,如 SpringMVC 中需要的各种重要组件,包括 Controller、Interceptor、Converter、ExceptionResolver 等。相关关系如下图所示:
当 IoC 子容器构造完成后调用了 onRefresh()方法,该方法的调用与 initServletBean()方法的调用相同,由父类调用但具体实现由子类覆盖,调用 onRefresh()方法时将前文创建的 IoC 子容器作为参数传入,查看 DispatcherServletBean 类的 onRefresh()方法源码如下:
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
onRefresh()方法直接调用了 initStrategies()方法,源码如上,通过函数名可以判断,该方法用于初始化创建 multipartResovle 来支持图片等文件的上传、本地化解析器、主题解析器、HandlerMapping 处理器映射器、HandlerAdapter 处理器适配器、异常解析器、视图解析器、flashMap 管理器等,这些组件都是 SpringMVC 开发中的重要组件,相关组件的初始化创建过程均在此完成。由于篇幅问题本文不再进行更深入的探讨,有兴趣的读者可以阅读本系列文章的其他博客内容。至此,DispatcherServlet 类的创建和初始化过程也就结束了,整个 Web 应用部署到容器后的初始化启动过程的重要部分全部分析清楚了,通过前文的分析我们可以认识到层次化设计的优点,以及 IoC 容器的继承关系所表现的隔离性。分析源码能让我们更清楚的理解和认识到相关初始化逻辑以及配置文件的配置原理。