Spring源码解析12——整合SpringMVC(一)

  Spring 的 MVC是基于 Servlet功能实现的,通过实现 Servlet接口的 DispatcherServlet 来封装其核心功能实现,通过将请求分派给处理程序,同时带有可配置的处理程序映射、视图解析本地语言、主题解析以及上载文件支持。默认的处理程序是非常简单的Controller.interface 接口,如下所示:

package org.springframework.web.servlet.mvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.ModelAndView;

@FunctionalInterface
public interface Controller {
    @Nullable
    ModelAndView handleRequest(HttpServletRequest var1, HttpServletResponse var2) throws Exception;
}

Controller.interface 接口的所有实现class的UML图,如下所示:
image

1、SpringMVC快速体验(maven项目)

  • maven的pom.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.xxx</groupId>
  <artifactId>SpringMVC01_start</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>SpringMVC01_start Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <!-- 版本锁定 -->
    <spring.version>5.0.2.RELEASE</spring.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.0</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
  <build>
    <finalName>SpringMVC01_start</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

  • 在/WEB-INF目录下添加web.xml文件
<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <!-- SpringMVC的核心控制器:前端控制器 -->
  <servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <!-- 配置Servlet的初始化参数,读取springmvc的配置文件,创建spring容器 -->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:WEB-INF/applicationContext.xml</param-value>
    </init-param>
    <!-- 配置servlet启动时加载对象 -->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

    <!--配置解决中文乱码的过滤器-->
  <filter>
    <filter-name>charFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <!--设置过滤器中的属性值-->
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <!-- 启动过滤器 -->
    <init-param>
      <param-name>forceEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>charFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!--请求方式转换的 过滤器-->
  <filter>
    <filter-name>hiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>hiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
<!--配置上下文载人器
上下文载人器载入除 Dispatcherservlet 载人的配置文件之外的其他上下文配置文件)
最常用的上下文载人器是一个 Servlet 监听器,其名称为ContextLoaderListener -->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

</web-app>
  • 在/resources/WEB-INF目录(对应web.xml文件中的<param-value>标签中的配置内容)和/webapp/WEB-INF目录下分别添加applicationContext.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

    <!--配置spring创建容器时,要扫描的包-->
    <context:component-scan base-package="com.xxx"></context:component-scan>
    <!-- 配置视图解析器 -->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--扫描/WEB-INF/pages目录下的所有jsp页面-->
        <property name="prefix" value="/WEB-INF/pages/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>

    <!--配置自定义类型转换器-->
    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean class="com.xxx.utils.StringToDateConverter"></bean>
            </set>
        </property>
    </bean>

    <!--配置spring开启注解mvc的支持-->
    <!-- 引用自定义类型转换器 -->
    <mvc:annotation-driven conversion-service="conversionService"></mvc:annotation-driven>

</beans>

目录结构如下图所示:
image

  • 在/WEB-INF/pages/目录(对应applicationContext.xml文件中的InternalResourceViewResolver.class的String prefix变量的值)下添加success.jsp页面
<%--
  Created by IntelliJ IDEA.
  User: xxx
  Date: 2019/1/19
  Time: 11:47
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
    <title>success</title>
</head>
<body>
    <h3>入门案例成功</h3>
    <h3>从request域中取数据的数据如下</h3>
    <%--从request域中取数据--%>
    ${requestScope.username} ${requestScope.password} ${requestScope.age} ${requestScope.address}</br>
    <%--从session域中取数据--%>
    <h3>从sessionScope域中取数据的数据如下</h3>
    ${sessionScope}
</body>
</html>

  • 在/WEB-INF目录下添加index.jsp
<%--
  Created by IntelliJ IDEA.
  User: xxx
  Date: 2019/1/19
  Time: 11:41
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>SpringMvc入门案例</title>
</head>
<body>
    <h3>Springmvc入门</h3>
    <a href="/controller/hello?username='zhangsan'&password='123'">点击跳转</a>
</body>
</html>
  • 在/java/com/xxx目录(对应applicationContext.xml文件中<context:component-scan>标签的base-package属性的值)下添加HelloController.class
package com.xxx.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/controller")
@SessionAttributes(value = {"username", "password"})
public class HelloController {
    @RequestMapping(path = "/hello",method = {RequestMethod.GET},params = {"password","username"},headers = {"Accept"})
    public ModelAndView beginningProgram(String username,String password){
        System.out.println(username);
        System.out.println(password);
        System.out.println("hello SpringMvc 返回ModelAndView");
        ModelMap map = new ModelMap();
        map.addAttribute("username", username);
        map.addAttribute("password", password);
        map.addAttribute("age", "0");
        map.addAttribute("address", "新华街道");
        return new ModelAndView( "success",map);
    }
}
  • 在/java/com/xxx/utils目录(对应applicationContext.xml文件<bean id="conversionService">标签的子标签<set>标签的配置内容)下添加StringToDateConverter.class
package com.xxx.utils;
import org.springframework.core.convert.converter.Converter;
import java.text.SimpleDateFormat;
import java.util.Date;

public class StringToDateConverter implements Converter<String,Date> {
    @Override
    public Date convert(String source) {
        try{
            if (source == null){
                throw new RuntimeException("传入数据为空,请您传入数据");
            }
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
            Date date = dateFormat.parse(source);
            return date;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
}
  • maven打包并部署到Tomcat中,此处往idea中添加一个tomcat服务,配置如下:
    image
    image
    image
    image
    image

  • 测试,启动idea内置的tomcat并用浏览器访问http://localhost:8080/
    image
    点击跳转后,后台控制台打印的内容和浏览器跳转的success.jsp页面分别如下:
    image
    image

2、ContextLoaderListener

  对于 SpringMVC 功能实现的分析,我们首先从web.xml开始,在 web.xm 文件中我们首先配置的就是 ContextLoaderListener.class(在<listener> 标签中),那么它所提供的功能有哪些,又是如何实现的呢?当使用编程方式的时候我们可以直接将 Spring 配置信息作为参数传人Spring 容器中,如:

ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

但是在Web下,我们需要更多的是与Web环境相互结合,通常的办法是将路径以context-param 的方式注册并使用ContextLoaderListener.class 进行监听读取。
ContextLoaderListener.class 的作用就是启动 Web 容器时,自动装配 ApplicationContext 的配置信息。因为它实现了 ServletContextListener.interface 这个接口,在 web.xml配置这个监听器,启动容器时就会默认执行它实现的方法,使用ServletContextListener.interface 接口,开发者能够在为客户端请求提供服务之前向 ServletContext 中添加任意的对象。这个对象在 ServletContext启动的时候被初始化,然后在 ServletContext整个运行期间都是可见的。每一个 Web 应用都有一个 ServletContext与之相关联。ServletContext 对象在应用启动时被创建在应用关闭的时候被销毁。ServletContext在全局范围内有效,类似于应用中的一个全局变量。在 ServletContextListener 中的核心逻辑便是初始化 WebApplicationContext 实例并存放至ServletContext 中。ServletContextListener.interface的UML关系图,如下所示:
image
WebApplicationContext.interface的UML关系图,如下所示:
image

2.1、ServletContextListener的使用

①、创建自定义 ServletContextListener
  首先我们创建 ServletContextListener,目标是在系统启动时添加自定义的属性,以便于在全局范围内可以随时调用。系统启动的时候会调用ServletContextListener.interface实现类的contextInitialized()函数,所以需要在这个方法中实现我们的初始化逻辑。所有修改和新增的代码都是基于上文1、SpringMVC快速体验(maven项目),新增MyDataContextListener.class

package com.xxx.listener;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class MyDataContextListener implements ServletContextListener {
    private ServletContext context = null;

    public MyDataContextListener() {
    }
    //该函数在ServletContext启动之后被调用
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        this.context = servletContextEvent.getServletContext();
        //可以向ServletContext存入key-value格式的数据,之后可以在任意的Servlet或者JSP中获取这个key-value格式的数据
        context.setAttribute("ServletContextListener","通过ServletContextListener初始化的key-value参数或配置" );
    }
    //该函数在ServletContext将要关闭的时候调用
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        this.context = null;
    }
}

②、修改HelloController.class继承ServletContextEvent.class,并新增成员变量ServletContext context,修改后如下所示:

package com.xxx.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;

@Controller
@RequestMapping("/controller")
@SessionAttributes(value = {"username", "password"})
public class HelloController extends ServletContextEvent {
    private ServletContext context = null;
    public HelloController(ServletContext source) {
        super(source);
        this.context = source;
    }

    @RequestMapping(path = "/hello",method = {RequestMethod.GET},params = {"password","username"},headers = {"Accept"})
    public ModelAndView beginningProgram(String username,String password){
        System.out.println(context.getAttribute("ServletContextListener"));
        ModelMap map = new ModelMap();
        map.addAttribute("username", username);
        map.addAttribute("password", password);
        map.addAttribute("age", "0");
        map.addAttribute("address", "新华街道");
        return new ModelAndView( "success",map);
    }
}

③、在web.xml中新增一个<listener>标签,新增后如下所示:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <!-- SpringMVC的核心控制器:前端控制器 -->
  <servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <!-- 配置Servlet的初始化参数,读取springmvc的配置文件,创建spring容器 -->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:WEB-INF/applicationContext.xml</param-value>
    </init-param>
    <!-- 配置servlet启动时加载对象 -->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

    <!--配置解决中文乱码的过滤器-->
  <filter>
    <filter-name>charFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <!--设置过滤器中的属性值-->
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <!-- 启动过滤器 -->
    <init-param>
      <param-name>forceEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>charFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!--请求方式转换的 过滤器-->
  <filter>
    <filter-name>hiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>hiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
<!--配置上下文载人器
上下文载人器载入除 Dispatcherservlet 载人的配置文件之外的其他上下文配置文件)
最常用的上下文载人器是一个 Servlet 监听器,其名称为ContextLoaderListener -->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <!--新增一个自定义的Listener-->
  <listener>
    <listener-class>com.xxx.listener.MyDataContextListener</listener-class>
  </listener>
</web-app>

④、其余不变,用maven打包并部署到Tomcat中后,启动idea内置的tomcat并用浏览器访问http://localhost:8080/, 点击跳转后,后台控制台打印的内容如下:
image

2.2、Spring中的ContextLoaderListener

ServletContext启动之后会调用ServletContextListener.interface::contextInitialized()函数,集体实现在ContextLoaderListener.class::contextInitialized()函数中,这2个的关系参照上文中的UML图。
ContextLoaderListener.class::contextInitialized()

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

这里涉及了一个常用类 WebApplicationContext.class,在Web应用中,我们会用到WebApplicationContext,WebApplicationContext 继承自ApplicationContext,在ApplicationContext 的基础上又追加了一些特定于 Web的操作及属性,非常类似于我们通过编程方式使用 Spring 时使用的ClassPathXmlApplicationContext类提供的功能。继续跟踪代码:
ContextLoader.class::initWebApplicationContext()

    private static final Map<ClassLoader, WebApplicationContext> currentContextPerThread = new ConcurrentHashMap<>(1);
    String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
    
    public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        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!");
        }

        Log logger = LogFactory.getLog(ContextLoader.class);
        servletContext.log("Initializing Spring root WebApplicationContext");
        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) {
                //初始化context
                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.
                        ApplicationContext parent = loadParentContext(servletContext);
                        cwac.setParent(parent);
                    }
                    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.put(ccl, this.context);
            }

            if (logger.isDebugEnabled()) {
                logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
                        WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
            }
            if (logger.isInfoEnabled()) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
            }

            return this.context;
        }
        catch (RuntimeException ex) {
            logger.error("Context initialization failed", ex);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
            throw ex;
        }
        catch (Error err) {
            logger.error("Context initialization failed", err);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
            throw err;
        }
    }

initWebApplicationContext()函数主要是体现了创建 WebApplicationContext 实例的一个功能架构,从函数中我们看到了初始化的大致步骤。

2.2.1、WebApplicationContext存在性的验证

  在配置中只允许声明一次 ServletContextListener,多次声明会扰乱 Spring的执行逻辑,所以这里首先做的就是对此验证,在Spring中如果创建WebApplicationContext 实例会记录在ServletContext 中以方便全局调用,而使用的 key 就是 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,所以验证的方式就是查看 ServletContext 实例中是否有对应 key 的属性。

2.2.2、创建WebApplicationContext实例

  如果通过验证,则Spring将创建WebApplicationContext实例的工作委托给了createWebApplicationContext()函数。
ContextLoader.class::createWebApplicationContext()
ContextLoader.class::determineContextClass()
ContextLoader.class中的静态代码块

    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, ContextLoader.class);
            defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
        }
        catch (IOException ex) {
            throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
        }
    }

    protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
        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);
    }
    
    protected Class<?> determineContextClass(ServletContext servletContext) {
        String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
        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 {
            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);
            }
        }
    }

根据以上静态代码块的内容,我们推断在当前类ContextLoader 同样目录下必定会存在属性文件 ContextLoader.properties,查看后果然存在,内容如下:

org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

综合以上代码分析,在初始化的过程中,程序首先会读取ContextLoader.class的同目录下的属性文件 ContextLoader.properties,并根据其中的配置提取将要实现 WebApplicationContext 接口的实现类,并根据这个实现类通过反射的方式进行实例的创建。

2.2.3.将实例记录在 servletContext中。
2.2.4.映射当前的类加载器与创建的实例到全局变量Map<ClassLoader, WebApplicationContext> currentContextPerThread 中。

3、DispatcherServlet

  在 Spring 中,ContextLoaderListener 只是辅助功能,用于创建 WebApplicationContext 类型实例,而真正的逻辑实现其实是在 DispatcherServlet中进行的,DispatcherServlet 是实现 servlet接口的实现类。
  servlet是一个Java编写的程序,此程序是基于HTTP协议的,在服务器端运行的(如 Tomcat ),是按照 servlet 规范编写的一个Java类。主要是处理客户端的请求并将其结果发送到客户端。servlet的生命周期是由 servlet 的容器来控制的,它可以分为3个阶段:初始化、运行和销毁。

  • servlet初始化阶段
    ①、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对象相关联的servletConfg 对象。我们可以在 destrory()函数的实现中,释放 servlet所占用的资源,如关闭数据库连接,关闭文件输入输出流等。
      servlet 的框架是由2个Java包组成:javax.servlet和javax.servlet.http。在javax.servlet包中定义了所有的 servlet 类都必须实现或扩展的通用接口和类,在javax.servlet.http 包中定义了采用 HTTP 通信协议的 HttpServlet 类。
      servlet 被设计成请求驱动,servlet 的请求可能包含多个数据项,当 Web 容器接收到某个servlet 请求时,servlet 把请求封装成一个 HttpServletRequest 对象(在javax.servlet.http这个jar包中),然后把对象传给 servlet 的对应的服务方法。
      HTTP 的请求方式包括 delete、get、options、post、put和trace,在 HttpServlet.class中分别提供了相应的服务方法,它们是 doDelete()、doGet()、doOptions()、doPost()、doPut()和 doTrace()。
3.1、servlet的使用

①、创建自定义 servlet
  所有修改和新增的代码都是基于上文1、SpringMVC快速体验(maven项目)中的代码,新增MyServlet.class

package com.xxx.servlet;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyServlet extends HttpServlet {
    public void init() {
        System.out.println("this is init method");
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        handleLogic(request, response);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        handleLogic(request, response);
    }

    private void handleLogic(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("handle myLogic");
        ServletContext sc = getServletContext();
        RequestDispatcher rd = null;
        rd = sc.getRequestDispatcher("/myServlet.jsp");
        try {
            rd.forward(request, response);
        } catch (ServletException | IOException e) {
            //定向的页面
            e.printStackTrace();
        }
    }
}

MyServlet.class中包含了对init()函数和get()函数/post()函数的处理,init()函数保证在servlet加载的时候能做一些逻辑操作,而 HttpServlet则会帮助我们根据方法类型的不同而将逻辑引入不同的函数。在子类中我们只需要重写对应的函数逻辑便可,如以上代码重写了doGet()函数和doPost()函数并将逻辑处理部分引导至 handleLogic()函数中,最后,又将页面跳转至myServlet.jsp。
②、在/webapp目录下添加myServlet.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>通过自定义Servlet跳转的jsp页面</title>
</head>
<body>
<h3>通过自定义Servlet跳转的jsp页面</h3>
</body>
</html>

目录结构如下
image

③、在web.xml中新增一个<servlet>标签,新增后如下所示:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <!-- SpringMVC的核心控制器:前端控制器 -->
  <servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <!-- 配置Servlet的初始化参数,读取springmvc的配置文件,创建spring容器 -->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:WEB-INF/applicationContext.xml</param-value>
    </init-param>
    <!-- 配置servlet启动时加载对象 -->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

<!--自定义servlet-->
  <servlet>
    <servlet-name>myservlet</servlet-name>
    <servlet-class>com.xxx.servlet.MyServlet</servlet-class>
    <!-- 配置servlet启动时加载对象 -->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>myservlet</servlet-name>
    <url-pattern>/myservlet</url-pattern>
  </servlet-mapping>

    <!--配置解决中文乱码的过滤器-->
  <filter>
    <filter-name>charFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <!--设置过滤器中的属性值-->
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <!-- 启动过滤器 -->
    <init-param>
      <param-name>forceEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>charFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!--请求方式转换的 过滤器-->
  <filter>
    <filter-name>hiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>hiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
<!--配置上下文载人器
上下文载人器载入除 Dispatcherservlet 载人的配置文件之外的其他上下文配置文件)
最常用的上下文载人器是一个 Servlet 监听器,其名称为ContextLoaderListener -->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <!--新增一个自定义的Listener-->
  <listener>
    <listener-class>com.xxx.listener.MyDataContextListener</listener-class>
  </listener>
</web-app>

④、其余不变,用maven打包并部署到Tomcat中后,启动idea内置的tomcat并用浏览器访问http://localhost:8080/myservlet, 后台控制台打印的内容和通过自定义的servlet跳转的jsp页面分别如下:
image
image

3.2、DispatcherServlet的初始化

  通过上面的实例我们了解到,在 servlet初始化阶段会调用其init()函数,所以我们首先要查看在 DispatcherServlet 中是否重写了 init ()函数。我们在其父类 HttpServletBean 中找到了该函数。DispatcherServlet的UML图,如下所示:
image

abstract HttpServletBean.class::init()

    private final Set<String> requiredProperties = new HashSet<>(4);

    @Override
    public final void init() throws ServletException {
        if (logger.isDebugEnabled()) {
            logger.debug("Initializing servlet '" + getServletName() + "'");
        }

        // Set bean properties from init parameters.
        //解析init-param并封装至pvs中
        PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
        if (!pvs.isEmpty()) {
            try {
                //将当前这个servlet转化为一个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;
            }
        }

        // Let subclasses do whatever initialization they like.
        //留给子类扩展
        initServletBean();

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

  DipatcherServlet 的初始化过程主要是通过将当前的 servlet 类型实例转换为 BeanWrapper 类型实例,以便使用 Spring中提供的注入功能进行对应属性的注人。这些属性如contextAtribute、contextClass、nameSpace、contextConfigLocation 等,都可以在 web.xml 文件中以初始化参数的方式配置在 servlet 的声明中。DispatcherServlet继承自抽象类FrameworkServlet,FrameworkServlet类上包含对应的同名属性,Spring 会保证这些参数被注入到对应的值中。属性注入主要包含以下几个步骤。

3.2.1、封装及验证初始化参数

ServletConfigPropertyValues.class 除了封装属性外还有对属性验证的功能。
abstract HttpServletBean.class::static ServletConfigPropertyValues.class

    private static class ServletConfigPropertyValues extends MutablePropertyValues {

        /**
         * Create new ServletConfigPropertyValues.
         * @param config 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);
                }
            }

            // Fail if we are still missing properties.
            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> 中配置的封装。当然,用户可以通过对 Set<String> requiredProperties 参数的初始化来强制验证某些属性的必要性,这样,在属性封装的过程中,一旦检测到Set<String> requiredProperties 中的属性没有指定初始值,就会抛出异常。

3.2.2、将当前servlet实例转化成BeanWrapper实例

  PropertyAccessorFactory.class::forBeanPropertyAccess()函数是Spring 中提供的工具方法,主要用于将指定实例转化为 Spring 中可以处理的 BeanWrapper 类型的实例。

3.2.3、注册自定义属性编辑器

  属性编辑器,我们在上文中已经介绍并且分析过其原理,这里使用属性编辑器的目的是在对当前实例(DispatcherServlet)属性注人过程中一旦遇到Resource 类型的属性就会使用ResourceEditor 去解析。

3.2.4、属性注入

  BeanWrapper.interface::setPropertyValues()(具体实现在abstract AbstractPropertyAccessor.class中)为Spring-bean 中的类和函数,支持Spring的自动注人。其实我们最常用的属性注人无非是contextAttribute、contextClass、nameSpace、contextConfigLocation 等。BeanWrapper.interface和abstract AbstractPropertyAccessor.class的UML关系图,如下所示:
image

3.2.5、servletBean的初始化

在 ContextLoaderListener 加载的时候已经创建了 WebApplicationContext实例,而在这个函数中最重要的就是对这个实例进行进一步的补充初始化。继续查看 initServletBean()函数。子类FrameworkServlet.class 覆盖了HttpServletBean.class 中的initServletBean()函数
abstract FrameworkServlet.class::initServletBean()

    @Override
    protected final void initServletBean() throws ServletException {
        getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
        if (logger.isInfoEnabled()) {
            logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
        }
        long startTime = System.currentTimeMillis();

        try {
            this.webApplicationContext = initWebApplicationContext();
            //设计为子类覆盖
            initFrameworkServlet();
        }
        catch (ServletException | RuntimeException ex) {
            logger.error("Context initialization failed", ex);
            throw ex;
        }

        if (logger.isInfoEnabled()) {
            long elapsedTime = System.currentTimeMillis() - startTime;
            logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
                    elapsedTime + " ms");
        }
    }

上面的函数设计了计时器来统计初始化的执行时间,而且提供了一个扩展方法initFrameworkServlet()用于子类的覆盖操作,而作为关键的初始化逻辑实现委托给了initWebApplicationContext()函数。

3.3、WebApplicationContext的初始化

  initWebApplicationContext()函数的主要工作就是创建或刷新WebApplicationContext实例并对servlet功能所使用的变量进行初始化。
abstract FrameworkServlet.class::initWebApplicationContext()

    protected WebApplicationContext initWebApplicationContext() {
        WebApplicationContext rootContext =
                WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        WebApplicationContext wac = null;

        if (this.webApplicationContext != null) {
            // A context instance was injected at construction time -> use it
            //this.webApplicationContext实例在构造函数中被注入
            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);
                }
            }
        }
        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
            //根据contextAttribute属性加载WebApplicationContext
            wac = findWebApplicationContext();
        }
        if (wac == null) {
            // No context instance is defined for this servlet -> create a local one
            wac = createWebApplicationContext(rootContext);
        }

        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.
            synchronized (this.onRefreshMonitor) {
                onRefresh(wac);
            }
        }

        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;
    }
3.3.1、寻找或创建对应的 WebApplicationContext实例

WebApplicationContext的寻找及创建包括以下2个步骤。
①、通过构造函数的注人进行初始化
当进人 initWebApplicationContext() 函数后通过判断 this.webApplicationContext != null 后,便可以确定 this.webApplicationContext是否是通过构造函数来初始化的。可是有读者可能会有疑问,在 abstract FrameworkServlet.class::initServletBean() 函数中明明是把创建好的实例记录在了this.webApplicationContext变量中

this.webApplicationContext = initWebApplicationContext();

何以判定这个参数是通过构造函数初始化,而不是通过上一次的函数返回值初始化呢?如果存在这个问题,那么就是读者忽略一个问题了:在Web中包含SpringWeb的核心逻辑的DispatcherServlet只可以被声明为一次,在Spring中已经存在验证,所以这就确保了如果this.webApplicationContext !=null,则可以直接判定 this.webApplicationContext 已经通过构造函数初始化。

②、通过 contextAttribute 进行初始化
  通过在 web.xml 文件中配置的 servlet 参数 contextAttribute 来查找 ServletContext 中对应的属性,默认为 WebApplicationContext.class.getName()+".ROOT",也就是在 ContextLoaderListener加载时会创建 WebApplicationContext.class类型的实例,并将实例以 WebApplicationContext.class.getName()+".ROOT"为key放人 ServletContext 中,当然读者可以重写初始化逻辑使用自己创建的WebApplicationContext,并在 servlet 的配置中通过初始化参数 contextAttribute 指定 key。
abstract FrameworkServlet.class::findWebApplicationContext()

    @Nullable
    protected WebApplicationContext findWebApplicationContext() {
        String attrName = getContextAttribute();
        if (attrName == null) {
            return null;
        }
        WebApplicationContext wac =
                WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
        if (wac == null) {
            throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
        }
        return wac;
    }

③、重新创建 WebApplicationContext 实例
  如果通过以上两种方式并没有找到任何突破,那就没办法了,只能在这里重新创建新的实例了。
abstract FrameworkServlet.class::createWebApplicationContext()

    protected WebApplicationContext createWebApplicationContext(@Nullable WebApplicationContext parent) {
        return createWebApplicationContext((ApplicationContext) parent);
    }

    protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
        //获取servlet的初始化参数contextClass,如果没有配置默认为XmlWebApplicationContext.class
        Class<?> contextClass = getContextClass();
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Servlet with name '" + getServletName() +
                    "' will try to create custom WebApplicationContext context of class '" +
                    contextClass.getName() + "'" + ", using parent context [" + parent + "]");
        }
        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");
        }
        //通过反射方式实例化contextClass
        ConfigurableWebApplicationContext wac =
                (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

        wac.setEnvironment(getEnvironment());
        //parent为在ContextLoaderListener中创建的实例,在ContextLoaderListener加载的时候初始化了WebApplicationContext类型实例
        wac.setParent(parent);
        //获取contextConfigLocation属性,配置在servlet初始化参数中
        String configLocation = getContextConfigLocation();
        if (configLocation != null) {
            wac.setConfigLocation(configLocation);
        }
        //初始化Spring环境包括加载配置文件等
        configureAndRefreshWebApplicationContext(wac);

        return wac;
    }
3.3.2、刷新上下文环境configureAndRefreshWebApplicationContext()函数

  无论是通过构造函数注人还是单独创建,都会调用configureAndRefreshWebApplicationContext()函数来对已经创建的 WebApplicationContext 实例进行配置及刷新,如下
abstract FrameworkServlet.class::configureAndRefreshWebApplicationContext()

    protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
        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
            if (this.contextId != null) {
                wac.setId(this.contextId);
            }
            else {
                // Generate default id...
                wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                        ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
            }
        }

        wac.setServletContext(getServletContext());
        wac.setServletConfig(getServletConfig());
        wac.setNamespace(getNamespace());
        wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

        // 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(getServletContext(), getServletConfig());
        }

        postProcessWebApplicationContext(wac);
        applyInitializers(wac);
        //加载配置文件及整合parent到ConfigurableWebApplicationContext中
        wac.refresh();
    }

无论调用方式如何变化,只要是使用 ApplicationContext.interface类型的实例所提供的功能,最后都免不了使用公共父类 AbstractApplicationContext.class::refresh()函数(在spring-context模块中)进行配置文件加载。
如果要详细了解AbstractApplicationContext.class::refresh(),请查看我的另一篇博客:Spring源码解析7——容器功能扩展

3.3.3、刷新

  onRefresh()函数是FrameworkServlet.class中提供的模板函数,在其子类 DispatcherServlet.class 中进行了重写,主要用于刷新 Spring在 Web功能实现中所必须使用的全局变量。下面我们会介绍它们的初始化过程以及使用场景,而至于具体的使用细节会在稍后的章节中再做详细介绍。
DispatcherServlet.class::initStrategies()

    @Override
    protected void onRefresh(ApplicationContext context) {
        initStrategies(context);
    }

    protected void initStrategies(ApplicationContext context) {
        initMultipartResolver(context);//初始化MultipartResolver
        initLocaleResolver(context);//初始化LocaleResolver
        initThemeResolver(context);//初始化ThemeResolver
        initHandlerMappings(context);//初始化HandlerMappings
        initHandlerAdapters(context);//初始化HandlerAdapters
        initHandlerExceptionResolvers(context);//初始化HandlerExceptionResolvers
        initRequestToViewNameTranslator(context);//初始化RequestToViewNameTranslator
        initViewResolvers(context);//初始化ViewResolvers
        initFlashMapManager(context);//初始化FlashMapManager
    }

①、初始化MultipartResolver
  在 Spring 中,MultipartResolver 主要用来处理文件上传。默认情况下,Spring是没有 multipart处理的,因为一些开发者想要自己处理它们。如果想使用Spring的multipart,则需要在 Web应用的上下文中添加 multipart解析器。这样,每个请求就会被检査是否包含 multipart。然而,如果请求中包含 multipart,那么上下文中定义的 MultipartResolver 就会解析它,这样请求中的multipart 属性就会像其他属性一样被处理。常用配置如下(配置在web.xml文件中的<servlet>标签的子标签<param-value>指定的classpath文件中,比如上文中/WEB-INF/applicationContext.xml文件):

    <!--配置文件解析器对象,单位byte-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="10485760"></property>
    </bean>

MultipartResolver就是在initMultipartResolver()函数中被加入到DispatcherServlet中的,如下:
DispatcherServlet.class::initMultipartResolver()

    private void initMultipartResolver(ApplicationContext context) {
        try {
            this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
            if (logger.isDebugEnabled()) {
                logger.debug("Using MultipartResolver [" + this.multipartResolver + "]");
            }
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Default is no multipart resolver.
            this.multipartResolver = null;
            if (logger.isDebugEnabled()) {
                logger.debug("Unable to locate MultipartResolver with name '" + MULTIPART_RESOLVER_BEAN_NAME +
                        "': no multipart request handling provided");
            }
        }
    }

②、初始化LocaleResolver
  在 Spring 的国际化配置中一共有3种使用方式。

  • 基于 URL 参数的配置。
      通过 URL 参数来控制国际化,比如你在页面上加一句<a href="?locale=zh_CN"> 简体中文来控制项目中使用的国际化参数。而提供这个功能的就是AcceptHeaderLocaleResolver,默认的参数名为locale,注意大小写。里面放的就是你的提交参数,比如en_US、zh_CN之类的具体配置如下;
<bean id="localeResolver" class="org,Springframework.web.servlet.i18n.AcceptHeaderLocaleResolver"/>
  • 基于session的配置。
      它通过检验用户会话中预置的属性来解析区域。最常用的是根据用户本次会话过程中的语言设定决定语言种类(例如,用户登录时选择语言种类,则此次登录周期内统一使用此语言设定 ),如果该会话属性不存在,它会根据 accept-language HTTP 头部确定默认区域。
<bean id-"localeResolver" class="org.Springframework.web.servlet.i18n.SessionlocaleResolver"/>
  • 基于 cookie 的国际化配置。
      CookieLocaleResolver 用于通过浏览器的 cookie 设置取得 Locale 对象。这种策略在应用程序不支持会话或者状态必须保存在客户端时有用,配置如下:
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>

这3种方式都可以解决国际化的问题,但是,对于LocalResolver的使用基础是在DispatcherServlet中的初始化
DispatcherServlet.class::initLocaleResolver()

    private void initLocaleResolver(ApplicationContext context) {
        try {
            this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
            if (logger.isDebugEnabled()) {
                logger.debug("Using LocaleResolver [" + this.localeResolver + "]");
            }
        }
        catch (NoSuchBeanDefinitionException ex) {
            // We need to use the default.
            this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
            if (logger.isDebugEnabled()) {
                logger.debug("Unable to locate LocaleResolver with name '" + LOCALE_RESOLVER_BEAN_NAME +
                        "': using default [" + this.localeResolver + "]");
            }
        }
    }

提取配置文件中设置的LocaleResolver来初始化DispatcherServlet中的localeResolver属性。
③、初始化ThemeResolver
  在 Web 开发中经常会遇到通过主题 Theme 来控制网页风格,这将进一步改善用户体验。简单地说,一个主题就是一组静态资源(比如样式表和图片),它们可以影响应用程序的视觉效果。Spring中的主题功能和国际化功能非常类似,该功能的构成包括3个内容,分别是主题资源、主题解析器和拦截器,具体配置此处省略,但都还是通过ThemeResolver进行加载的。
DispatcherServlet.class::initLocaleResolver()

    private void initThemeResolver(ApplicationContext context) {
        try {
            this.themeResolver = context.getBean(THEME_RESOLVER_BEAN_NAME, ThemeResolver.class);
            if (logger.isDebugEnabled()) {
                logger.debug("Using ThemeResolver [" + this.themeResolver + "]");
            }
        }
        catch (NoSuchBeanDefinitionException ex) {
            // We need to use the default.
            this.themeResolver = getDefaultStrategy(context, ThemeResolver.class);
            if (logger.isDebugEnabled()) {
                logger.debug("Unable to locate ThemeResolver with name '" + THEME_RESOLVER_BEAN_NAME +
                        "': using default [" + this.themeResolver + "]");
            }
        }
    }

④、初始化HandlerMappings
  当客户端发出Request时DispatcherServlet会将Request 提交给 HandlerMapping,然后HanlerMapping 根据 WebApplicationContext 的配置来回传给 DispatcherServlet相应的 Controller。
  在基于 SpringMVC 的 Web应用程序中,我们可以为 DispatcherServlet 提供多个 HandlerMapping 供其使用。DispatcherServlet 在选用 HandlerMapping 的过程中,将根据我们所指定的一系列 HandlerMapping 的优先级进行排序,然后优先使用优先级在前的HandlerMapping。如果当前的 HandlerMapping能够返回可用的Handler,DispatcherServlet则使用当前返回的 Handler进行 Web请求的处理,而不再继续询问其他的 HandlerMapping。否则,DispatcherServlet 将继续按照各个 HandlerMapping 的优先级进行询问,直到获取一个可用的 Handler 为止。初始化配置如下:
DispatcherServlet.class::initHandlerMappings()

    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");
            }
        }
    }

默认情况下,SpringMVC将加载当前系统中所有实现了HandlerMapping接口的bean。如果只期望 SpringMVC 加载指定的 handlermapping 时,可以修改 web.xml 中的 DispatcherServlet的初始参数,将boolean detectAllHandlerMappings的值设置为false:

<init-param>
    <param-name>detectAllHandlerMappings</param-name>
    <param-value>false</param-value>
</init-param>

  此时,SpringMVC将查找名为“handlerMapping”的bean,并作为当前系统中唯一的handlermapping。如果没有定义handlerMapping的话,则SpringMVC 将按照 org.Springframeworkweb.servlet.DispatcherServlet所在目录下的DispatcherServlet.properties中所定义的org.Springframeworkweb.servlet.HandlerMapping 的内容来加载默认的 handlerMapping(用户没有自定义 Strategies的情况下 ),DispatcherServlet.properties中的handlerMapping默认配置如下:

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

⑤、初始化HandlerAdapters
  从名字也能联想到这是一个典型的适配器模式的使用,在计算机编程中,适配器模式将一个类的接口适配成用户所期待的。使用适配器,可以使接口不兼容而无法在一起工作的类协同工作,做法是将类自己的接口包裹在一个已存在的类中。那么在处理handler 时为什么会使用适配器模式呢?回答这个问题我们首先要分析它的初始化逻辑。
DispatcherServlet.class::initHandlerAdapters()

    private void initHandlerAdapters(ApplicationContext context) {
        this.handlerAdapters = null;

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

        // Ensure we have at least some HandlerAdapters, by registering
        // default HandlerAdapters if no other adapters are found.
        if (this.handlerAdapters == null) {
            this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);
            if (logger.isDebugEnabled()) {
                logger.debug("No HandlerAdapters found in servlet '" + getServletName() + "': using default");
            }
        }
    }

同样在初始化的过程中涉及了一个变量boolean detectAllHandlerAdapters,boolean detectAllHandlerAdapters作用和 detectAllHandlerMappings类似,只不过作用对象为 handlerAdapter。亦可通过如下配置来强制系统只加载beanname为“handlerAdapter”的handlerAdapter。配置如下:

<init-param>
    <param-name>detectAllHandlerAdapters</param-name>
    <param-value>false</param-value>
</init-param>

如果无法找到对应的 bean,那么系统会尝试加载默认的handlerAdapter
DispatcherServlet.class::getDefaultStrategies()

    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(
                            "Unresolvable class definition for DispatcherServlet's default strategy class [" +
                            className + "] for interface [" + key + "]", err);
                }
            }
            return strategies;
        }
        else {
            return new LinkedList<>();
        }
    }

在 DispatcherServlet.class中存在这样一段初始化代码块:

    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 '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
        }
    }

在系统加载的时候,Properties defaultStrategies 根据当前路径 DispatcherServlet.properties 来初始化本身,查看 DispatcherServlet.properties 中对应于HandlerAdapter 的配置:

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

由此得知,如果程序开发人员没有在配置文件中定义自己的适配器,那么 Spring 会默认加载配置文件中的3个适配器。
  作为总控制器的派遣器 servlet通过处理器映射得到处理器后,会轮询处理器适配器模块查找能够处理当前 HTTP请求的处理器适配器的实现,处理器适配器模块根据处理器映射返回的处理器类型,例如简单的控制器类型、注解控制器类型或者远程调用处理器类型,来选择某一个适当的处理器适配器的实现,从而适配当前的HTTP请求。

  • HTTP 请求处理器适配器(HttpRequestHandlerAdapter)
    HTTP请求处理器适配器仅仅支持对HTTP请求处理器的适配。它简单地将HTTP请求对象和响应对象传递给 HTTP请求处理器的实现,它并不需要返回值。它主要应用在基于HTTP的远程调用的实现上。
  • 简单控制器处理器适配器(SimpleControllerHandlerAdapter)
    这个实现类将 HTTP请求适配到一个控制器的实现进行处理。这里控制器的实现是一个简单的控制器接口的实现。简单控制器处理器适配器被设计成一个框架类的实现,不需要被改写,客户化的业务逻辑通常是在控制器接口的实现类中实现的。
  • RequestMappingHandlerAdapter

⑥、初始化HandlerExceptionResolvers
  基于HandlerExceptionResolver.interface接口的异常处理,使用这种方式只需要实现resolveException()函数,该函数返回一个ModelAndView对象,在方法内部对异常的类型进行判断,然后尝试生成对应的 ModelAndView 对象,如果该方法返回了 nul,则 Spring 会继续寻找其它的实现了HandlerExceptionResolver.interface 接口的bean。换句话说,Spring 会搜索所有注册在其环境中的实现了HandlerExceptionResolver.interface 接口的 bean,逐个执行,直到返回了一个 ModelAndView 对象。初始化代码如下:
DispatcherServlet.class::initHandlerExceptionResolvers()

    private void initHandlerExceptionResolvers(ApplicationContext context) {
        this.handlerExceptionResolvers = null;

        if (this.detectAllHandlerExceptionResolvers) {
            // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
            Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
                    .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
            if (!matchingBeans.isEmpty()) {
                this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
                // We keep HandlerExceptionResolvers in sorted order.
                AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
            }
        }
        else {
            try {
                HandlerExceptionResolver her =
                        context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
                this.handlerExceptionResolvers = Collections.singletonList(her);
            }
            catch (NoSuchBeanDefinitionException ex) {
                // Ignore, no HandlerExceptionResolver is fine too.
            }
        }

        // Ensure we have at least some HandlerExceptionResolvers, by registering
        // default HandlerExceptionResolvers if no other resolvers are found.
        if (this.handlerExceptionResolvers == null) {
            this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
            if (logger.isDebugEnabled()) {
                logger.debug("No HandlerExceptionResolvers found in servlet '" + getServletName() + "': using default");
            }
        }
    }

⑦、初始化RequestToViewNameTranslator
  当Controller 处理器方法没有返回一个 Vew对象或逻辑视图名称,并且在该方法中没有直接往 response 的输出流里面写数据的时候,Spring就会采用约定好的方式提供一个逻辑视图名称。这个逻辑视图名称是通过 Spring定义的org.springframework.web.servlet.RequestToViewNameTranslator 接口的 getViewName() 函数来实现的,我们可以实现自己的 RequestToViewNameTranslator.interface 接口来约定好没有返回视图名称的时候如何确定视图名称。Spring已经给我们提供了一个它自己的实现,那就是 org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator。
在介绍 DefaultRequestToViewNameTranslator.class 是如何约定视图名称之前,先来看一下它支持用户定义的属性。

  • String prefix:前缀,表示约定好的视图名称需要加上的前缀,默认是空,
  • String suffix:后缀,表示约定好的视图名称需要加上的后缀,默认是空。
  • String separator:分隔符,默认是斜杠“/”。
  • boolean stripLeadingSlash:如果首字符是分隔符,是否要去除,默认是true。
  • boolean stripTrailingSlash:如果最后一个字符是分隔符,是否要去除,默认是true。
  • boolean stripExtension:如果请求路径包含扩展名是否要去除,默认是true。
  • UrlPathHelper urlPathHelper:URL编码和解码相关的配置,其中有一个UrlPathHelper.class::urlDecode变量,默认是 true。它会采用 request 指定的编码或者ISO-8859-1编码对 URL进行解码。

当我们没有在 SpringMVC 的配置文件中手动的定义一个名为viewNameTranlator 的 Bean的时候,Spring 就会为我们提供一个默认的 viewNameTranslator,即 DefaultRequestToViewNameTranslator。
  接下来看一下,当 Controller 处理器方法没有返回逻辑视图名称时,DefaultRequestToViewNameTranslator 是如何约定视图名称的。DefaultRequestToViewNameTranslator 会获取到请求的URI,然后根据提供的属性做一些改造,把改造之后的结果作为视图名称返回。这里以请求路径 htp:/localhost/test/index.html 为例,来说明一下 DefaultRequestToViewNameTranslator是如何工作的。该请求路径对应的请求 URI为/test/index.html,我们来看以下几种情况,它分别对应的逻辑视图名称是什么。

  • prefix 和 suffix 如果都存在,其他为默认值,那么对应返回的逻辑视图名称应该是prefixtest/indexsuffix。
  • stripLeadingSlash和stripExtension都为false,其他默认,这时候对应的逻辑视图名称是/test/index.html.
  • 都采用默认配置时,返回的逻辑视图名称应该是 test/index。

  如果逻辑视图名称跟请求路径相同或者相关关系都是一样的,那么我们就可以采用Spring为我们事先约定好的逻辑视图名称返回,这可以大大简化我们的开发工作,而以上功能实现的关键属性 viewNameTranslator,则是在 initRequestToViewNameTranslator() 函数中完成。
DispatcherServlet.class::initRequestToViewNameTranslator()

    private void initRequestToViewNameTranslator(ApplicationContext context) {
        try {
            this.viewNameTranslator =
                    context.getBean(REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, RequestToViewNameTranslator.class);
            if (logger.isDebugEnabled()) {
                logger.debug("Using RequestToViewNameTranslator [" + this.viewNameTranslator + "]");
            }
        }
        catch (NoSuchBeanDefinitionException ex) {
            // We need to use the default.
            this.viewNameTranslator = getDefaultStrategy(context, RequestToViewNameTranslator.class);
            if (logger.isDebugEnabled()) {
                logger.debug("Unable to locate RequestToViewNameTranslator with name '" +
                        REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME + "': using default [" + this.viewNameTranslator +
                        "]");
            }
        }
    }

⑧、初始化ViewResolvers
  在SpringMVC中,当Controller将请求处理结果放入到ModelAndView中以后,DispatcherServet 会根据 ModelAndView选择合适的视图进行渲染。SpringMVC选择 View 和创建View 对象是在 ViewResolver中。ViewResolver接口定义了 resolverViewName() 函数,根据 viewName 创建合适类型的 View 实现。那么如何配置 ViewResolver 呢?在Spring中,ViewResolver作为 Spring Bean 存在,可以在 Spring 配置文件中进行配置,例如下面的代码,配置了JSP相关的 viewResolver。

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views"/>
    <property name="suffix" value=".jsp"/>
</bean>

viewResolvers属性的初始化工作在initViewResolvers() 函数中完成。
DispatcherServlet.class::initViewResolvers()

    private void initViewResolvers(ApplicationContext context) {
        this.viewResolvers = null;

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

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

⑨、初始化FlashMapManager
  SpringMVC Flash attributes 提供了一个请求存储属性,可供其他请求使用。在使用重定向时候非常必要,例如 Post/Redirect/Get模式。Flash attributes 在重定向之前暂存(就像存在 session中)以便重定向之后还能使用,并立即删除。
  SpringMVC有两个主要的抽象来支持flash attributes。FlashMap用于保持 flash attributes,而 FlashMapManager 用于存储、检索、管理 FlashMap 实例。
  flash attribute 支持默认开启("on")并不需要显式启用,它永远不会导致 HTTP Session 的创建。这2个 FlashMap 实例都可以通过静态方法 RequestContextUtils从Spring MVC 的任何位置访问。
  flashMapManager 的初始化在 initFlashMapManager() 中完成。
DispatcherServlet.class::initFlashMapManager()

    private void initFlashMapManager(ApplicationContext context) {
        try {
            this.flashMapManager = context.getBean(FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class);
            if (logger.isDebugEnabled()) {
                logger.debug("Using FlashMapManager [" + this.flashMapManager + "]");
            }
        }
        catch (NoSuchBeanDefinitionException ex) {
            // We need to use the default.
            this.flashMapManager = getDefaultStrategy(context, FlashMapManager.class);
            if (logger.isDebugEnabled()) {
                logger.debug("Unable to locate FlashMapManager with name '" +
                        FLASH_MAP_MANAGER_BEAN_NAME + "': using default [" + this.flashMapManager + "]");
            }
        }
    }
posted @ 2026-01-12 09:05  Carey_ccl  阅读(2)  评论(0)    收藏  举报