javascript
Spring MVC 到底是如何工作的
轉(zhuǎn)載自??Spring MVC 到底是如何工作的
這篇文章將深入探討Spring框架的一部分——Spring Web MVC的強大功能及其內(nèi)部工作原理。
這篇文章的源代碼可以在GitHub上找到。
?
項目安裝
在本文中,我們將使用最新、最好的Spring Framework 5。我們將重點介紹Spring的經(jīng)典Web堆棧,該堆棧從框架的第一個版本中就嶄露頭角,并且現(xiàn)在依然是用Spring構(gòu)建Web應(yīng)用程序的主要方式。
對于初學(xué)者來說,為了安裝測試項目,最好使用Spring Boot和一些初學(xué)者依賴項;還需要定義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。截止到撰寫本文之時,這依然是里程碑發(fā)布版,可在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是如何工作的,我們將通過一個登錄頁面實現(xiàn)一個簡單的應(yīng)用程序。為了顯示登錄頁面,我們需要為上下文根創(chuàng)建帶有GET映射的@Controller注解類InternalController。
hello()方法是無參數(shù)的。它返回一個由Spring MVC解釋為視圖名稱的String(在示例中是login.html模板):
import org.springframework.web.bind.annotation.GetMapping; @GetMapping("/") public String hello() {return "login"; }為了處理用戶登錄,需要創(chuàng)建另一個用登錄數(shù)據(jù)處理POST請求的方法。然后根據(jù)結(jié)果將用戶重定向到成功或失敗的頁面。
請注意,login()方法接收域?qū)ο笞鳛閰?shù)并返回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——渲染頁面數(shù)據(jù)的鍵值映射
-
View——填充模型數(shù)據(jù)的頁面模板
連接這些是為了方便,這樣控制器方法可以一次返回它們。
要渲染HTML頁面,使用Thymeleaf作為視圖模板引擎,該引擎具有可靠和開箱即用的與Spring的集成。
Servlet作為Java Web應(yīng)用程序的基礎(chǔ)
那么,當在瀏覽器中輸入http:// localhost:8080/時,按Enter鍵,然后請求到達Web服務(wù)器,實際發(fā)生了什么?你如何從這個請求中看到瀏覽器中的Web表單?
鑒于該項目是一個簡單的Spring Boot應(yīng)用程序,因此可以通過Spring5Application運行它。
Spring Boot默認使用Apache Tomcat。因此,運行應(yīng)用程序時,你可能會在日志中看到以下信息:
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容器,因此發(fā)送給Tomcat Web服務(wù)器的每個HTTP請求自然都由Java servlet處理。所以Spring Web應(yīng)用程序入口點是一個servlet,這并不奇怪。
簡單地說,servlet就是任何Java Web應(yīng)用程序的核心組件;它是低層次的,不會像MVC那樣在特定的編程模式中諸多要求。
一個HTTP servlet只能接收一個HTTP請求,以某種方式處理,然后發(fā)回一個響應(yīng)。
而且,從Servlet 3.0 API開始,你現(xiàn)在可以超越XML配置,并開始利用Java配置(只有很小的限制條件)。
DispatcherServlet作為Spring MVC的核心
作為一個Web應(yīng)用程序的開發(fā)人員,我們真正想要做的是抽象出以下繁瑣和模板化的任務(wù),并專注于有用的業(yè)務(wù)邏輯:
-
將HTTP請求映射到某個處理方法
-
將HTTP請求數(shù)據(jù)和標題解析成數(shù)據(jù)傳輸對象(DTO)或域?qū)ο?/p>
-
模型 – 視圖 – 控制器集成
-
從DTO、域?qū)ο蟮壬身憫?yīng)
Spring?DispatcherServlet能夠提供這些。它是Spring Web?MVC框架的核心;此核心組件接收所有請求到應(yīng)用程序。
正如你所看到的,DispatcherServlet是非常可擴展的。例如,它允許你插入不同的現(xiàn)有或新的適配器進行大量的任務(wù):
-
將請求映射到應(yīng)該處理它的類或方法(HandlerMapping接口的實現(xiàn))
-
使用特定模式處理請求,如常規(guī)servlet,更復(fù)雜的MVC工作流,或POJO bean中的方法(HandlerAdapter接口的實現(xiàn))
-
按名稱解析視圖,允許你使用不同的模板引擎,XML,XSLT或任何其他視圖技術(shù)(ViewResolver接口的實現(xiàn))
-
通過使用默認的Apache Commons文件上傳實現(xiàn)或編寫你自己的MultipartResolver來解析多部分請求
-
使用任何LocaleResolver實現(xiàn)解決語言環(huán)境,包括cookie,會話,Accept HTTP頭,或任何其他確定用戶所期望的語言環(huán)境的方式
處理HTTP請求
首先,我們將簡單的HTTP請求的處理追蹤到在控制器層中的一個方法,然后返回到瀏覽器/客戶端。
DispatcherServlet具有很長的繼承層次結(jié)構(gòu);自上而下地逐個理解這些是有價值的。請求處理方法最讓我們感興趣。
?
理解HTTP請求,無論是在本地還是遠程的標準開發(fā)中,都是理解MVC體系結(jié)構(gòu)的關(guān)鍵部分。
GenericServlet
GenericServlet是Servlet規(guī)范的一部分,不直接關(guān)注HTTP。它定義了接收傳入請求并產(chǎn)生響應(yīng)的service()方法。
注意,ServletRequest和ServletResponse方法參數(shù)如何與HTTP協(xié)議無關(guān):
public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;這是最終被任何請求調(diào)用到服務(wù)器上的方法,包括簡單的GET請求。
HttpServlet
顧名思義,HttpServlet類就是規(guī)范中定義的基于HTTP的Servlet實現(xiàn)。
更實際的說,HttpServlet是一個抽象類,有一個service()方法實現(xiàn),service()方法實現(xiàn)通過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是層次結(jié)構(gòu)中第一個Spring-aware類。它使用從web.xml或WebApplicationInitializer接收到的servlet init-param值來注入bean的屬性。
在請求應(yīng)用程序的情況下,doGet(),doPost()等方法應(yīng)特定的HTTP請求而調(diào)用。
FrameworkServlet
FrameworkServlet集成Servlet功能與Web應(yīng)用程序上下文,實現(xiàn)了ApplicationContextAware接口。但它也能夠自行創(chuàng)建Web應(yīng)用程序上下文。
正如你已經(jīng)看到的,HttpServletBean超類注入init-params為bean屬性。所以,如果在servlet的contextClass init-param中提供了一個上下文類名,那么這個類的一個實例將被創(chuàng)建為應(yīng)用程序上下文。否則,將使用默認的XmlWebApplicationContext類。
由于XML配置現(xiàn)在已經(jīng)過時,Spring Boot默認使用AnnotationConfigWebApplicationContext配置DispatcherServlet。但是你可以輕松更改。
例如,如果你需要使用基于Groovy的應(yīng)用程序上下文來配置Spring Web MVC應(yīng)用程序,則可以在web.xml文件中使用以下DispatcherServlet配置:
dispatcherServletorg.springframework.web.servlet.DispatcherServletcontextClassorg.springframework.web.context.support.GroovyWebApplicationContext使用WebApplicationInitializer類,可以用更現(xiàn)代的基于Java的方式來完成相同的配置。
DispatcherServlet:統(tǒng)一請求處理
HttpServlet.service()實現(xiàn),會根據(jù)HTTP動詞的類型來路由請求,這在低級servlet的上下文中是非常有意義的。然而,在Spring MVC的抽象級別,方法類型只是可以用來映射請求到其處理程序的參數(shù)之一。
因此,FrameworkServlet類的另一個主要功能是將處理邏輯重新加入到單個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:豐富請求
最后,DispatcherServlet實現(xiàn)doService()方法。在這里,它增加了一些可能會派上用場的有用對象到請求: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()方法準備輸入和輸出Flash映射。Flash映射基本上是一種模式,該模式將參數(shù)從一個請求傳遞到另一個緊跟的請求。這在重定向期間可能非常有用(例如在重定向之后向用戶顯示一次性信息消息):
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)用負責(zé)請求調(diào)度的doDispatch()方法。
DispatcherServlet:調(diào)度請求
dispatch()方法的主要目的是為請求找到合適的處理程序,并為其提供請求/響應(yīng)參數(shù)。處理程序基本上是任何類型的object,不限于特定的接口。這也意味著Spring需要為此處理程序找到適配器,該處理程序知道如何與處理程序“交談”。
為了找到匹配請求的處理程序,Spring檢查HandlerMapping接口的注冊實現(xiàn)。有很多不同的實現(xiàn)可以滿足你的需求。
SimpleUrlHandlerMapping允許通過URL將請求映射到某個處理bean。例如,可以通過使用java.util.Properties實例注入其mappings屬性來配置,就像這樣:
/welcome.html=ticketController /show.html=ticketController可能處理程序映射最廣泛使用的類是RequestMappingHandlerMapping,它將請求映射到@Controller類的@ RequestMapping注釋方法。這正是使用控制器的hello()和login()方法連接調(diào)度程序的映射。
請注意,Spring-aware方法使用@GetMapping和@PostMapping進行注釋。這些注釋依次用@RequestMapping元注釋標記。
dispatch()方法還負責(zé)其他一些HTTP特定任務(wù):
-
在資源未被修改的情況下,GET請求的短路處理
-
針對相應(yīng)的請求應(yīng)用多部分解析器
-
如果處理程序選擇異步處理該請求,則會短路處理該請求
處理請求
現(xiàn)在Spring已經(jīng)確定了請求的處理程序和處理程序的適配器,是時候來處理請求了。下面是HandlerAdapter.handle()方法的簽名。請注意,處理程序可以選擇如何處理請求:
-
自主地編寫數(shù)據(jù)到響應(yīng)對象,并返回null
-
返回由DispatcherServlet呈現(xiàn)的ModelAndView對象
有幾種提供的處理程序類型。以下是SimpleControllerHandlerAdapter如何處理Spring MVC控制器實例(不要將其與@ Controller注釋POJO混淆)。
注意控制器處理程序如何返回ModelAndView對象,并且不自行呈現(xiàn)視圖:
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {return ((Controller) handler).handleRequest(request, response); }第二個是SimpleServletHandlerAdapter,它將常規(guī)的Servlet作為請求處理器。
Servlet不知道任何有關(guān)ModelAndView的內(nèi)容,只是簡單地自行處理請求,并將結(jié)果呈現(xiàn)給響應(yīng)對象。所以這個適配器只是返回null而不是ModelAndView:
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {((Servlet) handler).service(request, response); return null; }我們碰到的情況是,控制器是有若干@RequestMapping注釋的POJO,所以任何處理程序基本上是包裝在HandlerMethod實例中的這個類的方法。為了適應(yīng)這個處理器類型,Spring使用RequestMappingHandlerAdapter類。
處理參數(shù)和返回處理程序方法的值
注意,控制器方法通常不會使用HttpServletRequest和HttpServletResponse,而是接收和返回許多不同類型的數(shù)據(jù),例如域?qū)ο?#xff0c;路徑參數(shù)等。
此外,要注意,我們不需要從控制器方法返回ModelAndView實例。可能會返回視圖名稱,或ResponseEntity,或?qū)⒈晦D(zhuǎn)換為JSON響應(yīng)等的POJO。
RequestMappingHandlerAdapter確保方法的參數(shù)從HttpServletRequest中解析出來。另外,它從方法的返回值中創(chuàng)建ModelAndView對象。
在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對象是不同的HandlerMethodArgumentResolver實例的組合。
有超過30個不同的參數(shù)解析器實現(xiàn)。它們允許從請求中提取任何類型的信息,并將其作為方法參數(shù)提供。這包括URL路徑變量,請求主體參數(shù),請求標頭,cookies,會話數(shù)據(jù)等。
returnValueHandlers對象是HandlerMethodReturnValueHandler對象的組合。還有很多不同的值處理程序可以處理方法的結(jié)果來創(chuàng)建適配器所期望的ModelAndViewobject。
例如,當你從hello()方法返回字符串時,ViewNameMethodReturnValueHandler處理這個值。但是,當你從login()方法返回一個準備好的ModelAndView時,Spring會使用ModelAndViewMethodReturnValueHandler。
渲染視圖
到目前為止,Spring已經(jīng)處理了HTTP請求并接收了ModelAndView對象,所以它必須呈現(xiàn)用戶將在瀏覽器中看到的HTML頁面。它基于模型和封裝在ModelAndView對象中的選定視圖來完成。
另外請注意,我們可以呈現(xiàn)JSON對象,或XML,或任何可通過HTTP協(xié)議傳輸?shù)钠渌麛?shù)據(jù)格式。我們將在即將到來的REST-focused部分接觸更多。
讓我們回到DispatcherServlet。render()方法首先使用提供的LocaleResolver實例設(shè)置響應(yīng)語言環(huán)境。假設(shè)現(xiàn)代瀏覽器正確設(shè)置了Accept頭,并且默認使用AcceptHeaderLocaleResolver。
在渲染過程中,ModelAndView對象可能已經(jīng)包含對所選視圖的引用,或者只是一個視圖名稱,或者如果控制器依賴于默認視圖,則什么都沒有。
由于hello()和login()方法兩者都指定所需的視圖為String名稱,因此必須用該名稱查找。所以,這是viewResolvers列表開始起作用的地方:
for (ViewResolver viewResolver : this.viewResolvers) {View view = viewResolver.resolveViewName(viewName, locale);if (view != null) {return view;} }這是一個ViewResolver實例列表,包括由thymeleaf-spring5集成庫提供的ThymeleafViewResolver。該解析器知道在哪里搜索視圖,并提供相應(yīng)的視圖實例。
在調(diào)用視圖的render()方法后,Spring最終通過發(fā)送HTML頁面到用戶的瀏覽器來完成請求處理。
REST支持
除了典型的MVC場景之外,我們還可以使用框架來創(chuàng)建REST Web服務(wù)。
簡而言之,我們可以接受Resource作為輸入,指定POJO作為方法參數(shù),并使用@RequestBody對其進行注釋。也可以使用@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的可擴展性,這也是可行的。
為了將內(nèi)部DTO編組為REST表示,框架使用HttpMessageConverter基礎(chǔ)結(jié)構(gòu)。例如,其中一個實現(xiàn)是MappingJackson2HttpMessageConverter,它可以使用Jackson庫將模型對象轉(zhuǎn)換為JSON或從JSON轉(zhuǎn)換。
為了進一步簡化REST API的創(chuàng)建,Spring引入了@RestController注解。默認情況下,這很方便地假定了@ResponseBody語義,并避免在每個REST控制器上的明確設(shè)置:
import org.springframework.web.bind.annotation.RestController; @RestController public class RestfulWebServiceController {@GetMapping("/message")public MyOutputResource getMessage() {return new MyOutputResource("Hello!");} }結(jié)論
在這篇文章中,我們詳細了介紹在Spring MVC框架中請求的處理過程。了解框架的不同擴展是如何協(xié)同工作來提供所有魔法的,可以讓你能夠事倍功半地處理HTTP協(xié)議難題。
?
?
總結(jié)
以上是生活随笔為你收集整理的Spring MVC 到底是如何工作的的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 呦呦切克闹是啥意思 你看见过这个词语吗
- 下一篇: Spring经典面试题和答案