Spring專題2:MVC是如何工作的

Spring MVC 的工作原理,分享一篇外文翻譯的技術(shù)貼。
-英文原文:原文跳轉(zhuǎn)
-譯文跳轉(zhuǎn):譯文跳轉(zhuǎn)

在譯文基礎(chǔ)上,加了一些自己的理解,做了些梳理。

Spring MCN工作流程示意圖解析

工作流程圖

1.瀏覽器發(fā)送請(qǐng)求到前端控制器(DispatcherServlet),DispatcherServlet請(qǐng)求處理器映射器(HandlerMappering)去查找處理器(Handle):通過xml配置或者注解進(jìn)行查找

2.HandlerMapping匹配到處理該url請(qǐng)求的Controller、Interceptor(根據(jù)xml配置、注解進(jìn)行查找)返回給DispatcherServlet

3.DispatcherServlet調(diào)用Interceptor、Controller進(jìn)行請(qǐng)求處理,Controller處理結(jié)果為ModelAndView返回給DispatcherServlet

4.DispatcherServlet調(diào)用ViewResolver渲染ModelAndView為最終的View,最終轉(zhuǎn)為response返回給用戶

WEB配置項(xiàng)

以Tomcat為例,web容器中對(duì)spring進(jìn)行的配置有:

  • web.xml,具體配置項(xiàng)解讀如下:
<?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框架中是如何解決從頁面?zhèn)鱽淼淖址木幋a問題的呢?
    下面我們來看看Spring框架給我們提供過濾器CharacterEncodingFilter  
     這個(gè)過濾器就是針對(duì)于每次瀏覽器請(qǐng)求進(jìn)行過濾的,然后再其之上添加了父類沒有的功能即處理字符編碼。  
      其中encoding用來設(shè)置編碼格式,forceEncoding用來設(shè)置是否理會(huì) request.getCharacterEncoding()方法,設(shè)置為true則強(qiáng)制覆蓋之前的編碼格式。-->  
    <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>  
    <!-- 項(xiàng)目中使用Spring 時(shí),applicationContext.xml配置文件中并沒有BeanFactory,要想在業(yè)務(wù)層中的class 文件中直接引用Spring容器管理的bean可通過以下方式-->  
    <!--1、在web.xml配置監(jiān)聽器ContextLoaderListener-->  
    <!--ContextLoaderListener的作用就是啟動(dòng)Web容器時(shí),自動(dòng)裝配ApplicationContext的配置信息。因?yàn)樗鼘?shí)現(xiàn)了ServletContextListener這個(gè)接口,在web.xml配置這個(gè)監(jiān)聽器,啟動(dòng)容器時(shí),就會(huì)默認(rèn)執(zhí)行它實(shí)現(xiàn)的方法。  
    在ContextLoaderListener中關(guān)聯(lián)了ContextLoader這個(gè)類,所以整個(gè)加載配置過程由ContextLoader來完成。  
    它的API說明  
    第一段說明ContextLoader可以由 ContextLoaderListener和ContextLoaderServlet生成。  
    如果查看ContextLoaderServlet的API,可以看到它也關(guān)聯(lián)了ContextLoader這個(gè)類而且它實(shí)現(xiàn)了HttpServlet這個(gè)接口  
    第二段,ContextLoader創(chuàng)建的是 XmlWebApplicationContext這樣一個(gè)類,它實(shí)現(xiàn)的接口是WebApplicationContext->ConfigurableWebApplicationContext->ApplicationContext->  
    BeanFactory這樣一來spring中的所有bean都由這個(gè)類來創(chuàng)建  
     IUploaddatafileManager uploadmanager = (IUploaddatafileManager)    ContextLoaderListener.getCurrentWebApplicationContext().getBean("uploadManager");
     -->  
    <listener>  
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
    </listener>  
    <!--2、部署applicationContext的xml文件-->  
    <!--如果在web.xml中不寫任何參數(shù)配置信息,默認(rèn)的路徑是"/WEB-INF/applicationContext.xml,  
    在WEB-INF目錄下創(chuàng)建的xml文件的名稱必須是applicationContext.xml。  
    如果是要自定義文件名可以在web.xml里加入contextConfigLocation這個(gè)context參數(shù):  
    在<param-value> </param-value>里指定相應(yīng)的xml文件名,如果有多個(gè)xml文件,可以寫在一起并以“,”號(hào)分隔。  
    也可以這樣applicationContext-*.xml采用通配符,比如這那個(gè)目錄下有applicationContext-ibatis-base.xml,  
    applicationContext-action.xml,applicationContext-ibatis-dao.xml等文件,都會(huì)一同被載入。  
    在ContextLoaderListener中關(guān)聯(lián)了ContextLoader這個(gè)類,所以整個(gè)加載配置過程由ContextLoader來完成。-->  
    <context-param>  
        <param-name>contextConfigLocation</param-name>  
        <param-value>classpath:spring/applicationContext.xml</param-value>  
    </context-param>  

    <!--如果你的DispatcherServlet攔截"/",為了實(shí)現(xiàn)REST風(fēng)格,攔截了所有的請(qǐng)求,那么同時(shí)對(duì)*.js,*.jpg等靜態(tài)文件的訪問也就被攔截了。-->  
    <!--方案一:激活Tomcat的defaultServlet來處理靜態(tài)文件-->  
    <!--要寫在DispatcherServlet的前面, 讓 defaultServlet先攔截請(qǐng)求,這樣請(qǐng)求就不會(huì)進(jìn)入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是一個(gè)Servlet,,所以可以配置多個(gè)DispatcherServlet-->  
    <!--DispatcherServlet是前置控制器,配置在web.xml文件中的。攔截匹配的請(qǐng)求,Servlet攔截匹配規(guī)則要自已定義,把攔截下來的請(qǐng)求,依據(jù)某某規(guī)則分發(fā)到目標(biāo)Controller(我們寫的Action)來處理。-->  
    <servlet>  
        <servlet-name>DispatcherServlet</servlet-name><!--在DispatcherServlet的初始化過程中,框架會(huì)在web應(yīng)用的 WEB-INF文件夾下尋找名為[servlet-name]-servlet.xml 的配置文件,生成文件中定義的bean。-->  
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
        <!--指明了配置文件的文件名,不使用默認(rèn)配置文件名,而使用dispatcher-servlet.xml配置文件。-->  
        <init-param>  
            <param-name>contextConfigLocation</param-name>  
            <!--其中<param-value>**.xml</param-value> 這里可以使用多種寫法-->  
            <!--1、不寫,使用默認(rèn)值:/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、多個(gè)值用逗號(hào)分隔-->  
            <param-value>classpath:spring/dispatcher-servlet.xml</param-value>  
        </init-param>  
        <load-on-startup>1</load-on-startup><!--是啟動(dòng)順序,讓這個(gè)Servlet隨Servletp容器一起啟動(dòng)。-->  
    </servlet>  
    <servlet-mapping>  
        <!--這個(gè)Servlet的名字是dispatcher,可以有多個(gè)DispatcherServlet,是通過名字來區(qū)分的。每一個(gè)DispatcherServlet有自己的WebApplicationContext上下文對(duì)象。同時(shí)保存的ServletContext中和Request對(duì)象中.-->  
        <!--ApplicationContext是Spring的核心,Context我們通常解釋為上下文環(huán)境,我想用“容器”來表述它更容易理解一些,ApplicationContext則是“應(yīng)用的容器”了:P,Spring把Bean放在這個(gè)容器中,在需要的時(shí)候,用getBean方法取出-->  
        <servlet-name>DispatcherServlet</servlet-name>  
        <!--Servlet攔截匹配規(guī)則可以自已定義,當(dāng)映射為@RequestMapping("/user/add")時(shí),為例,攔截哪種URL合適?-->  
        <!--1、攔截*.do、*.htm, 例如:/user/add.do,這是最傳統(tǒng)的方式,最簡(jiǎn)單也最實(shí)用。不會(huì)導(dǎo)致靜態(tài)文件(jpg,js,css)被攔截。-->  
        <!--2、攔截/,例如:/user/add,可以實(shí)現(xiàn)現(xiàn)在很流行的REST風(fēng)格。很多互聯(lián)網(wǎng)類型的應(yīng)用很喜歡這種風(fēng)格的URL。弊端:會(huì)導(dǎo)致靜態(tài)文件(jpg,js,css)被攔截后不能正常顯示。 -->  
        <url-pattern>/</url-pattern> <!--會(huì)攔截URL中帶“/”的請(qǐng)求。-->  
    </servlet-mapping>  

    <welcome-file-list><!--指定歡迎頁面-->  
        <welcome-file>login.html</welcome-file>  
    </welcome-file-list>  
    <error-page> <!--當(dāng)系統(tǒng)出現(xiàn)404錯(cuò)誤,跳轉(zhuǎn)到頁面nopage.html-->  
        <error-code>404</error-code>  
        <location>/nopage.html</location>  
    </error-page>  
    <error-page> <!--當(dāng)系統(tǒng)出現(xiàn)java.lang.NullPointerException,跳轉(zhuǎn)到頁面error.html-->  
        <exception-type>java.lang.NullPointerException</exception-type>  
        <location>/error.html</location>  
    </error-page>  
    <session-config><!--會(huì)話超時(shí)配置,單位分鐘-->  
        <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"> 

    <!-- 自動(dòng)掃描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" />  
  
        <!-- 配置獲取連接等待超時(shí)的時(shí)間 -->  
        <property name="maxWait" value="60000" />  
  
        <!-- 配置間隔多久才進(jìn)行一次檢測(cè),檢測(cè)需要關(guān)閉的空閑連接,單位是毫秒 -->  
        <property name="timeBetweenEvictionRunsMillis" value="60000" />  
  
        <!-- 配置一個(gè)連接在池中最小生存的時(shí)間,單位是毫秒 -->  
        <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,并且指定每個(gè)連接上PSCache的大小 -->  
        <property name="poolPreparedStatements" value="false" />  
        <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />  
  
        <!-- 配置監(jiān)控統(tǒng)計(jì)攔截的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" />  
  
    <!-- 對(duì)dataSource 數(shù)據(jù)源進(jìn)行事務(wù)管理 -->  
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource" />  
  
    <!-- 配置使Spring采用CGLIB代理 -->  
    <aop:aspectj-autoproxy proxy-target-class="true" />  
  
    <!-- 啟用對(duì)事務(wù)注解的支持 -->  
    <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>  

關(guān)于Spring Bean

備注,加載applicationContext時(shí),使用到了Bean的一些邏輯,這里簡(jiǎn)單介紹下spring bean的工作機(jī)制。

在Spring中,所有管理的對(duì)象都是JavaBean對(duì)象,而BeanFactory和ApplicationContext就是spring框架的兩個(gè)IOC容器,現(xiàn)在一般使用ApplicationnContext,其不但包含了BeanFactory的作用,同時(shí)還進(jìn)行更多的擴(kuò)展。

Spring和Bean的關(guān)系

bean配置有三種方法:

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

這里主要介紹第一種,基于xml的Bean配置。

關(guān)鍵字:Bean id、Bean類名、property.

一般情況下,Spring IOC容器中的一個(gè)Bean即對(duì)應(yīng)配置文件中的一個(gè)<bean>,這種鏡像對(duì)應(yīng)關(guān)系應(yīng)該容易理解。其中id為這個(gè)Bean的名稱,通過容器的getBean("foo")即可獲取對(duì)應(yīng)的Bean,在容器中起到定位查找的作用,是外部程序和Spring IOC容器進(jìn)行交互的橋梁。class屬性指定了Bean對(duì)應(yīng)的實(shí)現(xiàn)類。

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

Spring提供了一個(gè)context的命名空間,它提供了通過掃描類包以應(yīng)用注解定義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"
         >
    <!--②掃描類包以應(yīng)用注解定義的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注入的方式有兩種:

  • 在XML中配置,此時(shí)分別有屬性注入、構(gòu)造函數(shù)注入和工廠方法注入

  • 使用注解的方式注入 @Autowired,@Resource,@Required。

項(xiàng)目安裝

在本文中,我們將使用最新、最好的Spring Framework 5。我們將重點(diǎn)介紹Spring的經(jīng)典Web堆棧,該堆棧從框架的第一個(gè)版本中就嶄露頭角,并且現(xiàn)在依然是用Spring構(gòu)建Web應(yīng)用程序的主要方式。

對(duì)于初學(xué)者來說,為了安裝測(cè)試項(xiàng)目,最好使用Spring Boot和一些初學(xué)者依賴項(xiàng);還需要定義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>

請(qǐng)注意,為了使用Spring 5,我們還需要使用Spring Boot 2.x。截止到撰寫本文之時(shí),這依然是里程碑發(fā)布版,可在Spring Milestone Repository中找到。讓我們把這個(gè)存儲(chǔ)庫添加到你的Maven項(xiàng)目中:

<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的當(dāng)前版本。

示例項(xiàng)目

為了理解Spring Web MVC是如何工作的,我們將通過一個(gè)登錄頁面實(shí)現(xiàn)一個(gè)簡(jiǎn)單的應(yīng)用程序。為了顯示登錄頁面,我們需要為上下文根創(chuàng)建帶有GET映射的@Controller注解類InternalController。

hello()方法是無參數(shù)的。它返回一個(gè)由Spring MVC解釋為視圖名稱的String(在示例中是login.html模板):

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

為了處理用戶登錄,需要?jiǎng)?chuàng)建另一個(gè)用登錄數(shù)據(jù)處理POST請(qǐng)求的方法。然后根據(jù)結(jié)果將用戶重定向到成功或失敗的頁面。

請(qǐng)注意,login()方法接收域?qū)ο笞鳛閰?shù)并返回ModelAndView對(duì)象:

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是兩個(gè)不同對(duì)象的持有者:

  • Model——渲染頁面數(shù)據(jù)的鍵值映射
  • View——填充模型數(shù)據(jù)的頁面模板

連接這些是為了方便,這樣控制器方法可以一次返回它們。

要渲染HTML頁面,使用Thymeleaf作為視圖模板引擎,該引擎具有可靠和開箱即用的與Spring的集成。

Servlet作為Java Web應(yīng)用程序的基礎(chǔ)

那么,當(dāng)在瀏覽器中輸入http:// localhost:8080/時(shí),按Enter鍵,然后請(qǐng)求到達(dá)Web服務(wù)器,實(shí)際發(fā)生了什么?你如何從這個(gè)請(qǐng)求中看到瀏覽器中的Web表單?

鑒于該項(xiàng)目是一個(gè)簡(jiǎn)單的Spring Boot應(yīng)用程序,因此可以通過Spring5Application運(yùn)行它。

Spring Boot默認(rèn)使用Apache Tomcat。因此,運(yùn)行應(yīng)用程序時(shí),你可能會(huì)在日志中看到以下信息:

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是一個(gè)Servlet容器,因此發(fā)送給Tomcat Web服務(wù)器的每個(gè)HTTP請(qǐng)求自然都由Java servlet處理。所以Spring Web應(yīng)用程序入口點(diǎn)是一個(gè)servlet,這并不奇怪。

簡(jiǎn)單地說,servlet就是任何Java Web應(yīng)用程序的核心組件;它是低層次的,不會(huì)像MVC那樣在特定的編程模式中諸多要求。

一個(gè)HTTP servlet只能接收一個(gè)HTTP請(qǐng)求,以某種方式處理,然后發(fā)回一個(gè)響應(yīng)。

而且,從Servlet 3.0 API開始,你現(xiàn)在可以超越XML配置,并開始利用Java配置(只有很小的限制條件)。

DispatcherServlet作為Spring MVC的核心

作為一個(gè)Web應(yīng)用程序的開發(fā)人員,我們真正想要做的是抽象出以下繁瑣和模板化的任務(wù),并專注于有用的業(yè)務(wù)邏輯:

  • 將HTTP請(qǐng)求映射到某個(gè)處理方法

  • 將HTTP請(qǐng)求數(shù)據(jù)和標(biāo)題解析成數(shù)據(jù)傳輸對(duì)象(DTO)或域?qū)ο?/p>

  • 模型 – 視圖 – 控制器集成

  • 從DTO、域?qū)ο蟮壬身憫?yīng)

Spring DispatcherServlet能夠提供這些。它是Spring Web MVC框架的核心;此核心組件接收所有請(qǐng)求到應(yīng)用程序。

正如你所看到的,DispatcherServlet是非??蓴U(kuò)展的。例如,它允許你插入不同的現(xiàn)有或新的適配器進(jìn)行大量的任務(wù):

  • 將請(qǐng)求映射到應(yīng)該處理它的類或方法(HandlerMapping接口的實(shí)現(xiàn))

  • 使用特定模式處理請(qǐng)求,如常規(guī)servlet,更復(fù)雜的MVC工作流,或POJO bean中的方法(HandlerAdapter接口的實(shí)現(xiàn))

  • 按名稱解析視圖,允許你使用不同的模板引擎,XML,XSLT或任何其他視圖技術(shù)(ViewResolver接口的實(shí)現(xiàn))

  • 通過使用默認(rèn)的Apache Commons文件上傳實(shí)現(xiàn)或編寫你自己的MultipartResolver來解析多部分請(qǐng)求

  • 使用任何LocaleResolver實(shí)現(xiàn)解決語言環(huán)境,包括cookie,會(huì)話,Accept HTTP頭,或任何其他確定用戶所期望的語言環(huán)境的方式

處理HTTP請(qǐng)求

首先,我們將簡(jiǎn)單的HTTP請(qǐng)求的處理追蹤到在控制器層中的一個(gè)方法,然后返回到瀏覽器/客戶端。

DispatcherServlet具有很長(zhǎng)的繼承層次結(jié)構(gòu);自上而下地逐個(gè)理解這些是有價(jià)值的。請(qǐng)求處理方法最讓我們感興趣。

示意圖

理解HTTP請(qǐng)求,無論是在本地還是遠(yuǎn)程的標(biāo)準(zhǔn)開發(fā)中,都是理解MVC體系結(jié)構(gòu)的關(guān)鍵部分。

GenericServlet

GenericServlet是Servlet規(guī)范的一部分,不直接關(guān)注HTTP。它定義了接收傳入請(qǐng)求并產(chǎn)生響應(yīng)的service()方法。

注意,ServletRequest和ServletResponse方法參數(shù)如何與HTTP協(xié)議無關(guān):

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

這是最終被任何請(qǐng)求調(diào)用到服務(wù)器上的方法,包括簡(jiǎn)單的GET請(qǐng)求。

HttpServlet

顧名思義,HttpServlet類就是規(guī)范中定義的基于HTTP的Servlet實(shí)現(xiàn)。

更實(shí)際的說,HttpServlet是一個(gè)抽象類,有一個(gè)service()方法實(shí)現(xiàn),service()方法實(shí)現(xiàn)通過HTTP方法類型分割請(qǐng)求,大致如下所示:

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是層次結(jié)構(gòu)中第一個(gè)Spring-aware類。它使用從web.xml或WebApplicationInitializer接收到的servlet init-param值來注入bean的屬性。

在請(qǐng)求應(yīng)用程序的情況下,doGet(),doPost()等方法應(yīng)特定的HTTP請(qǐng)求而調(diào)用。

FrameworkServlet

FrameworkServlet集成Servlet功能與Web應(yīng)用程序上下文,實(shí)現(xiàn)了ApplicationContextAware接口。但它也能夠自行創(chuàng)建Web應(yīng)用程序上下文。

正如你已經(jīng)看到的,HttpServletBean超類注入init-params為bean屬性。所以,如果在servlet的contextClass init-param中提供了一個(gè)上下文類名,那么這個(gè)類的一個(gè)實(shí)例將被創(chuàng)建為應(yīng)用程序上下文。否則,將使用默認(rèn)的XmlWebApplicationContext類。

由于XML配置現(xiàn)在已經(jīng)過時(shí),Spring Boot默認(rèn)使用AnnotationConfigWebApplicationContext配置DispatcherServlet。但是你可以輕松更改。

例如,如果你需要使用基于Groovy的應(yīng)用程序上下文來配置Spring Web MVC應(yīng)用程序,則可以在web.xml文件中使用以下DispatcherServlet配置:

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

使用WebApplicationInitializer類,可以用更現(xiàn)代的基于Java的方式來完成相同的配置。

DispatcherServlet:統(tǒng)一請(qǐng)求處理

HttpServlet.service()實(shí)現(xiàn),會(huì)根據(jù)HTTP動(dòng)詞的類型來路由請(qǐng)求,這在低級(jí)servlet的上下文中是非常有意義的。然而,在Spring MVC的抽象級(jí)別,方法類型只是可以用來映射請(qǐng)求到其處理程序的參數(shù)之一。

因此,F(xiàn)rameworkServlet類的另一個(gè)主要功能是將處理邏輯重新加入到單個(gè)processRequest()方法中,processRequest()方法反過來又調(diào)用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:豐富請(qǐng)求

最后,DispatcherServlet實(shí)現(xiàn)doService()方法。在這里,它增加了一些可能會(huì)派上用場(chǎng)的有用對(duì)象到請(qǐng)求:Web應(yīng)用程序上下文,區(qū)域解析器,主題解析器,主題源等:

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()方法準(zhǔn)備輸入和輸出Flash映射。Flash映射基本上是一種模式,該模式將參數(shù)從一個(gè)請(qǐng)求傳遞到另一個(gè)緊跟的請(qǐng)求。這在重定向期間可能非常有用(例如在重定向之后向用戶顯示一次性信息消息):

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()方法調(diào)用負(fù)責(zé)請(qǐng)求調(diào)度的doDispatch()方法。

DispatcherServlet:調(diào)度請(qǐng)求

dispatch()方法的主要目的是為請(qǐng)求找到合適的處理程序,并為其提供請(qǐng)求/響應(yīng)參數(shù)。處理程序基本上是任何類型的object,不限于特定的接口。這也意味著Spring需要為此處理程序找到適配器,該處理程序知道如何與處理程序“交談”。

為了找到匹配請(qǐng)求的處理程序,Spring檢查HandlerMapping接口的注冊(cè)實(shí)現(xiàn)。有很多不同的實(shí)現(xiàn)可以滿足你的需求。

SimpleUrlHandlerMapping允許通過URL將請(qǐng)求映射到某個(gè)處理bean。例如,可以通過使用java.util.Properties實(shí)例注入其mappings屬性來配置,就像這樣:

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

可能處理程序映射最廣泛使用的類是RequestMappingHandlerMapping,它將請(qǐng)求映射到@Controller類的@ RequestMapping注釋方法。這正是使用控制器的hello()和login()方法連接調(diào)度程序的映射。

請(qǐng)注意,Spring-aware方法使用@GetMapping和@PostMapping進(jìn)行注釋。這些注釋依次用@RequestMapping元注釋標(biāo)記。

dispatch()方法還負(fù)責(zé)其他一些HTTP特定任務(wù):

  • 在資源未被修改的情況下,GET請(qǐng)求的短路處理
  • 針對(duì)相應(yīng)的請(qǐng)求應(yīng)用多部分解析器
  • 如果處理程序選擇異步處理該請(qǐng)求,則會(huì)短路處理該請(qǐng)求

處理請(qǐng)求

現(xiàn)在Spring已經(jīng)確定了請(qǐng)求的處理程序和處理程序的適配器,是時(shí)候來處理請(qǐng)求了。下面是HandlerAdapter.handle()方法的簽名。請(qǐng)注意,處理程序可以選擇如何處理請(qǐng)求:

  • 自主地編寫數(shù)據(jù)到響應(yīng)對(duì)象,并返回null
  • 返回由DispatcherServlet呈現(xiàn)的ModelAndView對(duì)象
@Nullable
ModelAndView handle(HttpServletRequest request, 
                    HttpServletResponse response, 
                    Object handler) throws Exception;

有幾種提供的處理程序類型。以下是SimpleControllerHandlerAdapter如何處理Spring MVC控制器實(shí)例(不要將其與@ Controller注釋POJO混淆)。

注意控制器處理程序如何返回ModelAndView對(duì)象,并且不自行呈現(xiàn)視圖:

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

第二個(gè)是SimpleServletHandlerAdapter,它將常規(guī)的Servlet作為請(qǐng)求處理器。

Servlet不知道任何有關(guān)ModelAndView的內(nèi)容,只是簡(jiǎn)單地自行處理請(qǐng)求,并將結(jié)果呈現(xiàn)給響應(yīng)對(duì)象。所以這個(gè)適配器只是返回null而不是ModelAndView:

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

我們碰到的情況是,控制器是有若干@RequestMapping注釋的POJO,所以任何處理程序基本上是包裝在HandlerMethod實(shí)例中的這個(gè)類的方法。為了適應(yīng)這個(gè)處理器類型,Spring使用RequestMappingHandlerAdapter類。

處理參數(shù)和返回處理程序方法的值

注意,控制器方法通常不會(huì)使用HttpServletRequest和HttpServletResponse,而是接收和返回許多不同類型的數(shù)據(jù),例如域?qū)ο?,路徑參?shù)等。

此外,要注意,我們不需要從控制器方法返回ModelAndView實(shí)例。可能會(huì)返回視圖名稱,或ResponseEntity,或?qū)⒈晦D(zhuǎn)換為JSON響應(yīng)等的POJO。

RequestMappingHandlerAdapter確保方法的參數(shù)從HttpServletRequest中解析出來。另外,它從方法的返回值中創(chuàng)建ModelAndView對(duì)象。

在RequestMappingHandlerAdapter中有一段重要的代碼,可確保所有這些轉(zhuǎn)換魔法的發(fā)生:

ServletInvocableHandlerMethod invocableMethod 
  = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
    invocableMethod.setHandlerMethodArgumentResolvers(
      this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
    invocableMethod.setHandlerMethodReturnValueHandlers(
      this.returnValueHandlers);
}
argumentResolvers對(duì)象是不同的HandlerMethodArgumentResolver實(shí)例的組合。

有超過30個(gè)不同的參數(shù)解析器實(shí)現(xiàn)。它們?cè)试S從請(qǐng)求中提取任何類型的信息,并將其作為方法參數(shù)提供。這包括URL路徑變量,請(qǐng)求主體參數(shù),請(qǐng)求標(biāo)頭,cookies,會(huì)話數(shù)據(jù)等。

returnValueHandlers對(duì)象是HandlerMethodReturnValueHandler對(duì)象的組合。還有很多不同的值處理程序可以處理方法的結(jié)果來創(chuàng)建適配器所期望的ModelAndViewobject。

例如,當(dāng)你從hello()方法返回字符串時(shí),ViewNameMethodReturnValueHandler處理這個(gè)值。但是,當(dāng)你從login()方法返回一個(gè)準(zhǔn)備好的ModelAndView時(shí),Spring會(huì)使用ModelAndViewMethodReturnValueHandler。

渲染視圖

到目前為止,Spring已經(jīng)處理了HTTP請(qǐng)求并接收了ModelAndView對(duì)象,所以它必須呈現(xiàn)用戶將在瀏覽器中看到的HTML頁面。它基于模型和封裝在ModelAndView對(duì)象中的選定視圖來完成。

另外請(qǐng)注意,我們可以呈現(xiàn)JSON對(duì)象,或XML,或任何可通過HTTP協(xié)議傳輸?shù)钠渌麛?shù)據(jù)格式。我們將在即將到來的REST-focused部分接觸更多。

讓我們回到DispatcherServlet。render()方法首先使用提供的LocaleResolver實(shí)例設(shè)置響應(yīng)語言環(huán)境。假設(shè)現(xiàn)代瀏覽器正確設(shè)置了Accept頭,并且默認(rèn)使用AcceptHeaderLocaleResolver。

在渲染過程中,ModelAndView對(duì)象可能已經(jīng)包含對(duì)所選視圖的引用,或者只是一個(gè)視圖名稱,或者如果控制器依賴于默認(rèn)視圖,則什么都沒有。

由于hello()和login()方法兩者都指定所需的視圖為String名稱,因此必須用該名稱查找。所以,這是viewResolvers列表開始起作用的地方:

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

這是一個(gè)ViewResolver實(shí)例列表,包括由thymeleaf-spring5集成庫提供的ThymeleafViewResolver。該解析器知道在哪里搜索視圖,并提供相應(yīng)的視圖實(shí)例。

在調(diào)用視圖的render()方法后,Spring最終通過發(fā)送HTML頁面到用戶的瀏覽器來完成請(qǐng)求處理。

REST支持

除了典型的MVC場(chǎng)景之外,我們還可以使用框架來創(chuàng)建REST Web服務(wù)。

簡(jiǎn)而言之,我們可以接受Resource作為輸入,指定POJO作為方法參數(shù),并使用@RequestBody對(duì)其進(jìn)行注釋。也可以使用@ResponseBody注釋方法本身,以指定其結(jié)果必須直接轉(zhuǎn)換為HTTP響應(yīng):

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的可擴(kuò)展性,這也是可行的。

為了將內(nèi)部DTO編組為REST表示,框架使用HttpMessageConverter基礎(chǔ)結(jié)構(gòu)。例如,其中一個(gè)實(shí)現(xiàn)是MappingJackson2HttpMessageConverter,它可以使用Jackson庫將模型對(duì)象轉(zhuǎn)換為JSON或從JSON轉(zhuǎn)換。

為了進(jìn)一步簡(jiǎn)化REST API的創(chuàng)建,Spring引入了@RestController注解。默認(rèn)情況下,這很方便地假定了@ResponseBody語義,并避免在每個(gè)REST控制器上的明確設(shè)置:

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

結(jié)論

在這篇文章中,我們?cè)敿?xì)了介紹在Spring MVC框架中請(qǐng)求的處理過程。了解框架的不同擴(kuò)展是如何協(xié)同工作來提供所有魔法的,可以讓你能夠事倍功半地處理HTTP協(xié)議難題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong閱讀 22,970評(píng)論 1 92
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,711評(píng)論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,290評(píng)論 6 342
  • 【韓喜文2018.07.01星期周日】 好展館讓天下沒有賣不出去的產(chǎn)品 好展館讓天下沒有不能傳承的文化 日精進(jìn):8...
    韓喜文閱讀 54評(píng)論 0 0
  • 以前寫過一篇文章也是有關(guān)維特根斯坦哲學(xué)轉(zhuǎn)折的啟示的,其目的在于探索如何以一個(gè)正確的方式打開聊天。而如今這篇文章則是...
    楓別雨閱讀 941評(píng)論 0 5

友情鏈接更多精彩內(nèi)容