這篇文章將深刻探討Spring框架的一部分——Spring Web MVC的強大功能及其內部工做原理。html
在本文中,咱們將使用最新、最好的Spring Framework 5。咱們將重點介紹Spring的經典Web堆棧,該堆棧從框架的第一個版本中就嶄露頭角,而且如今依然是用Spring構建Web應用程序的主要方式。java
對於初學者來講,爲了安裝測試項目,最好使用Spring Boot和一些初學者依賴項;還須要定義parent:web
<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項目中:spring
<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的當前版本。apache
爲了理解Spring Web MVC是如何工做的,咱們將經過一個登陸頁面實現一個簡單的應用程序。爲了顯示登陸頁面,咱們須要爲上下文根建立帶有GET映射的@Controller註解類InternalController。編程
hello()方法是無參數的。它返回一個由Spring MVC解釋爲視圖名稱的String(在示例中是login.html模板):json
import org.springframework.web.bind.annotation.GetMapping; @GetMapping("/") public String hello() { return "login"; }
爲了處理用戶登陸,須要建立另外一個用登陸數據處理POST請求的方法。而後根據結果將用戶重定向到成功或失敗的頁面。api
請注意,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是兩個不一樣對象的持有者:tomcat
鏈接這些是爲了方便,這樣控制器方法能夠一次返回它們。
要渲染HTML頁面,使用Thymeleaf做爲視圖模板引擎,該引擎具備可靠和開箱即用的與Spring的集成。
那麼,當在瀏覽器中輸入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配置(只有很小的限制條件)。
做爲一個Web應用程序的開發人員,咱們真正想要作的是抽象出如下繁瑣和模板化的任務,並專一於有用的業務邏輯:
Spring DispatcherServlet可以提供這些。它是Spring Web MVC框架的核心;此核心組件接收全部請求到應用程序。
正如你所看到的,DispatcherServlet是很是可擴展的。例如,它容許你插入不一樣的現有或新的適配器進行大量的任務:
首先,咱們將簡單的HTTP請求的處理追蹤到在控制器層中的一個方法,而後返回到瀏覽器/客戶端。
DispatcherServlet具備很長的繼承層次結構;自上而下地逐個理解這些是有價值的。請求處理方法最讓咱們感興趣。
理解HTTP請求,不管是在本地仍是遠程的標準開發中,都是理解MVC體系結構的關鍵部分。
GenericServlet是Servlet規範的一部分,不直接關注HTTP。它定義了接收傳入請求併產生響應的service()方法。
注意,ServletRequest和ServletResponse方法參數如何與HTTP協議無關:
public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
這是最終被任何請求調用到服務器上的方法,包括簡單的GET請求。
顧名思義,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是層次結構中第一個Spring-aware類。它使用從web.xml或WebApplicationInitializer接收到的servlet init-param值來注入bean的屬性。
在請求應用程序的狀況下,doGet(),doPost()等方法應特定的HTTP請求而調用。
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的方式來完成相同的配置。
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實現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()方法。
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特定任務:
如今Spring已經肯定了請求的處理程序和處理程序的適配器,是時候來處理請求了。下面是HandlerAdapter.handle()方法的簽名。請注意,處理程序能夠選擇如何處理請求:
@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頁面到用戶的瀏覽器來完成請求處理。
除了典型的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協議難題。
你們掃描下方二維碼關注下個人微信公衆號,公衆號內沒有福利,只會按期生產技術性文章!