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有很長的繼承層次; 從上到下逐一理解這些個別方面是值得的。請求處理方法最讓我們感興趣。
在標準開發期間以及遠端理解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()方法實現的抽象類,它通過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.xml或WebApplicationInitializer接收的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方法的引數和返回值
請注意,控制器方法通常不接受HttpServletRequest和HttpServletResponse引數,而是接收和返回許多不同型別的資料,例如域物件,路徑引數等。
另請注意,您不需要從控制器方法返回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協議的棘手部分。