SpringMVC(二):DispatcherServlet的初始化过程

上篇介绍了SpringMVC相关的基础类,下面分析一下各个类是如何工作的。

首先从web.xml这个配置文件开始,在介绍Servlet的时候,提到过:

ApplicationContext是Spring的核心,在介绍Spring的时候知道它是一个Bean容器,在SpringMVC中,会用到的是WebApplicationContext,它继承自ApplicationContext。

WebApplicationContext的初始化方式和以前介绍的BeanFactory、ApplicationContext容器有所区别,因为WebApplicationContext需要ServletContext实例,也就是说它必须在拥有Web容器的前提下才能完成启动的工作。

在web.xml中配置自启动的Servlet或定义Web容器监听器(ServletContextListener),借助着两者中的任何一个,我们就可以启动Spring Web应用上下文的工作。

下面就来介绍一下这个web监听容器ServletContextListener。

一、ContextLoaderListener

这两个名字非常相似,首先ServletContextListener是一个接口,使用这个接口,它的位置是package javax.servlet,与Spring无关,使用这个接口,开发者能够在为客户端请求提供服务之前向ServletContext中添加任意的对象。

而ContextLoaderListener就是Spring用来实现这一接口的类,它向ServletContext中添加的对象就是一个WebApplicationContext对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.springframework.web.context;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}

public ContextLoaderListener(WebApplicationContext context) {
super(context);
}

@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}

@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}

ContextLoaderListener实现了ServletContextListener接口,在Servlet容器启动的时候,会初始化一个WebApplicationContext的实现类,并将其作为ServletContext的一个属性设置到Servlet环境中,具体实现代码在父类ContextLoader中:

1
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

ContextLoaderListener所初始化的这个Spring容器上下文,被称为根上下文

下面会介绍到,Spring在DispatcherServlet的初始化过程中,同样会初始化一个WebApplicationContext的实现类,作为自己独有的上下文,这个独有的上下文,会将上面的根上下文作为自己的父上下文,来存放SpringMVC的配置元素,然后同样作为ServletContext的一个属性,被设置到ServletContext中,只不过它的key就稍微有点不同,key和具体的DispatcherServlet注册在web.xml文件中的名字有关,从这一点也决定了,我们可以在web.xml文件中注册多个DispatcherServlet,因为Servlet容器中注册的Servlet名字肯定不一样,设置到Servlet环境中的key也肯定不同。

由于在Spring容器中,子上下文可以访问到所有父上下文中的信息,而父上下文访问不到子上下文的信息,这个根上下文,就很适合作为多个子上下文配置的集中点。以官方文档中的图来说明:

img

还有一张图比较直观:

从图中可以看出:

  • ContextLoaderListener初始化的上下文加载的Bean是对于整个应用程序共享的,不管是使用什么表现层技术,一般如DAO层、Service层Bean;
  • DispatcherServlet初始化的上下文加载的Bean是只对Spring Web MVC有效的Bean,如Controller、HandlerMapping、HandlerAdapter等等,该初始化上下文应该只加载Web相关组件。

下面具体介绍一下Servlet接口的实现类,也是SpringMVC的核心,DispatcherServlet。

二、DispatcherServlet

1、类继承关系

再来看一下它的继承关系。

QQ截图20170109110308

它最上面继承自HttpServlet,下面主要有三个具体的类:

  • HttpServletBean
  • FrameworkServlet
  • DispatcherServlet

通过这三个不同层次的类,来抽象不同的职责。

而因为它继承自HttpServlet,所以先简单回顾一下Servlet的生命周期:

  1. 初始化阶段 init()方法

    容器调用 init() 方法(一生只调一次,并且是在第一次调用Servlet的时候执行)

  2. 运行阶段 Service()方法

  3. 销毁阶段 destroy() 方法

这篇文章介绍的主要就是初始化方法,也就是从web容器调用init()方法开始。

2、HttpServletBean类与init方法

init方法在HttpServletBean类中:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package org.springframework.web.servlet;

@SuppressWarnings("serial")
public abstract class HttpServletBean extends HttpServlet
implements EnvironmentCapable, EnvironmentAware {

/** 上面的方法都省略了 */

/**
* 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 {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
}

// Set bean properties from init parameters.
//1、如下代码的作用是将Servlet初始化参数设置到该组件上
try {
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
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) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
throw ex;
}

// Let subclasses do whatever initialization they like.
//2、提供给子类初始化的扩展点,该方法由FrameworkServlet覆盖
initServletBean();

if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}

/**
* 下面的方法都省略了
*/
}

该方法主要做了两件事:

  • 将Servlet初始化参数(init-param)设置到该组件上(如contextAttribute、contextClass、namespace、contextConfigLocation),通过BeanWrapper简化设值过程,方便后续使用;

  • 提供给子类初始化扩展点,initServletBean(),该方法由FrameworkServlet覆盖。

关于这个设置属性,HttpServletBean这个类的设计中,运用了依赖注入思想完成了<init-param>配置元素的读取。他抽离出HttpServletBean这个类的目的也在于此,就是“以依赖注入的方式来读取Servlet类的<init-param>配置信息”,而且这里很明显是一种setter注入。

明白了HttpServletBean类的设计思想,我们也就知道可以如何从中获益。具体来说,我们继承HttpServletBean类(就像DispatcherServlet做的那样),在类中定义一个属性,为这个属性加上setter方法后,我们就可以在<init-param>元素中为其定义值。在类被初始化后,值就会被注入进来,我们可以直接使用它,避免了样板式的getInitParameter()方法的使用,而且还免费享有Spring中资源编辑器的功能,可以在web.xml中,通过“classpath:”直接指定类路径下的资源文件。

个人理解:从这个类的名字上也可以看出它的作用,HttpServletBean,分别为HttpServlet和Bean,一方面继承自HttpServlet,重写init方法作为入口,另一方面又是Bean,并且加载了在web.xml里面通过参数配置,来实现依赖注入。

下面就是调用的initServletBean方法。

2、FrameworkServlet类与initServletBean方法

这个方法在子类FrameworkServlet中重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.springframework.web.servlet;


@SuppressWarnings("serial")
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {

/** 省略 */
/**
* Overridden method of {@link HttpServletBean}, invoked after any bean properties
* have been set. Creates this servlet's WebApplicationContext.
*/
@Override
protected final void initServletBean() throws ServletException {
//省略部分代码
try {
//1、初始化Web上下文
this.webApplicationContext = initWebApplicationContext();
//2、提供给子类初始化的扩展点
initFrameworkServlet();
}
//省略部分代码
}
/** 省略 */
}

该方法主要做了两件事:

  • 初始化web上下文
  • 提供给子类初始化扩展点

这个上下文是DispatcherServlet创建的上下文,和上面Listener创建的上下文是父子关系。

下面是初始化web上下文的代码:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
  
protected WebApplicationContext initWebApplicationContext() {
//1.
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;

//2.
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);
}
}
}

//3.
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();
}

//4.
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
}

//5
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(wac);
}

//6.
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;
}

protected void onRefresh(ApplicationContext context) {
// For subclasses: do nothing by default.
}

结合上面的注释,看一下具体过程:

  1. 获取由ContextLoaderListener初始化并注册在ServletContext中的根上下文,记为rootContext
  2. 如果webApplicationContext已经不为空,表示这个Servlet类是通过编程式注册到容器中的(Servlet 3.0+中的ServletContext.addServlet() ),上下文也由编程式传入。若这个传入的上下文还没被初始化,将rootContext上下文设置为它的父上下文,然后将其初始化,否则直接使用。
  3. 通过wac变量的引用是否为null,判断第2步中是否已经完成上下文的设置(即上下文是否已经用编程式方式传入),如果wac==null成立,说明该Servlet不是由编程式注册到容器中的。此时以contextAttribute属性的值为键,在ServletContext中查找上下文,查找得到,说明上下文已经以别的方式初始化并注册在contextAttribute下,直接使用。
  4. 检查wac变量的引用是否为null,如果wac==null成立,说明2、3两步中的上下文初始化策略都没成功,此时调用createWebApplicationContext(rootContext),建立一个全新的以rootContext为父上下文的上下文,作为SpringMVC配置元素的容器上下文。大多数情况下我们所使用的上下文,就是这个新建的上下文。
  5. 以上三种初始化上下文的策略,都会回调onRefresh(ApplicationContext context)方法(回调的方式根据不同策略有不同),onRefresh方法在DispatcherServlet类中被覆写,以上面得到的上下文为依托,完成SpringMVC中默认实现类的初始化。
  6. 最后,将这个上下文发布到ServletContext中,也就是将上下文以一个和Servlet类在web.xml中注册名字有关的值为键,设置为ServletContext的一个属性。你可以通过改变publishContext的值来决定是否发布到ServletContext中,默认为true。

以上面6点跟踪FrameworkServlet类中的代码,可以比较清晰的了解到整个容器上下文的建立过程,也就能够领会到FrameworkServlet类的设计目的,它是用来建立一个和Servlet关联的Spring容器上下文,并将其注册到ServletContext中的。跳脱开SpringMVC体系,我们也能通过继承FrameworkServlet类,得到与Spring容器整合的好处,FrameworkServlet和HttpServletBean一样,是一个可以独立使用的类。整个SpringMVC设计中,处处体现开闭原则,这里显然也是其中一点。

可以看见最下面的onRefresh方法,和上面的套路一样,都是留给子类实现的。

3、DispatcherServlet类与onRefresh方法

FrameworkServlet的子类自然就是DispatcherServlet了,看一下它的onRefresh方法:

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
package org.springframework.web.servlet;

@SuppressWarnings("serial")
public class DispatcherServlet extends FrameworkServlet {

/**
* This implementation calls {@link #initStrategies}.
*/
@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);
}
}

DispatcherServlet类覆写了父类FrameworkServlet中的onRefresh(ApplicationContext context)方法,提供了SpringMVC各种编程元素的初始化。当然这些编程元素,都是作为容器上下文中一个个bean而存在的。具体的初始化策略,在initStrategies()方法中封装。

4、initHandlerMappings方法

我们以其中initHandlerMappings(context)方法为例,分析一下这些SpringMVC编程元素的初始化策略,其他的方法,都是以类似的策略初始化的。

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
32
33
34
35
36
37
/**
* Initialize the HandlerMappings used by this class.
* <p>If no HandlerMapping beans are defined in the BeanFactory for this namespace,
* we default to BeanNameUrlHandlerMapping.
*/
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;

if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}

// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isDebugEnabled()) {
logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
}
}
}

这段光是看代码和注释就可以看明白,关键的一个变量在于this.detectAllHandlerMappings

  • 如果为true:会把上下文中所有HandlerMapping类型的Bean都注册在handlerMappings这个List变量中。
  • 如果为false:那么将尝试获取名为handlerMapping的Bean,新建一个只有一个元素的List,将其赋给handlerMappings
  • 如果经过上面的过程,handlerMappings变量仍为空,那么说明你没有在上下文中提供自己HandlerMapping类型的Bean定义。此时,SpringMVC将采用默认初始化策略来初始化handlerMappings。

而这个变量默认为true,可以通过修改web.xml中DispatcherServlet的初始参数来将其设置为false

1
2
3
4
<init-param>
<param-name>detectAllHandlerMappings</param-name>
<param-value>false</param-value>
</init-param>

下面再看一下这个默认的初始化策略:

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
32
33
34
35
36
37
38
39
/**
* Create a List of default strategy objects for the given strategy interface.
* <p>The default implementation uses the "DispatcherServlet.properties" file (in the same
* package as the DispatcherServlet class) to determine the class names. It instantiates
* the strategy objects through the context's BeanFactory.
* @param context the current WebApplicationContext
* @param strategyInterface the strategy interface
* @return the List of corresponding strategy objects
*/
@SuppressWarnings("unchecked")
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
String key = strategyInterface.getName();
String value = defaultStrategies.getProperty(key);
if (value != null) {
String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
List<T> strategies = new ArrayList<>(classNames.length);
for (String className : classNames) {
try {
Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
Object strategy = createDefaultStrategy(context, clazz);
strategies.add((T) strategy);
}
catch (ClassNotFoundException ex) {
throw new BeanInitializationException(
"Could not find DispatcherServlet's default strategy class [" + className +
"] for interface [" + key + "]", ex);
}
catch (LinkageError err) {
throw new BeanInitializationException(
"Error loading DispatcherServlet's default strategy class [" + className +
"] for interface [" + key + "]: problem with class file or dependent class", err);
}
}
return strategies;
}
else {
return new LinkedList<>();
}
}

它是一个范型的方法,承担所有SpringMVC编程元素的默认初始化策略比如HandlerAdapters的默认初始化策略也通过它。方法的内容比较直白,就是以传递类的名称为键,从defaultStrategies这个Properties变量中获取实现类,然后反射初始化。

需要说明一下的是defaultStrategies变量的初始化,它是在DispatcherServlet的静态初始化代码块中加载的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";

private static final Properties defaultStrategies;

static {
// Load default strategy implementations from properties file.
// This is currently strictly internal and not meant to be customized
// by application developers.
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage());
}
}

这个DispatcherServlet.properties里面,以键值对的方式,记录了SpringMVC默认实现类,在org.springframework.web.servlet包里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

spring 3.1 以后有些类可能进行了替换,比如

RequestMappingHandlerMapping 来替换 DefaultAnnotationHandlerMapping,

用 RequestMappingHandlerAdapter 来替换 AnnotationMethodHandlerAdapter。

看一下默认的几个HandlerMapping的作用:

  • BeanNameUrlHandlerMapping:通过对比url和bean的name找到对应的对象
  • RequestMappingHandlerMapping:主要是针对注解配置@RequestMapping的,取代了最早的DefaultAnnotationHandlerMapping

再看一下HandlerAdapter:

  • HttpRequestHandlerAdapter:要求handler实现Controller接口,该接口的方法为

    ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)

  • SimpleControllerHandlerAdapter:要求handler实现HttpRequestHandler接口,该接口的方法为:

    void handleRequest(HttpServletRequest request, HttpServletResponse response)

  • RequestMappingHandlerAdapter:和上面的RequestMappingHandlerMapping配对使用,针对@RequestMapping,取代了最早的AnnotationMethodHandlerAdapter。

至此,我们分析完了initHandlerMappings(context)方法的执行过程,其他的初始化过程与这个方法非常类似。所有初始化方法执行完后,SpringMVC正式完成初始化,静静等待Web请求的到来。

三、总结

上面介绍了DispatcherServlet对应Servlet生命周期的init阶段,可以发现HttpServletBean、FrameworkServlet、DispatcherServlet三个不同的类层次,SpringMVC的设计者将三种不同的职责分别抽象,运用模版方法设计模式分别固定在三个类层次中。

  • HttpServletBean完成的是<init-param>配置元素的依赖注入
  • FrameworkServlet完成的是容器上下文的建立
  • DispatcherServlet完成的是SpringMVC具体编程元素的初始化策略。

四、参考地址

https://my.oschina.net/lichhao/blog/100138

https://my.oschina.net/lichhao/blog/102315

http://blog.csdn.net/lqx1988221/article/details/12620689