spring mvc的启动,是跟随着tomcat启动的,所以要深入理解spring mvc的启动过程与原理,需要先了解下tomcat启动的一些关键过程。
1、tomcat web应用启动及初始化过程
参考官方文档,tomcat web应用启动过程是这样的:
图1 tomcat web应用启动过程
大概意思就是,当一个Web应用部署到容器内时,在web应用开始执行用户请求前,会依次执行以下步骤:
- 部署描述文件web.xml中<listener>元素标记的事件监听器会被创建和初始化;
- 对于所有事件监听器,如果实现了ServletContextListener接口,将会执行其实现的contextInitialized()方法;
- 部署描述文件中由<filter>元素标记的过滤器会被创建和初始化,并调用其init()方法;
- 部署描述文件中由<servlet>元素标记的servlet会根据<load-on-startup>的权值按顺序创建和初始化,并调用其init()方法;
通过上述文档的描述,可知tomcat web应用启动初始化流程是这样的:
图2 tomcat web应用初始化过程
可以看出,在tomcat web应用的初始化流程是,先初始化listener,接着初始化filter,最后初始化servlet。
2、spring mvc应用的启动初始化
做过spring mvc项目开发的伙伴,都会配置一个web.xml配置文件,内容一般是这样的:
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<!--全局变量配置-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/spring-main.xml
</param-value>
</context-param>
<!--监听器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
<!--解决乱码问题的filter-->
<filter>
<filter-name>encodingFilter</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>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- MVC Servlet -->
<servlet>
<servlet-name>springServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/spring-mvc*.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
web.xml配置文件中也主要是配置了Listener,Filter,Servlet。
所以spring mvc应用启动的时候,主要是在这三大组件初始化的过程中完成对spring及spring mvc的初始化。
3、Listener与spring的初始化过程
web.xml配置文件中首先定义了<context-param>标签,用于配置一个全局变量,<context-param>标签的内容读取后会做为Web应用的全局变量使用。当Listener创建的时候,会使用到这个全局变量,因此,Web应用在容器中部署后,进行初始化时会先读取这个全局变量,之后再进行初始化过程。
接着定义了一个ContextLoaderListener类的Listener。查看ContextLoaderListener的类声明如图:
图3 ContextLoaderListener类源码
ContextLoaderListener类继承自ContextLoader类,并实现了ServletContextListener接口。
图4 ServletContextListener源码
ServletContextListener只有两个方法,contextInitialized和contextDestroyed,当Web应用初始化或销毁时会分别调用这两个方法。
ContextLoaderListener实现了ServletContextListener接口,因此在Web应用初始化时会调用contextInitialized方法,该方法的具体实现如下:
图5 contextInitialized方法
ContextLoaderListener的contextInitialized()方法直接调用了initWebApplicationContext()方法,这个方法是继承自ContextLoader类,通过函数名可以知道,该方法是用于初始化web应用上下文,即IOC容器。
这里是spring mvc应用启动的第一个重点,就是在ContextLoaderListener初始化的时候,初始化了spring IOC容器。
我们继续看ContextLoader类的initWebApplicationContext()方法。
//servletContext,servlet上下文
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 首先通过WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
// 这个String类型的静态变量获取一个根IoC容器,根IoC容器作为全局变量
// 存储在servletContext对象中,如果存在则有且只能有一个
// 如果在初始化根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!");
}
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
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容器放入到servletContext对象中,并设置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.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
}
return this.context;
}
catch (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
在jsp中,可以通过这两种方法获取到IOC容器:
WebApplicationContext applicationContext = (WebApplicationContext) servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
总的来说,initWebApplicationContext方法的主要目的是创建root WebApplicationContext对象,即根IOC容器。
如果整个Web应用存在根IOC容器则有且只能有一个,根IOC容器会作为全局变量存储在ServletContext对象中。然后将根IOC容器放入到ServletContext对象之前进行了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);
// 获取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();
}
configureAndRefreshWebApplicationContext方法里获取了<context-param>标签配置的全局变量,并且在方法最后调用了refresh()方法。
对spring容器初始化有一定了解的同学都知道,这是初始化spring容器的入口方法,其最终调用的是AbstractApplicationContext类中的refresh方法。
refresh()方法主要是创建并初始化contextConfigLocation类配置的xml文件中的Bean。
具体代码不贴了,有兴趣的同学,可以自行查阅。
到此为止,整个ContextLoaderListener类的启动过程就结束了,可以发现,创建ContextLoaderListener是比较重要的一个步骤,主要做的事情就是创建根IOC容器,并使用特定的key将其放入到servletContext对象中,供整个Web应用使用。
由于在ContextLoaderListener类中构造的根IOC容器配置的Bean是全局共享的,因此,在<context-param>标识的contextConfigLocation的xml配置文件一般包括:数据库DataSource、DAO层、Service层、事务等相关Bean。
4、Filter与spring的初始化
在监听器Listener初始化完成后,接下来会进行Filter的初始化操作,Filter的创建和初始化没有涉及IOC容器的相关操作,因此不是本文讲解的重点。
5、Servlet与spring的初始化
web应用启动的最后一个步骤就是创建和初始化相关servlet,servlet最重要的方法就是:init(),service(),destroy();
图6 Servlet源码
在spring mvc中,实现了一个非常重要的servlet,即DispatcherServlet。它是整个spring mvc应用的核心,用于获取分发用户请求并返回响应。
图7 DispatcherServlet类图
DispatcherServlet本质上也是一个Servlet,其源码实现充分利用了模板模式,将不变的部分统一实现,将变化的部分留给子类实现,上层父类不同程度的实现了相关接口的部分方法,留出了相关方法由子类覆盖。
图8 DispatcherServlet类初始化过程
web应用部署到容器启动后,进行Servlet的初始化时会调用相关的init(ServletConfig)方法,因此,DispatchServlet类的初始化过程也由该方法开始。
其中比较重要的是,FrameworkServlet类中的initServletBean()方法、initWebApplicationContext()方法以及DispatcherServlet类中的onRefresh()方法。
图9 FrameworkServlet类中的initServletBean()方法
FrameworkServlet的父类HttpServletBean中的initServletBean()方法,HttpServletBean抽象类在执行init()方法时会调用initServletBean()方法。
该方法中比较重要的是initWebApplicationContext()方法,该方法仍由FrameworkServlet抽象类实现,继续查看其源码如下所示:
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
// 如果仍旧没有查找到子IoC容器则创建一个子IoC容器
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
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.
synchronized (this.onRefreshMonitor) {
// 调用子类覆盖的onRefresh方法完成“可变”的初始化过程
onRefresh(wac);
}
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
通过函数名不难发现,该方法的主要作用同样是创建一个WebApplicationContext对象,即IOC容器,不过这里创建的是spring mvc容器,是前面ContextLoadListener创建的IOC容器的子容器。
为什么需要多个IOC容器?还是父子容器?这就要说明下父子IOC容器的访问特性了。
父子容器类似于类的继承关系,子类可以访问父类中的成员变量,而父类不可访问子类的成员变量,同样的,子容器可以访问父容器中定义的Bean,但父容器无法访问子容器定义的Bean。
创建多个容器的目的是,父IOC容器做为全局共享的IOC容器,存放Web应用共享的Bean,比如Service,DAO。而子IOC容器根据需求的不同,放入不同的Bean,比如Conroller,这样能够做到隔离,保证系统的安全性。
如果你看过sprin cloud netflix系列源码,比如spring-cloud-netflix-ribbon,spring-cloud-openfeign,就会发现它里边创建了很多父子容器,作用与这里是一样的,容器与容器相互隔离,保证系统的安全性。
我们继续讲解DispatcherServlet类的子IOC容器创建过程,如果当前Servlet存在一个IOC容器则为其设置根IOC容器作为其父类,并刷新该容器,用于初始化定义的Bean。
这里的方法与前文讲述的根IOC容器类似,同样会读取用户在web.xml中配置的<servlet>中的<init-param>值,用于查找相关的xml配置文件来创建定义的Bean。如果当前Servlet不存在一个子IoC容器就去查找一个,如果没有查找到,则调用createWebApplicationContext()方法去创建一个。
图10 createWebApplicationContext源码
该方法用于创建一个子IOC容器并将根IOC容器做为其父容器,接着执行配置和刷新操作构建相关的Bean。
至此,根IOC容器以及相关Servlet的子IOC容器已经初始化完成了,子容器中管理的Bean一般只被该Servlet使用,比如SpringMVC中需要的各种重要组件,包括Controller、Interceptor、Converter、ExceptionResolver等。
spring mvc父子容器之间的关系,可以用下图来描述:
图11 spring mvc父子容器
当IOC子容器构造完成后调用了onRefresh()方法,其实现是在子类DispatcherServlet中,查看DispatcherServletBean类的onRefresh()方法源码如下:
图12 onRefresh方法
onRefresh()方法调用了initStrategies()方法,通过函数名可以判断,该方法主要初始化创建multipartResovle来支持图片等文件的上传、本地化解析器、主题解析器、HandlerMapping处理器映射器、HandlerAdapter处理器适配器、异常解析器、视图解析器、flashMap管理器等。这些组件都是SpringMVC开发中的重要组件,相关组件的初始化创建过程均在此完成,所以在这里最终完成了spring mvc组件的初始化。
至此,整个spring mvc应用启动的原理以及过程就讲完了,总的来说,spring以及spring mvc的初始化过程是跟随在tomcat的Listener、Filter、Servlet的初始化过程完成的。
6、总结
spring以及spring mvc应用初始化过程还是比较清晰明了的,本文做了完整的分析记录。spring mvc也支持无web.xml配置文件的方式开发web应用,其原理是类似的,只不过是用代码的方式来创建了web.xml中需要的组件。
后续会分析spring boot应用的启动以及初始化过程分析,spring boot是在spring及spring mvc的基础上做了更深的封装,所以看起来也更复杂。spring boot应用既可以以war包的方式运行在tomcat上,也可以以jar的方式运行再内嵌的tomcat上,两种方式启动的源码有所不同,还需要分别分析。