Spring专题2:MVC是如何工作的

Spring MVC 的工作原理,分享一篇外文翻译的技术贴。
-英文原文:原文跳转
-译文跳转:译文跳转

在译文基础上,加了一些自己的理解,做了些梳理。

Spring MCN工作流程示意图解析

工作流程图

1.浏览器发送请求到前端控制器(DispatcherServlet),DispatcherServlet请求处理器映射器(HandlerMappering)去查找处理器(Handle):通过xml配置或者注解进行查找

2.HandlerMapping匹配到处理该url请求的Controller、Interceptor(根据xml配置、注解进行查找)返回给DispatcherServlet

3.DispatcherServlet调用Interceptor、Controller进行请求处理,Controller处理结果为ModelAndView返回给DispatcherServlet

4.DispatcherServlet调用ViewResolver渲染ModelAndView为最终的View,最终转为response返回给用户

WEB配置项

以Tomcat为例,web容器中对spring进行的配置有:

  • web.xml,具体配置项解读如下:
<?xml version="1.0" encoding="UTF-8"?>  
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"  
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">  

    <!-- 在Spring框架中是如何解决从页面传来的字符串的编码问题的呢?
    下面我们来看看Spring框架给我们提供过滤器CharacterEncodingFilter  
     这个过滤器就是针对于每次浏览器请求进行过滤的,然后再其之上添加了父类没有的功能即处理字符编码。  
      其中encoding用来设置编码格式,forceEncoding用来设置是否理会 request.getCharacterEncoding()方法,设置为true则强制覆盖之前的编码格式。-->  
    <filter>  
        <filter-name>characterEncodingFilter</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>characterEncodingFilter</filter-name>  
        <url-pattern>/*</url-pattern>  
    </filter-mapping>  
    <!-- 项目中使用Spring 时,applicationContext.xml配置文件中并没有BeanFactory,要想在业务层中的class 文件中直接引用Spring容器管理的bean可通过以下方式-->  
    <!--1、在web.xml配置监听器ContextLoaderListener-->  
    <!--ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。因为它实现了ServletContextListener这个接口,在web.xml配置这个监听器,启动容器时,就会默认执行它实现的方法。  
    在ContextLoaderListener中关联了ContextLoader这个类,所以整个加载配置过程由ContextLoader来完成。  
    它的API说明  
    第一段说明ContextLoader可以由 ContextLoaderListener和ContextLoaderServlet生成。  
    如果查看ContextLoaderServlet的API,可以看到它也关联了ContextLoader这个类而且它实现了HttpServlet这个接口  
    第二段,ContextLoader创建的是 XmlWebApplicationContext这样一个类,它实现的接口是WebApplicationContext->ConfigurableWebApplicationContext->ApplicationContext->  
    BeanFactory这样一来spring中的所有bean都由这个类来创建  
     IUploaddatafileManager uploadmanager = (IUploaddatafileManager)    ContextLoaderListener.getCurrentWebApplicationContext().getBean("uploadManager");
     -->  
    <listener>  
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
    </listener>  
    <!--2、部署applicationContext的xml文件-->  
    <!--如果在web.xml中不写任何参数配置信息,默认的路径是"/WEB-INF/applicationContext.xml,  
    在WEB-INF目录下创建的xml文件的名称必须是applicationContext.xml。  
    如果是要自定义文件名可以在web.xml里加入contextConfigLocation这个context参数:  
    在<param-value> </param-value>里指定相应的xml文件名,如果有多个xml文件,可以写在一起并以“,”号分隔。  
    也可以这样applicationContext-*.xml采用通配符,比如这那个目录下有applicationContext-ibatis-base.xml,  
    applicationContext-action.xml,applicationContext-ibatis-dao.xml等文件,都会一同被载入。  
    在ContextLoaderListener中关联了ContextLoader这个类,所以整个加载配置过程由ContextLoader来完成。-->  
    <context-param>  
        <param-name>contextConfigLocation</param-name>  
        <param-value>classpath:spring/applicationContext.xml</param-value>  
    </context-param>  

    <!--如果你的DispatcherServlet拦截"/",为了实现REST风格,拦截了所有的请求,那么同时对*.js,*.jpg等静态文件的访问也就被拦截了。-->  
    <!--方案一:激活Tomcat的defaultServlet来处理静态文件-->  
    <!--要写在DispatcherServlet的前面, 让 defaultServlet先拦截请求,这样请求就不会进入Spring了,我想性能是最好的吧。-->  
    <servlet-mapping>  
        <servlet-name>default</servlet-name>  
        <url-pattern>*.css</url-pattern>  
    </servlet-mapping>  
    <servlet-mapping>  
        <servlet-name>default</servlet-name>  
        <url-pattern>*.swf</url-pattern>  
    </servlet-mapping>  
    <servlet-mapping>  
        <servlet-name>default</servlet-name>  
        <url-pattern>*.gif</url-pattern>  
    </servlet-mapping>  
    <servlet-mapping>  
        <servlet-name>default</servlet-name>  
        <url-pattern>*.jpg</url-pattern>  
    </servlet-mapping>  
    <servlet-mapping>  
        <servlet-name>default</servlet-name>  
        <url-pattern>*.png</url-pattern>  
    </servlet-mapping>  
    <servlet-mapping>  
        <servlet-name>default</servlet-name>  
        <url-pattern>*.js</url-pattern>  
    </servlet-mapping>  
    <servlet-mapping>  
        <servlet-name>default</servlet-name>  
        <url-pattern>*.html</url-pattern>  
    </servlet-mapping>  
    <servlet-mapping>  
        <servlet-name>default</servlet-name>  
        <url-pattern>*.xml</url-pattern>  
    </servlet-mapping>  
    <servlet-mapping>  
        <servlet-name>default</servlet-name>  
        <url-pattern>*.json</url-pattern>  
    </servlet-mapping>  
    <servlet-mapping>  
        <servlet-name>default</servlet-name>  
        <url-pattern>*.map</url-pattern>  
    </servlet-mapping>  
    <!--使用Spring MVC,配置DispatcherServlet是第一步。DispatcherServlet是一个Servlet,,所以可以配置多个DispatcherServlet-->  
    <!--DispatcherServlet是前置控制器,配置在web.xml文件中的。拦截匹配的请求,Servlet拦截匹配规则要自已定义,把拦截下来的请求,依据某某规则分发到目标Controller(我们写的Action)来处理。-->  
    <servlet>  
        <servlet-name>DispatcherServlet</servlet-name><!--在DispatcherServlet的初始化过程中,框架会在web应用的 WEB-INF文件夹下寻找名为[servlet-name]-servlet.xml 的配置文件,生成文件中定义的bean。-->  
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
        <!--指明了配置文件的文件名,不使用默认配置文件名,而使用dispatcher-servlet.xml配置文件。-->  
        <init-param>  
            <param-name>contextConfigLocation</param-name>  
            <!--其中<param-value>**.xml</param-value> 这里可以使用多种写法-->  
            <!--1、不写,使用默认值:/WEB-INF/<servlet-name>-servlet.xml-->  
            <!--2、<param-value>/WEB-INF/classes/dispatcher-servlet.xml</param-value>-->  
            <!--3、<param-value>classpath*:dispatcher-servlet.xml</param-value>-->  
            <!--4、多个值用逗号分隔-->  
            <param-value>classpath:spring/dispatcher-servlet.xml</param-value>  
        </init-param>  
        <load-on-startup>1</load-on-startup><!--是启动顺序,让这个Servlet随Servletp容器一起启动。-->  
    </servlet>  
    <servlet-mapping>  
        <!--这个Servlet的名字是dispatcher,可以有多个DispatcherServlet,是通过名字来区分的。每一个DispatcherServlet有自己的WebApplicationContext上下文对象。同时保存的ServletContext中和Request对象中.-->  
        <!--ApplicationContext是Spring的核心,Context我们通常解释为上下文环境,我想用“容器”来表述它更容易理解一些,ApplicationContext则是“应用的容器”了:P,Spring把Bean放在这个容器中,在需要的时候,用getBean方法取出-->  
        <servlet-name>DispatcherServlet</servlet-name>  
        <!--Servlet拦截匹配规则可以自已定义,当映射为@RequestMapping("/user/add")时,为例,拦截哪种URL合适?-->  
        <!--1、拦截*.do、*.htm, 例如:/user/add.do,这是最传统的方式,最简单也最实用。不会导致静态文件(jpg,js,css)被拦截。-->  
        <!--2、拦截/,例如:/user/add,可以实现现在很流行的REST风格。很多互联网类型的应用很喜欢这种风格的URL。弊端:会导致静态文件(jpg,js,css)被拦截后不能正常显示。 -->  
        <url-pattern>/</url-pattern> <!--会拦截URL中带“/”的请求。-->  
    </servlet-mapping>  

    <welcome-file-list><!--指定欢迎页面-->  
        <welcome-file>login.html</welcome-file>  
    </welcome-file-list>  
    <error-page> <!--当系统出现404错误,跳转到页面nopage.html-->  
        <error-code>404</error-code>  
        <location>/nopage.html</location>  
    </error-page>  
    <error-page> <!--当系统出现java.lang.NullPointerException,跳转到页面error.html-->  
        <exception-type>java.lang.NullPointerException</exception-type>  
        <location>/error.html</location>  
    </error-page>  
    <session-config><!--会话超时配置,单位分钟-->  
        <session-timeout>360</session-timeout>  
    </session-config>  
</web-app>  
  • applicationContext.xml文件配置
<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"  
    xmlns:tx="http://www.springframework.org/schema/tx" xmlns:p="http://www.springframework.org/schema/p" xmlns:util="http://www.springframework.org/schema/util" xmlns:jdbc="http://www.springframework.org/schema/jdbc"  
    xmlns:cache="http://www.springframework.org/schema/cache"  
    xsi:schemaLocation="  
    http://www.springframework.org/schema/context  
    http://www.springframework.org/schema/context/spring-context.xsd  
    http://www.springframework.org/schema/beans  
    http://www.springframework.org/schema/beans/spring-beans.xsd  
    http://www.springframework.org/schema/tx  
    http://www.springframework.org/schema/tx/spring-tx.xsd  
    http://www.springframework.org/schema/jdbc  
    http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd  
    http://www.springframework.org/schema/cache  
    http://www.springframework.org/schema/cache/spring-cache-3.1.xsd  
    http://www.springframework.org/schema/aop  
    http://www.springframework.org/schema/aop/spring-aop.xsd  
    http://www.springframework.org/schema/util  
    http://www.springframework.org/schema/util/spring-util.xsd"> 

    <!-- 自动扫描web包 ,将带有注解的类 纳入spring容器管理 -->  
    <context:component-scan base-package="com.eduoinfo.finances.bank.web"></context:component-scan>  
  
    <!-- 引入jdbc配置文件 -->  
    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">  
        <property name="locations">  
            <list>  
                <value>classpath*:jdbc.properties</value>  
            </list>  
        </property>  
    </bean>  
  
    <!-- dataSource 配置 -->  
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">  
        <!-- 基本属性 url、user、password -->  
        <property name="url" value="${jdbc.url}" />  
        <property name="username" value="${jdbc.username}" />  
        <property name="password" value="${jdbc.password}" />  
  
        <!-- 配置初始化大小、最小、最大 -->  
        <property name="initialSize" value="1" />  
        <property name="minIdle" value="1" />  
        <property name="maxActive" value="20" />  
  
        <!-- 配置获取连接等待超时的时间 -->  
        <property name="maxWait" value="60000" />  
  
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->  
        <property name="timeBetweenEvictionRunsMillis" value="60000" />  
  
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->  
        <property name="minEvictableIdleTimeMillis" value="300000" />  
  
        <property name="validationQuery" value="SELECT 'x'" />  
        <property name="testWhileIdle" value="true" />  
        <property name="testOnBorrow" value="false" />  
        <property name="testOnReturn" value="false" />  
  
        <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->  
        <property name="poolPreparedStatements" value="false" />  
        <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />  
  
        <!-- 配置监控统计拦截的filters -->  
        <property name="filters" value="stat" />  
    </bean>  
  
    <!-- mybatis文件配置,扫描所有mapper文件 -->  
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:dataSource-ref="dataSource" p:configLocation="classpath:mybatis-config.xml" p:mapperLocations="classpath:com/eduoinfo/finances/bank/web/dao/*.xml" />  
  
    <!-- spring与mybatis整合配置,扫描所有dao -->  
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" p:basePackage="com.eduoinfo.finances.bank.web.dao" p:sqlSessionFactoryBeanName="sqlSessionFactory" />  
  
    <!-- 对dataSource 数据源进行事务管理 -->  
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource" />  
  
    <!-- 配置使Spring采用CGLIB代理 -->  
    <aop:aspectj-autoproxy proxy-target-class="true" />  
  
    <!-- 启用对事务注解的支持 -->  
    <tx:annotation-driven transaction-manager="transactionManager" />  
  
    <!-- Cache配置 -->  
    <cache:annotation-driven cache-manager="cacheManager" />  
    <bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:configLocation="classpath:ehcache.xml" />  
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cacheManager-ref="ehCacheManagerFactory" />  
  
</beans>  

关于Spring Bean

备注,加载applicationContext时,使用到了Bean的一些逻辑,这里简单介绍下spring bean的工作机制。

在Spring中,所有管理的对象都是JavaBean对象,而BeanFactory和ApplicationContext就是spring框架的两个IOC容器,现在一般使用ApplicationnContext,其不但包含了BeanFactory的作用,同时还进行更多的扩展。

Spring和Bean的关系

bean配置有三种方法:

  • 基于xml配置Bean
  • 使用注解定义Bean
  • 基于java类提供Bean定义信息

这里主要介绍第一种,基于xml的Bean配置。

关键字:Bean id、Bean类名、property.

一般情况下,Spring IOC容器中的一个Bean即对应配置文件中的一个<bean>,这种镜像对应关系应该容易理解。其中id为这个Bean的名称,通过容器的getBean("foo")即可获取对应的Bean,在容器中起到定位查找的作用,是外部程序和Spring IOC容器进行交互的桥梁。class属性指定了Bean对应的实现类。

    <!-- 引入jdbc配置文件 -->  
    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">  
        <property name="locations">  
            <list>  
                <value>classpath*:jdbc.properties</value>  
            </list>  
        </property>  
    </bean> 
使用注解配置信息启动spring容器

Spring提供了一个context的命名空间,它提供了通过扫描类包以应用注解定义Bean的方式:

<?xml version="1.0" encoding="UTF-8" ?>
<!--①声明context的命名空间-->
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context-3.0.xsd"
         >
    <!--②扫描类包以应用注解定义的Bean-->
   <context:component-scan base-package="com.baobaotao.anno"/>
   <bean class="com.baobaotao.anno.LogonService"></bean>
   <!-- context:component-scan base-package="com.baobaotao" resource-pattern="anno/*.class"/ -->
   <!-- context:component-scan base-package="com.baobaotao">
       <context:include-filter type="regex" expression="com\.baobaotao\.anno.*Dao"/>
       <context:include-filter type="regex" expression="com\.baobaotao\.anno.*Service"/>
       <context:exclude-filter type="aspectj" expression="com.baobaotao..*Controller+"/>
   </context:component-scan -->
</beans>
Bean注入

Bean注入的方式有两种:

项目安装

在本文中,我们将使用最新、最好的Spring Framework 5。我们将重点介绍Spring的经典Web堆栈,该堆栈从框架的第一个版本中就崭露头角,并且现在依然是用Spring构建Web应用程序的主要方式。

对于初学者来说,为了安装测试项目,最好使用Spring Boot和一些初学者依赖项;还需要定义parent:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.M5</version>
    <relativePath/>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
</dependencies>

请注意,为了使用Spring 5,我们还需要使用Spring Boot 2.x。截止到撰写本文之时,这依然是里程碑发布版,可在Spring Milestone Repository中找到。让我们把这个存储库添加到你的Maven项目中:

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

你可以在Maven Central上查看Spring Boot的当前版本。

示例项目

为了理解Spring Web MVC是如何工作的,我们将通过一个登录页面实现一个简单的应用程序。为了显示登录页面,我们需要为上下文根创建带有GET映射的@Controller注解类InternalController。

hello()方法是无参数的。它返回一个由Spring MVC解释为视图名称的String(在示例中是login.html模板):

import org.springframework.web.bind.annotation.GetMapping;
@GetMapping("/")
public String hello() {
    return "login";
}

为了处理用户登录,需要创建另一个用登录数据处理POST请求的方法。然后根据结果将用户重定向到成功或失败的页面。

请注意,login()方法接收域对象作为参数并返回ModelAndView对象:

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;
@PostMapping("/login")
public ModelAndView login(LoginData loginData) {
    if (LOGIN.equals(loginData.getLogin()) 
      && PASSWORD.equals(loginData.getPassword())) {
        return new ModelAndView("success", 
          Collections.singletonMap("login", loginData.getLogin()));
    } else {
        return new ModelAndView("failure", 
          Collections.singletonMap("login", loginData.getLogin()));
    }
}

ModelAndView是两个不同对象的持有者:

  • Model——渲染页面数据的键值映射
  • View——填充模型数据的页面模板

连接这些是为了方便,这样控制器方法可以一次返回它们。

要渲染HTML页面,使用Thymeleaf作为视图模板引擎,该引擎具有可靠和开箱即用的与Spring的集成。

Servlet作为Java Web应用程序的基础

那么,当在浏览器中输入http:// localhost:8080/时,按Enter键,然后请求到达Web服务器,实际发生了什么?你如何从这个请求中看到浏览器中的Web表单?

鉴于该项目是一个简单的Spring Boot应用程序,因此可以通过Spring5Application运行它。

Spring Boot默认使用Apache Tomcat。因此,运行应用程序时,你可能会在日志中看到以下信息:

2017-10-16 20:36:11.626  INFO 57414 --- [main] 
  o.s.b.w.embedded.tomcat.TomcatWebServer  : 
  Tomcat initialized with port(s): 8080 (http)
2017-10-16 20:36:11.634  INFO 57414 --- [main] 
  o.apache.catalina.core.StandardService   : 
  Starting service [Tomcat]
2017-10-16 20:36:11.635  INFO 57414 --- [main] 
  org.apache.catalina.core.StandardEngine  : 
  Starting Servlet Engine: Apache Tomcat/8.5.23

由于Tomcat是一个Servlet容器,因此发送给Tomcat Web服务器的每个HTTP请求自然都由Java servlet处理。所以Spring Web应用程序入口点是一个servlet,这并不奇怪。

简单地说,servlet就是任何Java Web应用程序的核心组件;它是低层次的,不会像MVC那样在特定的编程模式中诸多要求。

一个HTTP servlet只能接收一个HTTP请求,以某种方式处理,然后发回一个响应。

而且,从Servlet 3.0 API开始,你现在可以超越XML配置,并开始利用Java配置(只有很小的限制条件)。

DispatcherServlet作为Spring MVC的核心

作为一个Web应用程序的开发人员,我们真正想要做的是抽象出以下繁琐和模板化的任务,并专注于有用的业务逻辑:

  • 将HTTP请求映射到某个处理方法

  • 将HTTP请求数据和标题解析成数据传输对象(DTO)或域对象

  • 模型 – 视图 – 控制器集成

  • 从DTO、域对象等生成响应

Spring DispatcherServlet能够提供这些。它是Spring Web MVC框架的核心;此核心组件接收所有请求到应用程序。

正如你所看到的,DispatcherServlet是非常可扩展的。例如,它允许你插入不同的现有或新的适配器进行大量的任务:

  • 将请求映射到应该处理它的类或方法(HandlerMapping接口的实现)

  • 使用特定模式处理请求,如常规servlet,更复杂的MVC工作流,或POJO bean中的方法(HandlerAdapter接口的实现)

  • 按名称解析视图,允许你使用不同的模板引擎,XML,XSLT或任何其他视图技术(ViewResolver接口的实现)

  • 通过使用默认的Apache Commons文件上传实现或编写你自己的MultipartResolver来解析多部分请求

  • 使用任何LocaleResolver实现解决语言环境,包括cookie,会话,Accept HTTP头,或任何其他确定用户所期望的语言环境的方式

处理HTTP请求

首先,我们将简单的HTTP请求的处理追踪到在控制器层中的一个方法,然后返回到浏览器/客户端。

DispatcherServlet具有很长的继承层次结构;自上而下地逐个理解这些是有价值的。请求处理方法最让我们感兴趣。

示意图

理解HTTP请求,无论是在本地还是远程的标准开发中,都是理解MVC体系结构的关键部分。

GenericServlet

GenericServlet是Servlet规范的一部分,不直接关注HTTP。它定义了接收传入请求并产生响应的service()方法。

注意,ServletRequest和ServletResponse方法参数如何与HTTP协议无关:

public abstract void service(ServletRequest req, ServletResponse res) 
  throws ServletException, IOException;

这是最终被任何请求调用到服务器上的方法,包括简单的GET请求。

HttpServlet

顾名思义,HttpServlet类就是规范中定义的基于HTTP的Servlet实现。

更实际的说,HttpServlet是一个抽象类,有一个service()方法实现,service()方法实现通过HTTP方法类型分割请求,大致如下所示:

protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {
    String method = req.getMethod();
    if (method.equals(METHOD_GET)) {
        // ...
        doGet(req, resp);
    } else if (method.equals(METHOD_HEAD)) {
        // ...
        doHead(req, resp);
    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);
        // ...
    }

HttpServletBean

接下来,HttpServletBean是层次结构中第一个Spring-aware类。它使用从web.xml或WebApplicationInitializer接收到的servlet init-param值来注入bean的属性。

在请求应用程序的情况下,doGet(),doPost()等方法应特定的HTTP请求而调用。

FrameworkServlet

FrameworkServlet集成Servlet功能与Web应用程序上下文,实现了ApplicationContextAware接口。但它也能够自行创建Web应用程序上下文。

正如你已经看到的,HttpServletBean超类注入init-params为bean属性。所以,如果在servlet的contextClass init-param中提供了一个上下文类名,那么这个类的一个实例将被创建为应用程序上下文。否则,将使用默认的XmlWebApplicationContext类。

由于XML配置现在已经过时,Spring Boot默认使用AnnotationConfigWebApplicationContext配置DispatcherServlet。但是你可以轻松更改。

例如,如果你需要使用基于Groovy的应用程序上下文来配置Spring Web MVC应用程序,则可以在web.xml文件中使用以下DispatcherServlet配置:

dispatcherServlet
        org.springframework.web.servlet.DispatcherServlet
        contextClass
        org.springframework.web.context.support.GroovyWebApplicationContext

使用WebApplicationInitializer类,可以用更现代的基于Java的方式来完成相同的配置。

DispatcherServlet:统一请求处理

HttpServlet.service()实现,会根据HTTP动词的类型来路由请求,这在低级servlet的上下文中是非常有意义的。然而,在Spring MVC的抽象级别,方法类型只是可以用来映射请求到其处理程序的参数之一。

因此,FrameworkServlet类的另一个主要功能是将处理逻辑重新加入到单个processRequest()方法中,processRequest()方法反过来又调用doService()方法:

@Override
protected final void doGet(HttpServletRequest request, 
  HttpServletResponse response) throws ServletException, IOException {
    processRequest(request, response);
}
@Override
protected final void doPost(HttpServletRequest request, 
  HttpServletResponse response) throws ServletException, IOException {
    processRequest(request, response);
}
// …

DispatcherServlet:丰富请求

最后,DispatcherServlet实现doService()方法。在这里,它增加了一些可能会派上用场的有用对象到请求:Web应用程序上下文,区域解析器,主题解析器,主题源等:

request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, 
  getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

另外,doService()方法准备输入和输出Flash映射。Flash映射基本上是一种模式,该模式将参数从一个请求传递到另一个紧跟的请求。这在重定向期间可能非常有用(例如在重定向之后向用户显示一次性信息消息):

FlashMap inputFlashMap = this.flashMapManager
  .retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
    request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, 
      Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());

然后,doService()方法调用负责请求调度的doDispatch()方法。

DispatcherServlet:调度请求

dispatch()方法的主要目的是为请求找到合适的处理程序,并为其提供请求/响应参数。处理程序基本上是任何类型的object,不限于特定的接口。这也意味着Spring需要为此处理程序找到适配器,该处理程序知道如何与处理程序“交谈”。

为了找到匹配请求的处理程序,Spring检查HandlerMapping接口的注册实现。有很多不同的实现可以满足你的需求。

SimpleUrlHandlerMapping允许通过URL将请求映射到某个处理bean。例如,可以通过使用java.util.Properties实例注入其mappings属性来配置,就像这样:

/welcome.html=ticketController
/show.html=ticketController

可能处理程序映射最广泛使用的类是RequestMappingHandlerMapping,它将请求映射到@Controller类的@ RequestMapping注释方法。这正是使用控制器的hello()和login()方法连接调度程序的映射。

请注意,Spring-aware方法使用@GetMapping和@PostMapping进行注释。这些注释依次用@RequestMapping元注释标记。

dispatch()方法还负责其他一些HTTP特定任务:

  • 在资源未被修改的情况下,GET请求的短路处理
  • 针对相应的请求应用多部分解析器
  • 如果处理程序选择异步处理该请求,则会短路处理该请求

处理请求

现在Spring已经确定了请求的处理程序和处理程序的适配器,是时候来处理请求了。下面是HandlerAdapter.handle()方法的签名。请注意,处理程序可以选择如何处理请求:

  • 自主地编写数据到响应对象,并返回null
  • 返回由DispatcherServlet呈现的ModelAndView对象
@Nullable
ModelAndView handle(HttpServletRequest request, 
                    HttpServletResponse response, 
                    Object handler) throws Exception;

有几种提供的处理程序类型。以下是SimpleControllerHandlerAdapter如何处理Spring MVC控制器实例(不要将其与@ Controller注释POJO混淆)。

注意控制器处理程序如何返回ModelAndView对象,并且不自行呈现视图:

public ModelAndView handle(HttpServletRequest request, 
  HttpServletResponse response, Object handler) throws Exception {
    return ((Controller) handler).handleRequest(request, response);
}

第二个是SimpleServletHandlerAdapter,它将常规的Servlet作为请求处理器。

Servlet不知道任何有关ModelAndView的内容,只是简单地自行处理请求,并将结果呈现给响应对象。所以这个适配器只是返回null而不是ModelAndView:

public ModelAndView handle(HttpServletRequest request, 
  HttpServletResponse response, Object handler) throws Exception {
    ((Servlet) handler).service(request, response);
    return null;
}

我们碰到的情况是,控制器是有若干@RequestMapping注释的POJO,所以任何处理程序基本上是包装在HandlerMethod实例中的这个类的方法。为了适应这个处理器类型,Spring使用RequestMappingHandlerAdapter类。

处理参数和返回处理程序方法的值

注意,控制器方法通常不会使用HttpServletRequest和HttpServletResponse,而是接收和返回许多不同类型的数据,例如域对象,路径参数等。

此外,要注意,我们不需要从控制器方法返回ModelAndView实例。可能会返回视图名称,或ResponseEntity,或将被转换为JSON响应等的POJO。

RequestMappingHandlerAdapter确保方法的参数从HttpServletRequest中解析出来。另外,它从方法的返回值中创建ModelAndView对象。

在RequestMappingHandlerAdapter中有一段重要的代码,可确保所有这些转换魔法的发生:

ServletInvocableHandlerMethod invocableMethod 
  = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
    invocableMethod.setHandlerMethodArgumentResolvers(
      this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
    invocableMethod.setHandlerMethodReturnValueHandlers(
      this.returnValueHandlers);
}
argumentResolvers对象是不同的HandlerMethodArgumentResolver实例的组合。

有超过30个不同的参数解析器实现。它们允许从请求中提取任何类型的信息,并将其作为方法参数提供。这包括URL路径变量,请求主体参数,请求标头,cookies,会话数据等。

returnValueHandlers对象是HandlerMethodReturnValueHandler对象的组合。还有很多不同的值处理程序可以处理方法的结果来创建适配器所期望的ModelAndViewobject。

例如,当你从hello()方法返回字符串时,ViewNameMethodReturnValueHandler处理这个值。但是,当你从login()方法返回一个准备好的ModelAndView时,Spring会使用ModelAndViewMethodReturnValueHandler。

渲染视图

到目前为止,Spring已经处理了HTTP请求并接收了ModelAndView对象,所以它必须呈现用户将在浏览器中看到的HTML页面。它基于模型和封装在ModelAndView对象中的选定视图来完成。

另外请注意,我们可以呈现JSON对象,或XML,或任何可通过HTTP协议传输的其他数据格式。我们将在即将到来的REST-focused部分接触更多。

让我们回到DispatcherServlet。render()方法首先使用提供的LocaleResolver实例设置响应语言环境。假设现代浏览器正确设置了Accept头,并且默认使用AcceptHeaderLocaleResolver。

在渲染过程中,ModelAndView对象可能已经包含对所选视图的引用,或者只是一个视图名称,或者如果控制器依赖于默认视图,则什么都没有。

由于hello()和login()方法两者都指定所需的视图为String名称,因此必须用该名称查找。所以,这是viewResolvers列表开始起作用的地方:

for (ViewResolver viewResolver : this.viewResolvers) {
    View view = viewResolver.resolveViewName(viewName, locale);
    if (view != null) {
        return view;
    }
}

这是一个ViewResolver实例列表,包括由thymeleaf-spring5集成库提供的ThymeleafViewResolver。该解析器知道在哪里搜索视图,并提供相应的视图实例。

在调用视图的render()方法后,Spring最终通过发送HTML页面到用户的浏览器来完成请求处理。

REST支持

除了典型的MVC场景之外,我们还可以使用框架来创建REST Web服务。

简而言之,我们可以接受Resource作为输入,指定POJO作为方法参数,并使用@RequestBody对其进行注释。也可以使用@ResponseBody注释方法本身,以指定其结果必须直接转换为HTTP响应:

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
@ResponseBody
@PostMapping("/message")
public MyOutputResource sendMessage(
  @RequestBody MyInputResource inputResource) {
    return new MyOutputResource("Received: "
      + inputResource.getRequestMessage());
}

归功于Spring MVC的可扩展性,这也是可行的。

为了将内部DTO编组为REST表示,框架使用HttpMessageConverter基础结构。例如,其中一个实现是MappingJackson2HttpMessageConverter,它可以使用Jackson库将模型对象转换为JSON或从JSON转换。

为了进一步简化REST API的创建,Spring引入了@RestController注解。默认情况下,这很方便地假定了@ResponseBody语义,并避免在每个REST控制器上的明确设置:

import org.springframework.web.bind.annotation.RestController;
@RestController
public class RestfulWebServiceController {
    @GetMapping("/message")
    public MyOutputResource getMessage() {
        return new MyOutputResource("Hello!");
    }
}

结论

在这篇文章中,我们详细了介绍在Spring MVC框架中请求的处理过程。了解框架的不同扩展是如何协同工作来提供所有魔法的,可以让你能够事倍功半地处理HTTP协议难题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容