本文中使用的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有两个主要的实现:
RequestMappingHandlerMappingSimpleUrlHandlerMapping
前者用于对@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初始化完毕。
未完,待续。