本文中使用的Spring框架的版本为5.1
前置知识
从设计上说,Spring Web MVC 使用前端控制器模式围绕一个中心Servlet
进行设计,这个中心Servlet
就是DispatcherServlet
,在DispatcherServlet
中提供了用于处理请求的通用逻辑,而具体工作委托给可配置的组件执行,通过这种模式使得Spring Web MVC框架变得非常灵活。
与其它的Servlet
一样,DispatcherServlet
需要根据Servlet规范使用Java代码或者在web.xml
文件中进行配置。
Spring Web MVC在启动时,最先加载DispatcherServlet
,然后DispatcherServlet
再根据配置加载请求映射、视图解析、异常处理所需的组件。
下面是在web.xml
文件中对DispatcherServlet
进行配置的示例:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
复制代码
一些完成具体工作的Bean
在对请求进行处理时,DispatcherServlet
会把委托下面这些Bean对请求进行处理,以及给出适当的响应结果。
HandlerMapping
将请求与handler
进行映射,HandlerMapping
有两个主要的实现:
RequestMappingHandlerMapping
SimpleUrlHandlerMapping
前者用于对@RequestMapping
注解提供支持,后者支持控制器的显示注册。
HandlerAdapter
帮助DispatcherServlet
调用与请求路径匹配的handler
去处理请求。
通过使用适配器的方式使DispatcherServlet
不用关心handler
的具体实现细节,比如:调用@Controller注解的控制器需要对该注解进行处理。
HandlerExceptionResolver
使用不同的策略对异常进行处理,比如:返回的HTTP状态码是5xx还是4xx。
ViewResolver
对视图进行解析,比如对JSP与FreeMarker的模版文件进行解析。
LocaleResolver, LocaleContextResolver
提供本地化支持,比如:多国语言、时区等。
ThemeResolver
提供主题支持,用于个性化布局。
MultipartResolver
用于解析multi-part
请求。
FlashMapManager
用于管理存放Falsh Attribute
的FlashMap
,Falsh Attribute
用于跨请求传递数据。
源码分析
从源码中可以找到DispatcherServlet
类的定义如下:
public class DispatcherServlet extends FrameworkServlet
复制代码
可以看出,它继承自类FrameworkServlet
,下面我们再来看一下类的整体继承关系:
从类的继承关系来看,最后会通过实现Servlet
接口来处理HTTP请求,其中与Servlet
有关系的类按继承顺序从上至下分别是:
- GenericServlet,一个抽象类,实现了
Servlet
接口。 - HttpServlet,一个抽象类,继承自
GenericServlet
。 - HttpSerlvetBean,一个抽象类,继承自
HttpServlet
。 - FrameworkServlet,一个抽象类,继承自
HttpServlet
。 - DispatcherServlet,一个具体类,继承自
FrameworkServlet
。
其中GenericServlet
与HttpServlet
类只是简单对Servlet
接口做了一些封装与扩展,因此可以把分析的重点放在HttpSerlvetBean
、FrameworkServlet
与DispatcherServlet
这三个类上面。
在DispatcherServlet
初始化时,这三个类之间调用顺序如下图所示:
根据Servlet
规范,在Servlet
接口中会存在一个init()
方法,在一个Servlet
实被例化之后容器将会调用一次该方法。Spring Web MVC通过在HttpSerlvetBean
类中覆写init()
方法从而实现整个框架的加载。
HttpServletBean类
在HttpSerlvetBean
类的init()
方法中主要做了两件事,一是从web.xml文件中读取初始化参数,比如:contextConfigLocation
参数。二是调用由子类实现的initServletBean()
方法完成具体的初始化工作。
init()
方法的实现如下:
@Override
public final void init() throws ServletException {
// 从web.xml文件中读取初始化参数
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;
}
}
// 调用由子类实现的`initServletBean()`方法。
initServletBean();
}
复制代码
在上面这段代码中有点意思的是从web.xml
文件中读取初始化参数的方式,在这里我们拿contextConfigLocation
参数来举例子。
对于参数contextConfigLocation
而言,在HttpServletBean
的子类FrameworkServlet
中存在一个具有以下定义的私有变量:
@Nullable
private String contextConfigLocation;
复制代码
以及对应的Get与Set方法:
public void setContextConfigLocation(@Nullable String contextConfigLocation) {
this.contextConfigLocation = contextConfigLocation;
}
@Nullable
public String getContextConfigLocation() {
return this.contextConfigLocation;
}
复制代码
但是Spring并不会直接调用setContextConfigLocation()
方法来给contextConfigLocation
变量赋值,而是由BeanWrapper
搭配ResourceEditor
来给变量赋值。
init()
方法在读取初始化参数之后,便会调用initServletBean()
方法来做初始化工作,该方法的在HttpServletBean
中是一个被protected
修饰的空方法,其定义如下:
protected void initServletBean() throws ServletException
复制代码
而具体的初始化工作则在HttpServletBean
的子类FrameworkServlet
中通过覆写initServletBean()
方法来完成。
FrameworkServlet类
initServletBean()
方法的实现如下:
protected final void initServletBean() throws ServletException {
try {
// 初始化WebApplicationContext
this.webApplicationContext = initWebApplicationContext();
// 一个Hook,让子类有机会在上下文初始化后做一些相关的工作
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
}
复制代码
在该方法中主要做了下面两件事:
- 调用
initWebApplicationContext()
方法初始化webApplicationContext
。 - 调用预留的
initFrameworkServlet()
方法让子类在初始化之后有机会做一些额外的工作。
事实上,目前initFrameworkServlet()
是一个没有使用的空函数,并且子类也没有对它进行覆写,所以我们只需要关注initWebApplicationContext()
方法即可。
initWebApplicationContext()
方法的实现如下:
protected WebApplicationContext initWebApplicationContext() {
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()) {
// 如果该上下文还没有进行过refresh则为它设置一个ID并进行refresh
if (cwac.getParent() == null) {
// 如果该的上下文没有父上下文则为它设置一个。
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// 如果实例化时没有提供上下文,则查找ServletContext中有没有提供。
wac = findWebApplicationContext();
}
if (wac == null) {
// 如果上面没找到一个已经存在的上下文,则自己创建一个
wac = createWebApplicationContext(rootContext);
}
// 如果onRefresh还没有被调用,则手动调用一次
if (!this.refreshEventReceived) {
synchronized (this.onRefreshMonitor) {
// 调用子类实现的onRefresh初始化策略对象
onRefresh(wac);
}
}
if (this.publishContext) {
// 将WebApplicationContext放入ServletContext之中
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
复制代码
这个方法中主要做了下面这些事:
第一,检查是不是通过构造函数FrameworkServlet(WebApplicationContext webApplicationContext)
传入了webApplicationContext
参数。如果是,则会对传入的webApplicationContext
进行一些配置,比如:设置父上下文与上下文ID。
第二,检查ServletContext中有没有提供webApplicationContext
。如果有,拿过来直接使用。
第三,如果在第一步与第二步中都没有发现可用的webApplicationContext
,那就调用createWebApplicationContext()
方法自己创建一个。
第四,做一次兜底,通过refreshEventReceived
变量的值判断是否调用过onRefresh()
方法,如果从未调用过,则触发一次调用。
onRefresh
方法除了在initWebApplicationContext()
方法中调用了之外,在FrameworkServlet
中onApplicationEvent()
方法中也调用了onRefresh
方法。
onApplicationEvent()
方法的实现如下:
public void onApplicationEvent(ContextRefreshedEvent event) {
this.refreshEventReceived = true;
synchronized (this.onRefreshMonitor) {
onRefresh(event.getApplicationContext());
}
}
复制代码
在上面的代码中除了调用onRefresh()
方法之外,还给变量refreshEventReceived
赋值为真,确保onRefresh()
方法只会被调用一次。
那么问题来了,又是谁在调用onApplicationEvent()
方法?
对于这个问题我们先来看一下用于创建webApplicationContext
的createWebApplicationContext()
方法的实现:
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
Class<?> contextClass = getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(getEnvironment());
wac.setParent(parent);
// 使用configLocation所指定的配置文件
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
// 对webApplicationContext进行配置
configureAndRefreshWebApplicationContext(wac);
return wac;
}
复制代码
在这个方法中,主要做了两件事:一是使用configLocation
所指定的配置文件来加载Bean,二是调用configureAndRefreshWebApplicationContext()
方法对webApplicationContext
进行配置。
如果你还有印象的话,你可能记得下面这段位于initWebApplicationContext()
方法中的代码也调用过 configureAndRefreshWebApplicationContext()
方法:
if (this.webApplicationContext != null) {
// 实例化时如果提供了webApplicationContext参数,则使用它。
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
// 如果该上下文还没有进行过refresh则为它设置一个ID并进行refresh
if (cwac.getParent() == null) {
// 如果该的上下文没有父上下文则为它设置一个。
cwac.setParent(rootContext);
}
// 对webApplicationContext进行配置
configureAndRefreshWebApplicationContext(cwac);
}
}
}
复制代码
于是乎,我们要研究一下在configureAndRefreshWebApplicationContext()
方法中做了哪些不为人知的事情。
configureAndRefreshWebApplicationContext()
方法的实现如下:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// 如果上下文的ID原装的,则为其设置一个更具可读性的ID
// 使用配置文件指定的ID
if (this.contextId != null) {
wac.setId(this.contextId);
}
else {
// 生成一个默认的ID
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
}
}
wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.setNamespace(getNamespace());
// 添加一个Listener用于监听webApplicationContext的refresh事件
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
// 初始化PropertySource
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
}
postProcessWebApplicationContext(wac);
// 调用ApplicationContextInitializer
applyInitializers(wac);
// 调用webApplicationContext的refresh方法
wac.refresh();
}
复制代码
在上面的代码中做了这么几件事:
- 第一步,为webApplicationContext设置一个ID。
- 第二步,添加一个ContextRefreshListener用于监听webApplicationContext的refresh事件。
- 第三步,初始化PropertySource。
- 第四步,调用webApplicationContext的refresh方法。
在上面第二步中,ContextRefreshListener
类的实现如下:
private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
FrameworkServlet.this.onApplicationEvent(event);
}
}
复制代码
通过这个类的实现可以看出,这个类作为FrameworkServlet
的内部类在收到上下文已刷新的事件后会调用onApplicationEvent()
方法。
现在我们已经可以前面的问题了:是谁在调用onApplicationEvent()
方法?。
通过上述的分析我们可以知道在configureAndRefreshWebApplicationContext()
方法中对webApplicationContext
设置了一个监听器,这个监听器在监听到上下文refresh之后会调用onApplicationEvent()
方法。
既然onApplicationEvent()
方法与initWebApplicationContext()
都会调用onRefresh()
方法,那么在onRefresh()
方法中又做了哪些事情?
onRefresh()
方法在FrameworkServlet
中的定义如下:
protected void onRefresh(ApplicationContext context) {
}
复制代码
可见,它在FrameworkServlet
类中是一个空方法。具体逻辑由FrameworkServlet
的子类DispatcherServlet
覆写这个方法来实现,下文将对它的具体实现进行分析。
DispatcherServlet类
DispatcherServlet
类中onRefresh()
方法的实现如下:
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
// 初始化解析multi-part请求需要的组件
initMultipartResolver(context);
// 初始化用于提供本地化支持的主键
initLocaleResolver(context);
// 初始化用于提供主题支持的组件
initThemeResolver(context);
// 初始化用于请求映射的组件
initHandlerMappings(context);
// 初始化用于对handler进行适配的组件
initHandlerAdapters(context);
// 初始化用于异常处理的组件
initHandlerExceptionResolvers(context);
// 初始化用于视图解析的组件
initRequestToViewNameTranslator(context);
initViewResolvers(context);
// 初始化用于管理Flash Attribute的组件
initFlashMapManager(context);
}
复制代码
通过上面的代码中可以看出,onRefresh()
方法将具体的工作委托给了initStrategies()
方法。
在initStrategies()
方法中,初始化了一系列的策略对象用于对不同的功能提供支持,比如:请求映射、视图解析、异常处理等。
至此,DispatcherServlet
初始化完毕。
未完,待续。