前言
内容主要参考自《Spring源码深度解析》一书,算是读书笔记或是原书的补充。进入正文后可能会引来各种不适,毕竟阅读源码是件极其痛苦的事情。
本文主要涉及书中第十一章的部分,依照书中内容以及个人理解对Spring源码进行了注释,详见Github仓库:https://github.com/MrSorrow/spring-framework
Spring框架提供了构建Web应用程序的全功能MVC模块。Spring MVC分离了控制器、模型对象、分派器以及处理程序对象的角色,这种分离让它们更容易进行定制。
Spring的MVC是基于Servlet功能实现的,通过实现Servlet接口的DispatcherServlet来封装其核心功能实现,通过将请求分派给处理程序,同时带有可配置的处理程序映射、视图解析、本地语言、主题解析以及上传文件支持。默认的处理程序是非常简单的 Controller
接口,只有一个方法 ModelAndView handleRequest(request, response)
。Spring提供了一个控制器层次结构,可以派生出许多子类。
SpringMVC解决的问题无外乎以下几点:
- 将Web页面的请求传给服务器
- 根据不同的请求利用不同的逻辑单元进行处理
- 返回处理的结果数据并跳转至响应的页面
本文源码分析部分也主要分为三块,分别研究Spring父容器的加载,DispatcherServlet
初始化 (包含Spring MVC子容器的加载) 以及 DispatcherServlet
处理Web请求的过程。
I. SpringMVC测试示例
这一部分由于坑比较多,又单独开了一篇文章,专门讲解Spring源码工程中如何搭建SpringMVC的测试模块的。详见:Spring源码——SpringMVC测试工程搭建。如果对SpringMVC不熟悉如何使用的,建议先查找相关资料学习一下,这里就不多提了。相信你都看源码了,SpringMVC肯定是会用的。
II. ContextLoaderListener
对于SpringMVC功能实现的分析,我们首先从 web.xml 开始,在 web.xml 文件中我们首先配置的就是 ContextLoaderListener
,那么它所提供的功能有哪些又是如何实现的呢?
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
当使用编程方式的时候我们可以直接将Spring配置文件路径作为参数传入Spring容器中,如下:
ApplicationContext context = new ClassPathXmlApplicationContext(“applicationContext.xml”);
但是我们在第一部分测试示例中,包括日常开发,并没有去将配置文件的路径参数显示的传递给容器。实际上是靠在 web.xml 配置 <context-param> 标签来进行设置路径的,那么可以推测Spring一定能够获得这个配置参数,去指定路径加载配置文件。
<!--Spring配置文件-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-config.xml</param-value>
</context-param>
没错,做到这件事情的正是 org.springframework.web.context.ContextLoaderListener
监听器。ContextLoaderListener
的作用就是启动 Web 容器时,自动装配 ApplicationContext 的配置信息。因为 ContextLoaderListener
实现了 ServletContextListener
这个接口,在 web.xml 中配置了这个监听器,启动 Web 容器时,就会默认执行它实现的方法,使用 ServletContextListener
接口,开发者能够在为客户端请求提供服务之前向ServletContext中添加任意的对象。这个对象在ServletContext启动的时候被初始化,然后在ServletContext整个运行期间都是可见的。
每一个Web应用都有一个 ServletContext
与之相关联。``ServletContext对象在应用启动时被创建,在应用关闭的时候被销毁。
ServletContext` 在全局范围内有效,类似于Web应用中的一个全局变量。
使用ServletContextListener
ContextLoaderListener
实现了 ServletContextListener
这个接口,我们具体看一下 ContextLoaderListener
的类继承结构。
ServletContextListener
并不是Spring中的接口,而是 javax.servlet
包下的。它包含两个接口,分别在 Web 应用启动时执行和 ServletContext 将要关闭时执行。
public interface ServletContextListener extends EventListener {
/**
** Notification that the web application initialization process is starting.
* All ServletContextListeners are notified of context initialization before
* any filter or servlet in the web application is initialized.
* The default implementation is a NO-OP.
* @param sce Information about the ServletContext that was initialized
*/
public default void contextInitialized(ServletContextEvent sce) {
}
/**
** Notification that the servlet context is about to be shut down. All
* servlets and filters have been destroy()ed before any
* ServletContextListeners are notified of context destruction.
* The default implementation is a NO-OP.
* @param sce Information about the ServletContext that was destroyed
*/
public default void contextDestroyed(ServletContextEvent sce) {
}
}
而Spring通过实现 ServletContextListener
接口,核心目的便是系统启动时初始化 WebApplicationContext
实例并存放至 ServletContext
中。
正式分析Spring中的代码前我们同样还是首先具体了解 ServletContextListener
的使用。
① 创建自定义ServletContextListener
首先创建 ServletContextListener
实现类,目标是在系统启动时添加自定义的属性,以便于在全局范围内可以随时调用。系统启动的时候会调用 ServletContextListener
实现类的 contextInitialized()
方法,所以需要在这个方法中实现我们的初始化逻辑。
public class MyContextListener implements ServletContextListener {
private ServletContext servletContext;
// 该方法在ServletContext启动之后被调用,并准备好处理客户端请求
@Override
public void contextInitialized(ServletContextEvent sce) {
servletContext = sce.getServletContext();
servletContext.setAttribute("name", "=========wgp========");
System.out.println("web application is starting...");
}
// 这个方法在ServletContext将要关闭的时候调用
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("servlet context is going to shut down...");
System.out.println(servletContext.getAttribute("name"));
}
}
② 注册监听器
在 web.xml 文件中需要注册自定义的监听器。
<listener>
<listener-class>guo.ping.mvctest.context.MyContextListener</listener-class>
</listener>
③ 测试结果
启动项目可以看到 contextInitialized()
方法的执行。
当停止项目时,同样可以看到 contextDestroyed()
方法的执行,同时发现在 contextInitialized()
方法中设置给 ServletContext 的属性成功了。
Spring中的ContextLoaderListener
分析了 ServletContextListener
的使用方式后再来分析Spring中的 ContextLoaderListener
的实现就容易理解的多,虽然 ContextLoaderListener
实现的逻辑要复杂的多,但是大致的套路还是万变不离其宗。
查看 ContextLoaderListener
实现 ServletContextListener
接口的方法内容。可以看到,初始化主要就是为了初始化一个Spring IOC容器。
/**
* Initialize the root web application context.
* 该方法在ServletContext启动之后被调用,并准备好处理客户端请求
*/
@Override
public void contextInitialized(ServletContextEvent event) {
// 初始化WebApplicationContext
initWebApplicationContext(event.getServletContext());
}
/**
* Close the root web application context.
* 这个方法在ServletContext将要关闭的时候调用
*/
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
这里涉及了一个常用类 WebApplicationContext
:在Web应用中,我们会用到 WebApplicationContext
,WebApplicationContext
继承自 ApplicationContext
,在 ApplicationContext
的基础上又追加了一些特定于 Web 的操作及属性,非常类似于我们通过编程方式使用Spring时使用的 ClassPathXmlApplicationContext
类提供的功能。我们查看一下 XmWebApplicationContext
的类继承结构,可以发现它和 ClassPathXmlApplicationContext
基本变化不会太大。
我们正式进入Spring的 ContextLoaderListener
中的 contextInitialized()
方法,内部调用了 initWebApplicationContext(event.getServletContext())
方法初始化 WebApplicationContext 容器。
/**
* 通过ServletContext对象初始化Spring的WebApplicationContext(父容器)
* 该方法在ServletContext启动之后被调用,并准备好处理客户端请求
* 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) {
// web.xml中存在多次ContextLoader定义就会抛出异常
// WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE=org.springframework.web.context.WebApplicationContext.ROOT
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) {
// 创建Spring的WebApplicationContext
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.
// 看看是否有父容器,有的话设置给当前创建的容器,DispatcherServlet没有重写方法,直接返回null
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 设置cwac相关属性并调用refresh
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 记录在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中
currentContextPerThread.put(ccl, this.context);
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext: initialization completed 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(event.getServletContext())
方法主要是体现了创建 WebApplicationContext 实例的一个功能架构,从函数中我们看到了初始化的大致步骤。
① WebApplicationContext存在性验证
在配置中只允许声明一次 ServletContextListener
,多次声明会扰乱Spring的执行逻辑,所以这里首先做的就是对此验证。在Spring中如果创建 WebApplicationContext 实例会记录在 ServletContext
中以方便全局调用,而使用的 key 就是 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,所以验证的方式就是查看 ServletContext
实例中是否有对应 key 的属性。
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!");
}
② 创建WebApplicationContext实例
Spring通过 createWebApplicationContext(servletContext)
方法进行初始化一个 WebApplicationContext 实例。
/**
* 创建WebApplicationContext
* Instantiate the root WebApplicationContext for this loader, either the
* default context class or a custom context class if specified.
* <p>This implementation expects custom contexts to implement the
* {@link ConfigurableWebApplicationContext} interface.
* Can be overridden in subclasses.
* <p>In addition, {@link #customizeContext} gets called prior to refreshing the
* context, allowing subclasses to perform custom modifications to the context.
* @param sc current servlet context
* @return the root WebApplicationContext
* @see ConfigurableWebApplicationContext
*/
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
// 判断WebApplicationContext具体要创建的子类类型
Class<?> contextClass = determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
创建 WebApplicationContext 实例需要两个步骤,首先需要确定具体的实现类类型,毕竟 WebApplicationContext 仅仅只是一个上层接口,之后通过反射创建一个实例即可。
所以Spring委托 determineContextClass(sc)
方法去判断 WebApplicationContext 具体要创建的子类类型。
/**
* 判断决定WebApplicationContext具体要创建的子类类型
* Return the WebApplicationContext implementation class to use, either the
* default XmlWebApplicationContext or a custom context class if specified.
* @param servletContext current servlet context
* @return the WebApplicationContext implementation class to use
* @see #CONTEXT_CLASS_PARAM
* @see org.springframework.web.context.support.XmlWebApplicationContext
*/
protected Class<?> determineContextClass(ServletContext servletContext) {
// 获取ServletContext名称为“contextClass”的初始化参数的值
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
// 如果web.xml中指定了WebApplicationContext具体要创建的子类类型,就用指定的,否则采用默认的
if (contextClassName != null) {
try {
return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load custom context class [" + contextClassName + "]", ex);
}
}
else {
// 默认是org.springframework.web.context.support.XmlWebApplicationContext类型
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}
如果用户在 web.xml 中配置了具体需要创建的容器类型,那么这里就会被获取到。
<context-param>
<param-name>contextClass</param-name>
<param-value>具体的容器类型</param-value>
</context-param>
如果用户没有配置,则会从 defaultStrategies 获取名为 WebApplicationContext.class.getName() 的属性值,那么 defaultStrategies 什么时候存储了这个属性呢?查看 ContextLoader
类中的一段静态代码块。
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.
// 从ContextLoader.properties配置文件中读取默认实现类
try {
// DEFAULT_STRATEGIES_PATH = "ContextLoader.properties"
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
}
}
根据以上静态代码块的内容,我们推断在当前类 ContextLoader
同样目录下必定会存在属性文件ContextLoader.properties
,查看后果然存在。
所以用户如果没有配置具体的 WebApplicationContext 要创建的子类类型,Spring默认的实现类型为 XmlWebApplicationContext
。
③ 刷新
在 initWebApplicationContext()
方法中除了创建实例一句关键代码之外,还有一个方法 configureAndRefreshWebApplicationContext(cwac, servletContext)
,也非常重要。
研究该方法之前,我们先来看一下 WebApplicationContext 实例的 debug 内容,可以看到很多内容都是空,包括最重要的 beanFactory 容器也是空。
那么 configureAndRefreshWebApplicationContext(cwac, servletContext)
主要就是为了初始化 WebApplicationContext 实例的各种内容,我们具体进入方法一探究竟。
/**
* 配置并刷新WebApplicationContext
* @param wac
* @param sc
*/
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
// 替换WebApplicationContext容器的id,起一个更有意义的名字。如果ServletContext配置了则使用配置,否则默认规则起名
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else {
// 生成默认id替换,WebApplicationContext全限定类名+":"+项目名
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
// 将ServletContext设置给Spring容器
wac.setServletContext(sc);
// 设置Spring容器的配置文件路径
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);
// 调用Spring容器的refresh()方法,加载配置文件
wac.refresh();
}
更新 id
从之前的截图可以看到,WebApplicationContext 实例的 id 为 org.springframework.web.context.support.XmlWebApplicationContext@3a2efed9,为了更好的区别容器,Spring对 id 进行了更名。同样,如果用户在 web.xml 中配置了 contextId 这个参数值的话,就将WebApplicationContext 实例的 id 设置为用户配置的,否则Spring采用默认的方式进行更改名字。
Spring默认的命名规则为 WebApplicationContext
的全限定类名 + “:” + 项目名。例如:org.springframework.web.context.WebApplicationContext:/mvc_test。
将ServletContext设置给容器
之前我们将容器设置在 ServletContext 的属性中,现在又将 ServletContext 注入进 WebApplicationContext 实例中,有那么一种循环依赖的感觉哈 : )
// 将ServletContext设置给Spring容器
wac.setServletContext(sc);
设置Spring容器的配置文件路径
在初始化 WebApplicationContext 实例的 beanFactory 属性之前,我们首先要从 web.xml 中获取到用户配置的Spring配置文件所在位置。
// 设置Spring容器的配置文件路径
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
获取方式没什么好谈的,但是我们好像记得,如果用户没有配置,默认Spring会去寻找 /WEB-INF/applicationContext.xml 文件。这是在哪里体现的呢?
查看 XmlWebApplicationContext
类中的属性方法,可以看见 getDefaultConfigLocations()
方法获取到的默认配置文件路径就是 /WEB-INF/applicationContext.xml。
public class XmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {
/** Default config location for the root context. */
public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";
/** Default prefix for building a config location for a namespace. */
public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/";
/** Default suffix for building a config location for a namespace. */
public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml";
······
/**
* The default location for the root context is "/WEB-INF/applicationContext.xml",
* and "/WEB-INF/test-servlet.xml" for a context with the namespace "test-servlet"
* (like for a DispatcherServlet instance with the servlet-name "test").
*/
@Override
protected String[] getDefaultConfigLocations() {
if (getNamespace() != null) {
return new String[] {
DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
}
else {
return new String[] {
DEFAULT_CONFIG_LOCATION};
}
}
}
刷新容器
当所有的准备要素都准备好,就可以刷新容器了,调用 refresh()
方法就是我们之前分析的 ApplicationContext 中的 refresh()
方法。
// 调用Spring容器的refresh()方法,加载配置文件
wac.refresh();
我们再看一眼当执行完 refresh()
方法后,WebApplicationContext 实例的内容。
④ 设置到ServletContext
创建完实例后,将其设置到 ServletContext
的 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 属性中,之前我们就是依据这个来进行判断是否已经存在实例的。
// 记录在servletContext中
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
⑤ 将当前的类加载器与实例添加到全局变量
// 映射当前的类加载器与创建的实例到全局变量currentContextPerThread中
currentContextPerThread.put(ccl, this.context);
这样,创建Spring的 WebApplicationContext
就完成了,也就是执行完 initWebApplicationContext()
方法。
III. DispatcherServlet
上一部分中Spring已经实例化了一个 IOC 容器,容器中已经预先初始化完毕一些 bean。
可以看到,已经包含了我们在 spring-config.xml 文件中配置的试图解析器 bean。
Servlet 是一个 Java 编写的程序,此程序是基于 HTTP 协议的,在服务器端运行的 (如Tomcat),是按照 Servlet 规范编写的一个 Java 类。主要是处理客户端的请求并将其结果发送到客户端。Servlet 的生命周期是由 Servlet 的容器来控制的,它可以分为3个阶段:初始化、运行和销毁。
- 初始化。Servlet 容器加载 Servlet 类,把 Servlet 类的 .class 文件中的数据读到内存中。Servlet 容器创建一个
ServletConfig
对象。ServletConfig
对象包含了 Servlet 的初始化配置信息。Servlet 容器创建一个 Servlet 对象。Servlet 容器调用 Servlet 对象的init()
方法进行初始化。 - 运行。当 Servlet 容器接收到一个请求时,Servlet 容器会针对这个请求创建 servletRequest 和 servletResponse对象,然后调用
service()
方法。并把这两个参数传递给service()
方法。service()
方法通过 servletRequest 对象获得请求的信息,并处理该请求。再通过 servletResponse 对象生成这个请求的响应结果。最后销毁 servletRequest 和 servletResponse 对象。我们不管这个请求是 post 提交的还是 get 提交的,最终这个请求都会由service()
方法来处理。 - 销毁阶段。当Web应用被终止时,Servlet 容器会先调用 Servlet 对象的
destrory()
方法,然后再销毁 servlet 对象,同时也会销毁与 servlet 对象相关联的 servletConfig 对象。我们可以在destrory()
方法的实现中,释放 servlet 所占用的资源,如关闭数据库连接,关闭文件输入输出流等。
Servlet 的框架是由两个 Java 包组成:javax.servlet 和 javax.servlet.http。在 javax.servlet 包中定义了所有的 servlet 类都必须实现或扩展的通用接口和类,在 javax.servlet.http 包中定义了采用 HTTP 通信协议的 HttpServlet
类。
Servlet 被设计成请求驱动,Servlet 的请求可能包含多个数据项,当 Web 容器接收到某个 Servlet 请求时,Servlet 把请求封装成一个 HttpServletRequest
对象,然后把对象传给 Servlet 的对应的服务方法。HTTP 的请求方式包括 delete、get、options、post、put 和 trace,在 HttpServlet
类中分别提供了相应的服务方法,它们是 doDelete()
、doGet()
、doOptions()
、doPost()
、doPut()
和 doTrace()
。
关于 Servlet 的相关使用,可以参考 Servlet 教程。
初始化DispatcherServlet
web.xml 文件中我们仅仅配置了一个 Servlet 就是 DispatcherServlet
。所以 Servlet 容器控制着 DispatcherServlet
的生命周期,我们从初始化阶段开始展开分析。
<servlet>
<servlet-name>mvc-test</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--SpringMVC配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
正式分析 DispatcherServlet
之前我们先看一下它的类继承结构。
DispatcherServlet
继承自 FrameworkServlet
,FrameworkServlet
又继承自 HttpServletBean
。我们查看 DispatcherServlet
的初始化方法 init() 方法,其实现是在 HttpServletBean
中的。
/**
* 重写了Servlet的init()方法,DispatcherServlet的init()方法就是这个,DispatcherServlet的生命周期开始
* 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.isTraceEnabled()) {
logger.trace("Initializing servlet '" + getServletName() + "'");
}
// Set bean properties from init parameters.
// 解析web.xml中的init-param并封装至PropertyValues中,其中就包含了SpringMVC的配置文件路径
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
// 将DispatchServlet类实例(this)转化为一个BeanWrapper,从而能够以Spring的方式来对init-param的值进行注入
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
// 注册自定义属性编辑器,一旦遇到Resource类型的属性将会使用ResourceEditor进行解析
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;
}
}
// 留给子类扩展,父类FrameworkServlet重写了
initServletBean();
if (logger.isTraceEnabled()) {
logger.trace("Servlet '" + getServletName() + "' configured successfully");
}
}
DipatcherServlet
的初始化过程主要是通过将当前的 servlet 类型实例转换为 BeanWrapper
类型实例,以便使用Spring中提供的注入功能进行对应属性的注入。这些属性如 contextAttribute、contextClass、nameSpace、contextConfigLocation 等,都可以在 web.xml 文件中以初始化参数的方式配置在 servlet 的声明中,Spring会保证这些参数被注入到对应的值中。
属性注入主要包含以下几个步骤。
① 封装及验证初始化参数
ServletConfigPropertyValues
除了封装属性外还有对属性验证的功能,传入的参数主要是 ServletConfig
实例以及需要验证存在与否的必须的属性。用户可以通过对 requiredProperties 参数的初始化来强制验证某些属性的必要性,这样,在属性封装的过程中,一旦检测到 requiredProperties 中的属性没有指定初始值,就会抛出异常。
/**
* ServletConfigPropertyValues除了封装属性外还有对属性验证的功能
* PropertyValues implementation created from ServletConfig init parameters.
*/
private static class ServletConfigPropertyValues extends MutablePropertyValues {
/**
* 对web.xml中的初始化参数进行封装
* Create new ServletConfigPropertyValues.
* @param config the ServletConfig we'll use to take PropertyValues from
* @param requiredProperties set of property names we need, where
* we can't accept default values
* @throws ServletException if any required properties are missing
*/
public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties)
throws ServletException {
Set<String> missingProps = (!CollectionUtils.isEmpty(requiredProperties) ?
new HashSet<>(requiredProperties) : null);
// 获取初始化参数名称
Enumeration<String> paramNames = config.getInitParameterNames();
while (paramNames.hasMoreElements()) {
String property = paramNames.nextElement();
Object value = config.getInitParameter(property);
addPropertyValue(new PropertyValue(property, value));
if (missingProps != null) {
missingProps.remove(property);
}
}
// 用户可以通过对requiredProperties参数的初始化来强制验证某些属性的必要性,这样,
// 在属性封装的过程中,一旦检测到requiredProperties中的属性没有指定初始值,就会抛出异常。
if (!CollectionUtils.isEmpty(missingProps)) {
throw new ServletException(
"Initialization from ServletConfig for servlet '" + config.getServletName() +
"' failed; the following required properties were missing: " +
StringUtils.collectionToDelimitedString(missingProps, ", "));
}
}
}
从代码中得知,封装属性主要是对初始化的参数进行封装,也就是 servlet 中配置的 <init-param> 中配置的封装。这些初始化参数被 servlet 容器已经封装在了 ServletConfig
实例中,如下图所示显示了Spring MVC的配置文件路径参数。
通过从 ServletConfig
实例中获取出属性值并将其重新封装成 PropertyValue
。
② 用BeanWrapper包裹DispatcherServlet实例
PropertyAccessorFactory.forBeanPropertyAccess()
是Spring中提供的工具方法,主要用于将指定实例转化为Spring中可以处理的 BeanWrapper
类型的实例,方便将上一步包含 init-param 参数信息的 PropertyValue
注入进去。
// 将DispatchServlet类实例(this)转化为一个BeanWrapper,从而能够以Spring的方式来对init-param的值进行注入
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
/**
* Obtain a BeanWrapper for the given target object,
* accessing properties in JavaBeans style.
* @param target the target object to wrap
* @return the property accessor
* @see BeanWrapperImpl
*/
public static BeanWrapper forBeanPropertyAccess(Object target) {
return new BeanWrapperImpl(target);
}
③ 注册解析Resource类型的属性编辑器
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
// 注册自定义属性编辑器,一旦遇到Resource类型的属性将会使用ResourceEditor进行解析
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
关于自定义属性编辑器,我们在分析 ApplicationContext 已经进行相关介绍,可以自行回顾一下。注册 org.springframework.core.io.ResourceEditor
自定义属性编辑器后,凡是遇到 org.springframework.core.io.Resource
类型的属性,将会利用 ResourceEditor
进行解析赋值。
我们查看 ResourceEditor
的 setAsText()
方法。
@Override
public void setAsText(String text) {
if (StringUtils.hasText(text)) {
String locationToUse = resolvePath(text).trim();
setValue(this.resourceLoader.getResource(locationToUse));
}
else {
setValue(null);
}
}
@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
for (ProtocolResolver protocolResolver : this.protocolResolvers) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
if (location.startsWith("/")) {
return getResourceByPath(location);
}
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// Try to parse the location as a URL...
URL url = new URL(location);
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
}
}
其实就是能够将String类型的资源路径,读取返回Spring中对资源统一的封装类型 Resource
。
④ 属性注入
BeanWrapper
为Spring中的方法,支持Spring的自动注入。其实我们最常用的属性注入无非是 contextAttribute、contextClass、nameSpace、contextConfigLocation 等属性。
// 属性注入
bw.setPropertyValues(pvs, true);
⑤ 初始化servletBean
HttpServletBean
中仅仅定义了该方法的模板,而其子类 FrameworkServlet
重写了该方法。
/**
* 覆盖了HttpServletBean的方法
* 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 {
getServletContext().log("Initializing Spring FrameworkServlet '" +