Spring MVC 到底是如何工作的?

2017-11-23    分類:JAVA開發、程式設計開發、首頁精華0人評論發表於2017-11-23

本文由碼農網 – 小峰原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

這篇文章將深入探討Spring框架的一部分——Spring Web MVC的強大功能及其內部工作原理。

這篇文章的原始碼可以在GitHub上找到。

專案安裝

在本文中,我們將使用最新、最好的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協議難題。

譯文連結:http://www.codeceo.com/article/how-spring-mvc-work.html
英文原文:How Spring MVC Really Works
翻譯作者:碼農網 – 小峰
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章