Spring MVC是Spring系列框架中使用频率最高的部分。不管是Spring Boot还是传统的Spring项目,只要是Web项目都会使用到Spring MVC部分。本篇博客将对其启动流程和工作流程进行简要分析。
我们以一个常见的简单web.xml
配置进行Spring MVC启动过程的分析,web.xml
配置内容如下:
<web-app>
<display-name>Archetype Created Web Applicationdisplay-name>
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>/WEB-INF/applicationContext.xmlparam-value>
context-param>
<filter>
<filter-name>characterEncodingFilterfilter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
<init-param>
<param-name>encodingparam-name>
<param-value>UTF-8param-value>
init-param>
<init-param>
<param-name>forceEncodingparam-name>
<param-value>trueparam-value>
init-param>
filter>
<filter-mapping>
<filter-name>characterEncodingFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
<servlet>
<servlet-name>dispatcherservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>dispatcherservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
web-app>
根据Java Servlet Specification v4.0,Web应用部署的相关步骤如下:
当一个Web应用部署到容器内时(如Tomcat),在Web应用开始响应执行用户请求前,以下步骤会被依次执行:
web.xml
)由
元素标记的事件监听器会被创建和初始化ServletContextListener
接口,将会执行其实现的contextInitialized()
方法
元素标记的过滤器会被创建和初始化,并调用其init()
方法
元素标记的servlet会根据的权值按顺序创建和初始化,并调用其init()
方法我们可以知道,Web应用的初始化流程是:先初始化Listener
,接着初始化Filter
,最后初始化Servlet
。
根据上面web.xml
的配置,首先定义了
标签,用于配置一个全局变量,其内容读取后会被放进application
中,作为Web应用的全局变量使用,接下来创建listener
时会使用到这个全局变量。
中定义了一个ContextLoaderListener
,这个类继承了ContextLoader
,并实现了ServletContextListener
接口。
首先来看一下ServletContextListener
的源码:
该接口只有两个方法:contextInitialized()
和contextDestroyed()
。当Web应用初始化或销毁时会分别调用上述两个方法。
回到ContextLoaderListener
,当Web应用初始化时,就会调用contextInitialized()
方法:
这个方法又调用了父类ContextLoader
的initWebApplicationContext()
方法。下面是该方法的源代码:
/**
* Initialize Spring's web application context for the given servlet context,
* using the application context provided at construction time, or creating a new one
* according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
* "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
* @param servletContext current servlet context
* @return the new WebApplicationContext
* @see #ContextLoader(WebApplicationContext)
* @see #CONTEXT_CLASS_PARAM
* @see #CONFIG_LOCATION_PARAM
*/
public WebApplicationContext initWebApplicationContext(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!");
}
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.
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.
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
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;
}
}
initWebApplicationContext()
方法的主要目的就是创建根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);
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
中的
标签配置的全局变量contextConfigLocation
,并在最后调用了refresh()
方法。其中ConfigurableWebApplicationContext
是一个接口。
通过IDEA的导航功能,可以发现一个抽象类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
是全局共享的,因此,在
标识的contextConfigLocation
的xml配置文件一般包括:数据库DataSource、DAO层、Service层、事务等相关Bean。
本文web.xml
中设定了一个characterEncodingFilter
,用于解决中文乱码的问题,此处仅对其进行简要介绍。
characterEncodingFilter
继承自抽象类OncePerRequestFilter
,实现了其doFilterInternal()
方法,在其中对请求与响应进行文字编码相关的操作,源代码如下:
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String encoding = getEncoding();
if (encoding != null) {
if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
request.setCharacterEncoding(encoding);
}
if (isForceResponseEncoding()) {
response.setCharacterEncoding(encoding);
}
}
filterChain.doFilter(request, response);
}
其中FilterChain
是一个接口,其只有doFilter()
一个方法,定义如下:
在Web程序开始运行时,Listener
完成初始化后,各个过滤器的init()
方法就会被调用,且只会被调用一次,顺序取决于web.xml
中的定义顺序。程序运行过程中,过滤器会对用户的请求、服务器的响应进行过滤,符合规则就放行,不符合规则就进行拦截或修改,期间的数据就是由FilterChain
承载并传递的。关于其具体运作方式,此处就不展开了。
Servlet是Spring MVC的核心,用于获取、分发用户请求,并返回服务器响应。大部分情况下使用Spring提供的DispatcherServlet
即可,其是Spring MVC中唯一的Servlet。DispatcherServlet
的继承关系如下:
Servlet的生命周期分为3个阶段:初始化,运行和销毁。而其初始化阶段可分为:
DispatcherServlet
本质上还是一个Servlet,所以初始化也是通过init()
方法来完成的。但是,在DispatcherServlet
类中并没有直接出现init()
方法,而是覆盖了FrameworkServlet
类的onRefresh()
方法,其中调用了initStrategies()
方法:
/**
* This implementation calls {@link #initStrategies}.
*/
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
/**
* Initialize the strategy objects that this servlet uses.
* 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);
}
可以看到,initStrategies()
方法对不同功能模块进行了初始化,各模块功能如下:
initMultipartResolver()
:初始化MultipartResolver,用于处理文件上传服务。如果有文件上传,那么就会将当前的HttpServletRequest包装成DefaultMultipartHttpServletRequest,并且将每个上传的内容封装成CommonsMultipartFile对象。需要在dispatcherServlet-servlet.xml
中配置文件上传解析器。initLocaleResolver()
:用于处理应用的国际化问题,本地化解析策略。initThemeResolver()
:用于定义一个主题。initHandlerMapping()
:用于定义请求映射关系。initHandlerAdapters()
:用于根据Handler的类型定义不同的处理规则。initHandlerExceptionResolvers()
:当Handler处理出错后,会通过此将错误日志记录在log文件中,默认实现类是SimpleMappingExceptionResolver。initRequestToViewNameTranslators()
:将指定的ViewName按照定义的 RequestToViewNameTranslators替换成想要的格式。initViewResolvers()
:用于将View解析成页面。initFlashMapManager()
:用于生成FlashMap管理器。顺藤摸瓜,可以发现,onRefresh()
方法被initWebApplicationContext()
方法调用了。
initWebApplicationContext()
方法在FrameworkServlet
类中:
/**
* Initialize and publish the WebApplicationContext for this servlet.
* Delegates to {@link #createWebApplicationContext} for actual creation
* of the context. Can be overridden in subclasses.
* @return the WebApplicationContext instance
* @see #FrameworkServlet(WebApplicationContext)
* @see #setContextClass
* @see #setContextConfigLocation
*/
protected WebApplicationContext initWebApplicationContext() {
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
cwac.setParent(rootContext);
}
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
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(wac);
}
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
和之前Listener初始化时有点类似,initWebApplicationContext()
的主要作用同样是创建一个WebApplicationContext
对象,即Ioc容器
,不过前文讲过每个Web应用最多只能存在一个根IoC容器
,这里创建的实际上是特定Servlet拥有的子IoC容器
。IoC容器
存在父与子的关系,类似于类的继承,父容器不能访问子容器定义的Bean
,但子容器能访问父容器定义的Bean
。对于DispatcherServlet
,其父容器就是根IoC容器
。只被特定Servlet
使用的Bean
,一般在其对应的子容器中声明,所以Controller
, HandlerAdapter
, ViewResolver
等Spring MVC组件,就由DispatcherServlet
对应的子IoC容器
管理。Spring MVC IoC容器关系如下:
说回方法的调用关系,initWebApplicationContext()
在initServletBean()
中被调用了,而FrameworkServlet
继承自HttpServletBean
,覆盖了其initServletBean()
方法。
来到了HttpServletBean
,其initServletBean()
方法内容为空,在init()
方法中被调用:
/**
* Map config parameters onto bean properties of this servlet, and
* invoke subclass initialization.
* @throws ServletException if bean properties are invalid (or required
* properties are missing), or if subclass initialization fails.
*/
@Override
public final void init() throws ServletException {
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
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()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
initServletBean();
}
终于找到init()
方法了。HttpServletBean
继承了HttpServlet
,而HttpServlet
又继承了GenericServlet
,通过调用查找发现init()
在GenericServlet
中被其init()
方法调用:
/**
* Called by the servlet container to indicate to a servlet that the
* servlet is being placed into service. See {@link Servlet#init}.
*
* This implementation stores the {@link ServletConfig}
* object it receives from the servlet container for later use.
* When overriding this form of the method, call
* super.init(config)
.
*
* @param config the ServletConfig
object
* that contains configuration
* information for this servlet
*
* @exception ServletException if an exception occurs that
* interrupts the servlet's normal
* operation
*
* @see UnavailableException
*/
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
这里稍微有点绕圈子,此处的this.init()
是在HttpServletBean
中被覆盖的那个方法,而调用它的init()
,则是实现了Servlet
接口的init()
方法。
至此,梳理一下顺序,我们可以得到一张DispatcherServlet
初始化的调用逻辑图:
在执行完onRefresh()
方法后,DispatcherServlet
就初始化完成了。
图中涉及到一些Spring MVC组件:
DispatcherServlet
:前端控制器(不需要程序员开发)HandlerMapping
:处理器映射器(不需要程序员开发)Handler
:处理器(需要程序员开发)HandlerAdapter
:处理器适配器ViewResolver
:视图解析器(不需要程序员开发)View
:视图 (需要程序员开发 )Model
:模型流程具体步骤说明:
SpringMVC 启动流程及相关源码分析
Listener、Filter、Servlet的创建及初始化顺序
【Spring MVC】DispatcherServlet详解(容器初始化超详细过程源码分析)