1. 程式人生 > >Spring Web MVC 深入研究

Spring Web MVC 深入研究

Spring Web MVC的特性

Spring Web MVC如何真正起作用

介紹

這是對Spring Web MVC的強大功能和內部工作的深入研究,它是Spring Framework的一部分。

專案設定

在本文中,我們將使用最新和最好的Spring Framework 5.我們將重點放在Spring的經典Web堆疊上,該堆疊可以從框架的最初版本獲得,並且仍然是構建Web應用程式的主要方式。與春天。 

對於初學者來說,要設定測試專案,您將使用Spring Boot及其一些初學者依賴項; 你還需要定義父:

<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的工作原理,您將使用登入頁面實現一個簡單的應用程式。要顯示登入頁面,請建立一個@Controller -annotated類InternalController,其中包含上下文根的GET對映。

問候()方法是無引數。它返回一個String,由Spring MVC解釋為檢視名稱(在我們的例子中是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是兩個不同物件的持有者:

  • 模型 - 用於呈現頁面的資料的鍵值對映
  • 檢視 - 填充了模型資料的頁面模板

這些是為了方便而連線的,因此控制器方法可以同時返回它們。

要呈現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,會話,接受 HTTP標頭或確定使用者期望的語言環境的任何其他方式

處理HTTP請求

首先,讓我們將簡單HTTP請求的處理跟蹤到控制器層中的方法並返回到瀏覽器/客戶端。

的DispatcherServlet有很長的繼承層次; 從上到下逐一理解這些個別方面是值得的。請求處理方法最讓我們感興趣。

https://lh3.googleusercontent.com/4ahYReme6gkGU8NIU--JzvxgCckXNYzCBa_Fi7Xk6DwqNctyNj0pOB_UPU4Euboy66vfURfovJK8VUgJMTz0ms2yQtFnqEhw9iJnWd_pCCpWN5tNXIYhUFtkCxakO-GTyuKlHoqs

在標準開發期間以及遠端理解HTTP請求是理解MVC架構的關鍵部分。

GenericServlet類

GenericServlet是Servlet規範的一部分,不直接關注HTTP。它定義了接收傳入請求並生成響應的service()方法。

請注意ServletRequestServletResponse方法引數如何與HTTP協議無關:

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

這是最終在對伺服器的任何請求上呼叫的方法,包括簡單的GET請求。

HttpServlet的

顧名思義,HttpServlet類是以HTTP為中心的Servlet實現,也是由規範定義的。

在更實際的術語中,HttpServlet是一個帶有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的類。它使用從web.xmlWebApplicationInitializer接收的servlet init-param值注入bean的屬性。

如果對您的應用程式發出請求,則會針對這些特定的HTTP請求呼叫doGet()doPost()等方法。

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()方法,該方法又呼叫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 -annotated方法。這正是將排程程式與控制器的hello()login()方法連線起來的對映。

請注意,您的Spring感知方法相應地使用@GetMapping@PostMapping進行註釋。反過來,這些註釋用@RequestMapping元註釋標記。

排程()方法還需要照顧其他一些特定的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類。

處理Handler方法的引數和返回值

請注意,控制器方法通常不接受HttpServletRequestHttpServletResponse引數,而是接收和返回許多不同型別的資料,例如域物件,路徑引數等。

另請注意,您不需要從控制器方法返回ModelAndView例項。您可以返回檢視名稱,或者將轉換為JSON響應的ResponseEntity或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);
}

有超過30種不同的引數解析器實現。它們允許從請求中提取任何型別的資訊並將其作為方法引數提供。這包括URL路徑變數,請求正文引數,請求標頭,cookie,會話資料等。

所述returnValueHandlers物件是一個複合HandlerMethodReturnValueHandler物件。還有許多不同的值處理程式可以處理方法的結果,以建立介面卡所期望的ModelAndView物件。

例如,當您從hello()方法返回一個字串時,ViewNameMethodReturnValueHandler將處理該值。但是當你從login()方法返回一個準備好的ModelAndView時,Spring使用了ModelAndViewMethodReturnValueHandler

渲染檢視

到目前為止,Spring已經處理了HTTP請求並收到了一個ModelAndView物件,因此它必須呈現使用者將在瀏覽器中看到的HTML頁面。它基於模型和封裝在ModelAndView物件中的選定檢視來實現。

另請注意,您可以呈現JSON物件,XML或可通過HTTP協議傳輸的任何其他資料格式。我們將在接下來的REST重點部分中詳細介紹。

讓我們回到DispatcherServlet。的渲染()方法首先將使用所提供的響應區域的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的情況下,包括我們ThymeleafViewResolver由提供thymeleaf-spring5整合庫。此解析器知道在何處搜尋檢視,並提供相應的檢視例項。

在呼叫檢視的render()方法之後,Spring最終通過將HTML頁面傳送到使用者的瀏覽器來完成請求處理:

REST支援

除了典型的MVC場景之外,我們還可以使用該框架來建立REST Web服務。

簡單地說,您可以接受資源作為輸入,將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協議的棘手部分。