Spring源码——Spring MVC

前言

内容主要参考自《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 的类继承结构。

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() 方法的执行。

contextInitialized

当停止项目时,同样可以看到 contextDestroyed() 方法的执行,同时发现在 contextInitialized() 方法中设置给 ServletContext 的属性成功了。

contextDestroyed

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应用中,我们会用到 WebApplicationContextWebApplicationContext 继承自 ApplicationContext,在 ApplicationContext 的基础上又追加了一些特定于 Web 的操作及属性,非常类似于我们通过编程方式使用Spring时使用的 ClassPathXmlApplicationContext 类提供的功能。我们查看一下 XmWebApplicationContext 的类继承结构,可以发现它和 ClassPathXmlApplicationContext 基本变化不会太大。

XmWebApplicationContext

我们正式进入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,查看后果然存在。

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 实例的 idorg.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

创建完实例后,将其设置到 ServletContextWebApplicationContext.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

父容器初始化的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 容器会针对这个请求创建 servletRequestservletResponse对象,然后调用 service() 方法。并把这两个参数传递给 service() 方法。service() 方法通过 servletRequest 对象获得请求的信息,并处理该请求。再通过 servletResponse 对象生成这个请求的响应结果。最后销毁 servletRequestservletResponse 对象。我们不管这个请求是 post 提交的还是 get 提交的,最终这个请求都会由 service() 方法来处理。
  • 销毁阶段。当Web应用被终止时,Servlet 容器会先调用 Servlet 对象的 destrory() 方法,然后再销毁 servlet 对象,同时也会销毁与 servlet 对象相关联的 servletConfig 对象。我们可以在 destrory() 方法的实现中,释放 servlet 所占用的资源,如关闭数据库连接,关闭文件输入输出流等。

Servlet 的框架是由两个 Java 包组成:javax.servletjavax.servlet.http。在 javax.servlet 包中定义了所有的 servlet 类都必须实现或扩展的通用接口和类,在 javax.servlet.http 包中定义了采用 HTTP 通信协议的 HttpServlet 类。

Servlet 被设计成请求驱动,Servlet 的请求可能包含多个数据项,当 Web 容器接收到某个 Servlet 请求时,Servlet 把请求封装成一个 HttpServletRequest 对象,然后把对象传给 Servlet 的对应的服务方法。HTTP 的请求方式包括 deletegetoptionspostputtrace,在 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

DispatcherServlet 继承自 FrameworkServletFrameworkServlet 又继承自 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中提供的注入功能进行对应属性的注入。这些属性如 contextAttributecontextClassnameSpacecontextConfigLocation 等,都可以在 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

通过从 ServletConfig 实例中获取出属性值并将其重新封装成 PropertyValue

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 进行解析赋值。

我们查看 ResourceEditorsetAsText() 方法。

@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的自动注入。其实我们最常用的属性注入无非是 contextAttributecontextClassnameSpacecontextConfigLocation 等属性。

// 属性注入
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 '" +
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值