Spring MVC 的工作原理则果,分享一篇外文翻譯的技術(shù)貼蜂科。
-英文原文:原文跳轉(zhuǎn)
-譯文跳轉(zhuǎn):譯文跳轉(zhuǎn)
在譯文基礎(chǔ)上,加了一些自己的理解短条,做了些梳理。
Spring MCN工作流程示意圖解析
1.瀏覽器發(fā)送請(qǐng)求到前端控制器(DispatcherServlet)才菠,DispatcherServlet請(qǐng)求處理器映射器(HandlerMappering)去查找處理器(Handle):通過(guò)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返回給用戶(hù)
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框架中是如何解決從頁(yè)面?zhèn)鱽?lái)的字符串的編碼問(wèn)題的呢?
下面我們來(lái)看看Spring框架給我們提供過(guò)濾器CharacterEncodingFilter
這個(gè)過(guò)濾器就是針對(duì)于每次瀏覽器請(qǐng)求進(jìn)行過(guò)濾的步悠,然后再其之上添加了父類(lèi)沒(méi)有的功能即處理字符編碼签杈。
其中encoding用來(lái)設(shè)置編碼格式,forceEncoding用來(lái)設(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配置文件中并沒(méi)有BeanFactory谚咬,要想在業(yè)務(wù)層中的class 文件中直接引用Spring容器管理的bean可通過(guò)以下方式-->
<!--1鹦付、在web.xml配置監(jiān)聽(tīng)器ContextLoaderListener-->
<!--ContextLoaderListener的作用就是啟動(dòng)Web容器時(shí),自動(dòng)裝配ApplicationContext的配置信息择卦。因?yàn)樗鼘?shí)現(xiàn)了ServletContextListener這個(gè)接口敲长,在web.xml配置這個(gè)監(jiān)聽(tīng)器郎嫁,啟動(dòng)容器時(shí),就會(huì)默認(rèn)執(zhí)行它實(shí)現(xiàn)的方法祈噪。
在ContextLoaderListener中關(guān)聯(lián)了ContextLoader這個(gè)類(lèi)泽铛,所以整個(gè)加載配置過(guò)程由ContextLoader來(lái)完成。
它的API說(shuō)明
第一段說(shuō)明ContextLoader可以由 ContextLoaderListener和ContextLoaderServlet生成钳降。
如果查看ContextLoaderServlet的API厚宰,可以看到它也關(guān)聯(lián)了ContextLoader這個(gè)類(lèi)而且它實(shí)現(xiàn)了HttpServlet這個(gè)接口
第二段,ContextLoader創(chuàng)建的是 XmlWebApplicationContext這樣一個(gè)類(lèi)遂填,它實(shí)現(xiàn)的接口是WebApplicationContext->ConfigurableWebApplicationContext->ApplicationContext->
BeanFactory這樣一來(lái)spring中的所有bean都由這個(gè)類(lèi)來(lái)創(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中不寫(xiě)任何參數(shù)配置信息,默認(rèn)的路徑是"/WEB-INF/applicationContext.xml吓坚,
在WEB-INF目錄下創(chuàng)建的xml文件的名稱(chēng)必須是applicationContext.xml撵幽。
如果是要自定義文件名可以在web.xml里加入contextConfigLocation這個(gè)context參數(shù):
在<param-value> </param-value>里指定相應(yīng)的xml文件名,如果有多個(gè)xml文件礁击,可以寫(xiě)在一起并以“,”號(hào)分隔盐杂。
也可以這樣applicationContext-*.xml采用通配符,比如這那個(gè)目錄下有applicationContext-ibatis-base.xml哆窿,
applicationContext-action.xml链烈,applicationContext-ibatis-dao.xml等文件,都會(huì)一同被載入挚躯。
在ContextLoaderListener中關(guān)聯(lián)了ContextLoader這個(gè)類(lèi)强衡,所以整個(gè)加載配置過(guò)程由ContextLoader來(lái)完成。-->
<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)文件的訪(fǎng)問(wèn)也就被攔截了缩搅。-->
<!--方案一:激活Tomcat的defaultServlet來(lái)處理靜態(tài)文件-->
<!--要寫(xiě)在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ī)則要自已定義筑累,把攔截下來(lái)的請(qǐng)求袱蜡,依據(jù)某某規(guī)則分發(fā)到目標(biāo)Controller(我們寫(xiě)的Action)來(lái)處理。-->
<servlet>
<servlet-name>DispatcherServlet</servlet-name><!--在DispatcherServlet的初始化過(guò)程中慢宗,框架會(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> 這里可以使用多種寫(xiě)法-->
<!--1、不寫(xiě),使用默認(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奏司,是通過(guò)名字來(lái)區(qū)分的乔询。每一個(gè)DispatcherServlet有自己的WebApplicationContext上下文對(duì)象。同時(shí)保存的ServletContext中和Request對(duì)象中.-->
<!--ApplicationContext是Spring的核心韵洋,Context我們通常解釋為上下文環(huán)境竿刁,我想用“容器”來(lái)表述它更容易理解一些,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)類(lèi)型的應(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><!--指定歡迎頁(yè)面-->
<welcome-file>login.html</welcome-file>
</welcome-file-list>
<error-page> <!--當(dāng)系統(tǒng)出現(xiàn)404錯(cuò)誤梗脾,跳轉(zhuǎn)到頁(yè)面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)到頁(yè)面error.html-->
<exception-type>java.lang.NullPointerException</exception-type>
<location>/error.html</location>
</error-page>
<session-config><!--會(huì)話(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包 ,將帶有注解的類(lèi) 納入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" />
<!-- 打開(kāi)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ì)象爆价,而B(niǎo)eanFactory和ApplicationContext就是spring框架的兩個(gè)IOC容器垦巴,現(xiàn)在一般使用ApplicationnContext,其不但包含了BeanFactory的作用铭段,同時(shí)還進(jìn)行更多的擴(kuò)展骤宣。
bean配置有三種方法:
- 基于xml配置Bean
- 使用注解定義Bean
- 基于java類(lèi)提供Bean定義信息
這里主要介紹第一種,基于xml的Bean配置序愚。
關(guān)鍵字:Bean id憔披、Bean類(lèi)名、property.
一般情況下爸吮,Spring IOC容器中的一個(gè)Bean即對(duì)應(yīng)配置文件中的一個(gè)<bean>,這種鏡像對(duì)應(yīng)關(guān)系應(yīng)該容易理解芬膝。其中id為這個(gè)Bean的名稱(chēng),通過(guò)容器的getBean("foo")即可獲取對(duì)應(yīng)的Bean形娇,在容器中起到定位查找的作用锰霜,是外部程序和Spring IOC容器進(jìn)行交互的橋梁。class屬性指定了Bean對(duì)應(yīng)的實(shí)現(xiàn)類(lèi)桐早。
<!-- 引入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的命名空間癣缅,它提供了通過(guò)掃描類(lèi)包以應(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"
>
<!--②掃描類(lèi)包以應(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ù)注入和工廠(chǎng)方法注入
使用注解的方式注入 @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é)者來(lái)說(shuō)抡秆,為了安裝測(cè)試項(xiàng)目,最好使用Spring Boot和一些初學(xué)者依賴(lài)項(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檩坚。截止到撰寫(xiě)本文之時(shí)着撩,這依然是里程碑發(fā)布版,可在Spring Milestone Repository中找到匾委。讓我們把這個(gè)存儲(chǔ)庫(kù)添加到你的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是如何工作的,我們將通過(guò)一個(gè)登錄頁(yè)面實(shí)現(xiàn)一個(gè)簡(jiǎn)單的應(yīng)用程序赂乐。為了顯示登錄頁(yè)面薯鳍,我們需要為上下文根創(chuàng)建帶有GET映射的@Controller注解類(lèi)InternalController。
hello()方法是無(wú)參數(shù)的挨措。它返回一個(gè)由Spring MVC解釋為視圖名稱(chēng)的String(在示例中是login.html模板):
import org.springframework.web.bind.annotation.GetMapping;
@GetMapping("/")
public String hello() {
return "login";
}
為了處理用戶(hù)登錄挖滤,需要?jiǎng)?chuàng)建另一個(gè)用登錄數(shù)據(jù)處理POST請(qǐng)求的方法。然后根據(jù)結(jié)果將用戶(hù)重定向到成功或失敗的頁(yè)面浅役。
請(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——渲染頁(yè)面數(shù)據(jù)的鍵值映射
- View——填充模型數(shù)據(jù)的頁(yè)面模板
連接這些是為了方便,這樣控制器方法可以一次返回它們觉既。
要渲染HTML頁(yè)面惧盹,使用Thymeleaf作為視圖模板引擎,該引擎具有可靠和開(kāi)箱即用的與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)用程序,因此可以通過(guò)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)單地說(shuō)橄仆,servlet就是任何Java Web應(yīng)用程序的核心組件;它是低層次的衅斩,不會(huì)像MVC那樣在特定的編程模式中諸多要求盆顾。
一個(gè)HTTP servlet只能接收一個(gè)HTTP請(qǐng)求,以某種方式處理畏梆,然后發(fā)回一個(gè)響應(yīng)您宪。
而且,從Servlet 3.0 API開(kāi)始奠涌,你現(xiàn)在可以超越XML配置宪巨,并開(kāi)始利用Java配置(只有很小的限制條件)。
DispatcherServlet作為Spring MVC的核心
作為一個(gè)Web應(yīng)用程序的開(kāi)發(fā)人員溜畅,我們真正想要做的是抽象出以下繁瑣和模板化的任務(wù)捏卓,并專(zhuān)注于有用的業(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是非沉淠可擴(kuò)展的。例如汤功,它允許你插入不同的現(xiàn)有或新的適配器進(jìn)行大量的任務(wù):
將請(qǐng)求映射到應(yīng)該處理它的類(lèi)或方法(HandlerMapping接口的實(shí)現(xiàn))
使用特定模式處理請(qǐng)求岭洲,如常規(guī)servlet蕊程,更復(fù)雜的MVC工作流,或POJO bean中的方法(HandlerAdapter接口的實(shí)現(xiàn))
按名稱(chēng)解析視圖,允許你使用不同的模板引擎菩彬,XML,XSLT或任何其他視圖技術(shù)(ViewResolver接口的實(shí)現(xiàn))
通過(guò)使用默認(rèn)的Apache Commons文件上傳實(shí)現(xiàn)或編寫(xiě)你自己的MultipartResolver來(lái)解析多部分請(qǐng)求
使用任何LocaleResolver實(shí)現(xiàn)解決語(yǔ)言環(huán)境康栈,包括cookie辆苔,會(huì)話(huà),Accept HTTP頭忿族,或任何其他確定用戶(hù)所期望的語(yǔ)言環(huán)境的方式
處理HTTP請(qǐng)求
首先锣笨,我們將簡(jiǎn)單的HTTP請(qǐng)求的處理追蹤到在控制器層中的一個(gè)方法,然后返回到瀏覽器/客戶(hù)端道批。
DispatcherServlet具有很長(zhǎng)的繼承層次結(jié)構(gòu)错英;自上而下地逐個(gè)理解這些是有價(jià)值的。請(qǐng)求處理方法最讓我們感興趣隆豹。
理解HTTP請(qǐng)求椭岩,無(wú)論是在本地還是遠(yuǎn)程的標(biāo)準(zhǔn)開(kāi)發(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é)議無(wú)關(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類(lèi)就是規(guī)范中定義的基于HTTP的Servlet實(shí)現(xiàn)瞒渠。
更實(shí)際的說(shuō),HttpServlet是一個(gè)抽象類(lèi)技扼,有一個(gè)service()方法實(shí)現(xiàn)伍玖,service()方法實(shí)現(xiàn)通過(guò)HTTP方法類(lèi)型分割請(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
接下來(lái)剿吻,HttpServletBean是層次結(jié)構(gòu)中第一個(gè)Spring-aware類(lèi)窍箍。它使用從web.xml或WebApplicationInitializer接收到的servlet init-param值來(lái)注入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超類(lèi)注入init-params為bean屬性帆卓。所以,如果在servlet的contextClass init-param中提供了一個(gè)上下文類(lèi)名米丘,那么這個(gè)類(lèi)的一個(gè)實(shí)例將被創(chuàng)建為應(yīng)用程序上下文剑令。否則,將使用默認(rèn)的XmlWebApplicationContext類(lèi)拄查。
由于XML配置現(xiàn)在已經(jīng)過(guò)時(shí)吁津,Spring Boot默認(rèn)使用AnnotationConfigWebApplicationContext配置DispatcherServlet。但是你可以輕松更改堕扶。
例如碍脏,如果你需要使用基于Groovy的應(yīng)用程序上下文來(lái)配置Spring Web MVC應(yīng)用程序,則可以在web.xml文件中使用以下DispatcherServlet配置:
dispatcherServlet
org.springframework.web.servlet.DispatcherServlet
contextClass
org.springframework.web.context.support.GroovyWebApplicationContext
使用WebApplicationInitializer類(lèi)稍算,可以用更現(xiàn)代的基于Java的方式來(lái)完成相同的配置典尾。
DispatcherServlet:統(tǒng)一請(qǐng)求處理
HttpServlet.service()實(shí)現(xiàn),會(huì)根據(jù)HTTP動(dòng)詞的類(lèi)型來(lái)路由請(qǐng)求邪蛔,這在低級(jí)servlet的上下文中是非常有意義的急黎。然而,在Spring MVC的抽象級(jí)別侧到,方法類(lèi)型只是可以用來(lái)映射請(qǐng)求到其處理程序的參數(shù)之一勃教。
因此,F(xiàn)rameworkServlet類(lèi)的另一個(gè)主要功能是將處理邏輯重新加入到單個(gè)processRequest()方法中匠抗,processRequest()方法反過(guò)來(lái)又調(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)求奶是。這在重定向期間可能非常有用(例如在重定向之后向用戶(hù)顯示一次性信息消息):
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ù)。處理程序基本上是任何類(lèi)型的object初嘹,不限于特定的接口及汉。這也意味著Spring需要為此處理程序找到適配器,該處理程序知道如何與處理程序“交談”屯烦。
為了找到匹配請(qǐng)求的處理程序坷随,Spring檢查HandlerMapping接口的注冊(cè)實(shí)現(xiàn)。有很多不同的實(shí)現(xiàn)可以滿(mǎn)足你的需求漫贞。
SimpleUrlHandlerMapping允許通過(guò)URL將請(qǐng)求映射到某個(gè)處理bean甸箱。例如,可以通過(guò)使用java.util.Properties實(shí)例注入其mappings屬性來(lái)配置迅脐,就像這樣:
/welcome.html=ticketController
/show.html=ticketController
可能處理程序映射最廣泛使用的類(lèi)是RequestMappingHandlerMapping芍殖,它將請(qǐng)求映射到@Controller類(lèi)的@ 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í)候來(lái)處理請(qǐng)求了蒂窒。下面是HandlerAdapter.handle()方法的簽名。請(qǐng)注意,處理程序可以選擇如何處理請(qǐng)求:
- 自主地編寫(xiě)數(shù)據(jù)到響應(yīng)對(duì)象洒琢,并返回null
- 返回由DispatcherServlet呈現(xiàn)的ModelAndView對(duì)象
@Nullable
ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception;
有幾種提供的處理程序類(lèi)型秧秉。以下是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è)類(lèi)的方法。為了適應(yīng)這個(gè)處理器類(lèi)型愉择,Spring使用RequestMappingHandlerAdapter類(lèi)骄崩。
處理參數(shù)和返回處理程序方法的值
注意,控制器方法通常不會(huì)使用HttpServletRequest和HttpServletResponse薄辅,而是接收和返回許多不同類(lèi)型的數(shù)據(jù)要拂,例如域?qū)ο螅窂絽?shù)等站楚。
此外脱惰,要注意,我們不需要從控制器方法返回ModelAndView實(shí)例窿春±唬可能會(huì)返回視圖名稱(chēng),或ResponseEntity旧乞,或?qū)⒈晦D(zhuǎn)換為JSON響應(yīng)等的POJO蔚润。
RequestMappingHandlerAdapter確保方法的參數(shù)從HttpServletRequest中解析出來(lái)。另外尺栖,它從方法的返回值中創(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í)例的組合延赌。
有超過(guò)30個(gè)不同的參數(shù)解析器實(shí)現(xiàn)除盏。它們?cè)试S從請(qǐng)求中提取任何類(lèi)型的信息,并將其作為方法參數(shù)提供挫以。這包括URL路徑變量者蠕,請(qǐng)求主體參數(shù),請(qǐng)求標(biāo)頭掐松,cookies踱侣,會(huì)話(huà)數(shù)據(jù)等粪小。
returnValueHandlers對(duì)象是HandlerMethodReturnValueHandler對(duì)象的組合。還有很多不同的值處理程序可以處理方法的結(jié)果來(lái)創(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)用戶(hù)將在瀏覽器中看到的HTML頁(yè)面刺下。它基于模型和封裝在ModelAndView對(duì)象中的選定視圖來(lái)完成。
另外請(qǐng)注意稽荧,我們可以呈現(xiàn)JSON對(duì)象橘茉,或XML,或任何可通過(guò)HTTP協(xié)議傳輸?shù)钠渌麛?shù)據(jù)格式姨丈。我們將在即將到來(lái)的REST-focused部分接觸更多畅卓。
讓我們回到DispatcherServlet。render()方法首先使用提供的LocaleResolver實(shí)例設(shè)置響應(yīng)語(yǔ)言環(huán)境蟋恬。假設(shè)現(xiàn)代瀏覽器正確設(shè)置了Accept頭翁潘,并且默認(rèn)使用AcceptHeaderLocaleResolver。
在渲染過(guò)程中歼争,ModelAndView對(duì)象可能已經(jīng)包含對(duì)所選視圖的引用拜马,或者只是一個(gè)視圖名稱(chēng),或者如果控制器依賴(lài)于默認(rèn)視圖沐绒,則什么都沒(méi)有俩莽。
由于hello()和login()方法兩者都指定所需的視圖為String名稱(chēng),因此必須用該名稱(chēng)查找乔遮。所以扮超,這是viewResolvers列表開(kāi)始起作用的地方:
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
這是一個(gè)ViewResolver實(shí)例列表,包括由thymeleaf-spring5集成庫(kù)提供的ThymeleafViewResolver蹋肮。該解析器知道在哪里搜索視圖瞒津,并提供相應(yīng)的視圖實(shí)例。
在調(diào)用視圖的render()方法后括尸,Spring最終通過(guò)發(fā)送HTML頁(yè)面到用戶(hù)的瀏覽器來(lái)完成請(qǐng)求處理巷蚪。
REST支持
除了典型的MVC場(chǎng)景之外,我們還可以使用框架來(lái)創(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庫(kù)將模型對(duì)象轉(zhuǎn)換為JSON或從JSON轉(zhuǎn)換。
為了進(jìn)一步簡(jiǎn)化REST API的創(chuàng)建匕荸,Spring引入了@RestController注解爹谭。默認(rèn)情況下,這很方便地假定了@ResponseBody語(yǔ)義榛搔,并避免在每個(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)求的處理過(guò)程。了解框架的不同擴(kuò)展是如何協(xié)同工作來(lái)提供所有魔法的践惑,可以讓你能夠事倍功半地處理HTTP協(xié)議難題腹泌。