首頁»Java WEB»Spring MVC 到底是如何工作的?

Spring MVC 到底是如何工作的?

來源:codeceo 發布時間:2017-12-04 閱讀次數:

  這篇文章將深入探討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協議難題。 

  英文原文:How Spring MVC Really Works

QQ群:WEB開發者官方群(515171538),驗證消息:10000
微信群:加小編微信 849023636 邀請您加入,驗證消息:10000
提示:更多精彩內容關注微信公眾號:全棧開發者中心(fsder-com)
網友評論(共1條評論) 正在載入評論......
理智評論文明上網,拒絕惡意謾罵 發表評論 / 共1條評論
登錄會員中心
李逵劈鱼9900炮 新强时时三星走势图 香港9047开奖现场 pk赛车官网开结果 荷甲在欧洲俱乐部排名 时时走势图老时时360 平特独平一码 快乐12推算软件 新疆时时三星综合走势图 幸运分分彩全计划安卓 江苏快3三不同推荐