Spring基礎(四)

16. Web MVC 框架

16.1 Spring Web MVC 框架介紹

Spring Web 模型-視圖-控制器(MVC) 框架是圍繞 DispatcherServlet而設計的,其支持可配置的 handler 映射,視圖解析,本地化、時區和主題的解析以及文件上傳的功能。DispatcherServlet 負責將請求分發到不同的 handler。默認的 handler 通過@Controller 和 @RequestMapping注解,提供多種靈活的處理方法。若加上 @PathVariable 注解和其他輔助功能,你也可用使用 @Controller 機制來創建 RESTful web 站點和應用程序。
用 Spring Web MVC ,你不需要實現框架指定的任何接口或繼承任意基類,就可以使用任意對象作為命令對象(或表單對象)。Spring 的數據綁定相當之靈活,比如,Spring可以將不匹配的類型作為應用可識別的驗證錯誤,而不是系統錯誤,所以,你不需要去重復定義一套屬性一致而類型是原始字符串的業務邏輯對象,去處理錯誤的提交或對字符串進行類型轉換。反過來說就是,spring 允許你直接將正確類型的參數綁定到業務邏輯對象。

Spring 的視圖解析也相當之靈活。完成一個請求,Controller 通常是負責準備一個數據模型 Map 和選擇一個指定的視圖,當然,也支持直接將數據寫到響應流里。視圖名稱的解析是高度可配置的,可以通過文件擴展名、accept header 的 Content-Type、bean 的名稱、屬性文件或自定義的 ViewResolver 實現來解析。模型(Model,MVC 中的 M),是一個 Map 接口,提供對視圖數據的完全抽象,可直接與渲染模版集成,如 JSP,Veloctiy,Freemarker;或直接生成原始數據,或xml、json等其他類型的響應內容。模型 Map 接口只是負責將數據轉換為合適格式,如 jsp 請求屬性,velocity 的 model 等。

16.2 The DispatcherServlet

像其他 web MVC 框架一樣, Spring web MVC 框架也是基于請求驅動,圍繞一個核心 Servlet 轉發請求到對應的 Controller 而設計的,提供對web 程序開發的基礎的支持。然而 Spring 的 DispatcherServlet 并不僅僅擁有這些,因為 Spring MVC 框架集成了 Spring IOC 容器,因此,Spring MVC 可以使用 Spring 提供的其他功能。

Spring Web MVC 請求處理的宏觀圖

DispatcherServlet 繼承了 HttpServlet ,是一個真實的 Servlet,因此可以在 web.xml 文件聲明。另外你需要使用 url 匹配元件指定 DispatcherServlet 處理的請求。如下例子,使用了標準 java EE Servlet 配置,配置了一個 DispatcherServlet的聲明和匹配 url 元件:

<web-app>
    <servlet>
        <servlet-name>example</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>example</servlet-name>
        <url-pattern>/example/*</url-pattern>
    </servlet-mapping>

</web-app>

在剛才配置的例子中,所有以 /example 開始的請求都會被名為 example 的 DispatcherServlet 所處理。在 Servlet 3.0+ 環境,也可以以編程方式配置上述 DispatcherServlet。如下代碼與上述 web.xml 配置例子等效:

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {
        ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet());
        registration.setLoadOnStartup(1);
        registration.addMapping("/example/*");
    }

}

上述的操作僅僅是開啟了 Spring Web MVC 之旅的第一步,現在你需要配置 Spring Web MVC 所使用到的各種 bean(這不在本節討論范圍)。
在Spring里可以獲取到 ApplicationContext 實例,在 web MVC 框架,每一個 DispatcherServlet 都擁有自己的 WebApplicationContext,這個 WebApplicationContext 繼承了根 WebApplicationContext 定義的所有 bean.
DispatcherServlet 在初始化時,Spring MVC 會查找 web 應用 WEB-INF 目錄下的[servlet-name]-servlet.xml 并創建在此文件定義的 bean,若在全局范圍里有一個名稱相同的 bean,全局范圍的 bean 會被覆蓋掉。

<web-app>
    <servlet>
        <servlet-name>golfing</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>golfing</servlet-name>
        <url-pattern>/golfing/*</url-pattern>
    </servlet-mapping>
</web-app>

上述配置,要求應用程序在 WEB-INF 目錄下有一個 golfing-servlet.xml 文件,在這個文件里,會包含 Spring MVC 的所有組件(beans)。你可以通過定義 servlet 初始化參數來改變[servlet-name]-servlet.xml 文件的路徑,如下:

<web-app>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

6.2.1 WebApplicationContext 的專用 bean

DispatcherServlet 使用了其專用的 bean 來處理請求和渲染視圖。這些 bean 是 Spring 的組成部分之一,你可以選擇在 WebApplicationContext配置所使用一個或多個專用的bean。當然,你并不需要一開始就去配置這些專用的 bean,因為在你不配置這些 bean時,Spring 會維護一系列默認的 bean。首先我們看一下 DispatcherServlet 依賴了哪些專用的 bean,后續再作詳解。

Bean 類型 解釋
HandlerMapping 將傳入的請求映射到處理器,與一系列基于各種條件的 pre- 和 post- 處理器,這些處理器根據 HandlerMapping 實現的不同而會有所差異。
HandlerAdapter 幫助 DispatcherServlet 去調用請求所映射的 handler,不管hadler 最終是否會被調用,這個處理過程都會存在的。比如,調用注解控制器前需要解析各種 annotations。因此,HandlerAdapter 的主要目的就是從 DispatcherServlet 中屏蔽這些處理細節。
HandlerExceptionResolver 將異常映射到指定視圖,也支持自定義更加復雜的異常處理流程
ViewResolver 將合理的視圖名稱解釋為真實的視圖類型
ThemeResolver 解釋 web 程序可用的主題,比如,提供個性化的布局
MultipartResolver 解釋 multi-part 請求,比如,在 html form 里支持文件上傳
16.2.2 默認的 DispatcherServlet 配置

如上一節所說,每一個 DispatcherServlet 都維持了一系列默認的實現。這些默認實現的信息保存在 org.springframework.web.servlet 包里的 DispatcherServlet.properties 文件。

盡管所有專用的 bean 都有其合理的默認值。遲早你也需要根據實際去自定義這些 bean 的中一個或多個屬性值。例如一種很常見的自定義應用,配置一個 InternalResourceViewResolver,其 prefix 為視圖文件的父文件夾。
不管這些默認細節如何實現,在這里都需要清楚一個概念——一旦在 WebApplicationContext 配置自己專用的 bean,就有效覆蓋了原有一系列默認的實現,至少也會作為這個專用 bean 的一個實例。

16.2.3 DispatcherServlet 處理順序

在你建立一個 DispatcherServlet 之后,并處理一個傳進來的請求時,DispatcherServlet 會按照以下順序年來處理這個請求:

  • 尋找 WebApplicationContext,并將 WebApplicationContext作為一個屬性綁定到請求里,以便控制器或其他原件在后續中使用。默認會以 DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE 鍵綁定到請求里。
  • 將本地化解析器綁定到請求里,以便在處理這個請求時,原件可以解析到客戶端的地區(為了渲染視圖,準備日期等)。如果你不需要本地化解析器,可以忽略這個步驟。
  • 將主題解析其綁定到請求里,讓原件(如視圖)決定去使用哪一種主題。如果你不需要使用主題,可以忽略這個步驟。
  • 如果你指定一個 multipart file 解析器,會檢查這個請求包含 multiparts 請求。當發現了 multiparts,這個請求會被封裝為 MultipartHttpServletRequest 對象,提供給后續原件處理。
  • 尋找合適的 handler。如何找到這個 handler,執行與這個 handler 關聯的執行鏈,目的是準備一個 model 或 渲染。
  • 如果返回一個 model,渲染相對應的視圖。反之(可能是因為 pre- 或 post- 處理器攔截了這個請求,也可能是權限問題),便不渲染任何視圖,因為這個請求可能已執行完成。
    handler 異常解析是在 WebApplicationContext 聲明的,接收在上述處理過程拋出的異常。使用異常解析器,你可以根據異常信息自定義其處理方式。

16.3 實現控制器(Controller)邏輯

Controller層面主要是控制web請求的路由和視圖解析,Spring對于Controller的支持是十分完善的,我們在定義路由邏輯的時候并要求我們繼承和實現某個類,只需要添加Spring的注解信息就可以做到。下面我么你先看一個簡單的Controller定義邏輯:

@Controller
public class HelloWorldController {

    @RequestMapping("/helloWorld")
    public String helloWorld(Model model) {
        model.addAttribute("message", "Hello World!");
        return "helloWorld";
    }
}

如你所見,@Controller 和 @RequestMapping 允許靈活的配置方法簽名。在上述例子中,helloWorld 方法接受一個 Model 參數,并返回一個視圖名稱,當然也允許添加方法入參和返回不同類型的值,這些內容將會在后面解釋。@Controller 、@RequestMapping 和其他一些功能注解組成了 Spring MVC 實現的基礎,這一節將會談到這些組成的注解和在 Servlet 環境的普遍用法。

16.3.1 使用 @Controller 定義控制器

@Controller 表明了被注解類的服務角色——控制器。Spring 不需要去繼承任何 Controller 的基類或引用任意的 Servlet API。當然了,如何你需要的, 你仍然可以引用 Servlet API。

@Controller 注解定義了被注解類的原型,表明了注解類的服務角色。dispatcher 會掃描這些被 @Controller 標記的類并檢測 @RequestMapping 標記的方法(見下一節)。

你可以在 dispatcher 上下文顯式定義控制器 bean,不過,為了與 Spring 支持在類路徑上檢測 bean 并自動注冊這些 bean 定義 保持一致,@Controller 也許允許自動檢測。

要開啟注解控制器的掃描功能,需要在你的配置里添加組件掃描元件。如下 xml 所示,可以使用 spring-context 模式開啟此掃描功能:

    <context:component-scan base-package="org.springframework.samples.petclinic.web"/>

6.3.2 使用 @RequestMapping 映射請求

你可以在類或指定 handler 方法上,使用 @RequestMapping 注解來映射 URL,如 /appointments。定義在類上以為這該類下的所有的方法級別的@RequestMapping將以類上定義的路徑為基礎path前綴,如果類上沒有定義的話,將直接使用方法級別的value值作為匹配的path。

@Controller
@RequestMapping("/appointments")
public class AppointmentsController {

    private final AppointmentBook appointmentBook;

    @Autowired
    public AppointmentsController(AppointmentBook appointmentBook) {
        this.appointmentBook = appointmentBook;
    }

    @RequestMapping(method = RequestMethod.GET)
    public Map<String, Appointment> get() {
        return appointmentBook.getAppointmentsForToday();
    }

    @RequestMapping(value="/new", method = RequestMethod.GET)
    public AppointmentForm getNewForm() {
        return new AppointmentForm();
    }
}

例子中,在多處地方使用 @RequestMapping。第一個用在了類上,表示@RequestMapping 這個控制器下的所有 handler 方法都是相對 /appointments 路徑而言的。get() 方法對 @RequestMapping 做了進一步的細化 —— 此方法只接收 GET 請求方式(@RequestMapping 默認匹配所有的 http 方法),換句話說就是 /appointments 的GET 請求會調用這個方法; add() 方法也做一個類似的細化; getNewForm() 方法在 RequestMapping 上組合定義了 http 方法和路徑,因此此方法會處理 appointments/new 的 GET 請求。

URI 模版模式

URI 模版是一個類似于 URI 的字符串,其中包含了一個或多個變量。當你將這些變量替換掉市,就變回了 URI可在方法入參上使用注解 @PathVariable 綁定 URI 的模版參數:

@RequestMapping(value="/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable String ownerId, Model model) {
    Owner owner = ownerService.findOwner(ownerId);
    model.addAttribute("owner", owner);
    return "displayOwner";
}

URI 模版 " /owners/{ownerId}" 指定了參數 owernId。當控制器處理這個請求時,會將 URI 中匹配的部分賦值給 owernId 變量。如,當傳入 /owners/fred 請求時,owernId 的值就是 fred。
在處理 @PathVariable 注解時,Srping MVC 是根據名稱來匹配 URI 模版變量的。你可以在注解里指定這個名稱:

@RequestMapping(value="/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable("ownerId") String theOwner, Model model) {
    // implementation omitted
}

如果URI 模版變量名和入參名一致,可以省略這個細節。只要你的代碼不是不帶調試信息的編譯,Spring MVC 將匹配入參名和 URI 變量名。

一個方法可以有任意個 @PathVariable 注解。

@RequestMapping(value="/owners/{ownerId}/pets/{petId}", method=RequestMethod.GET)
public String findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
    Owner owner = ownerService.findOwner(ownerId);
    Pet pet = owner.getPet(petId);
    model.addAttribute("pet", pet);
    return "displayPet";
}

URI 模版可以組合類型和參數路徑的 @RequestMapping。因此,findPet 可以處理類似 /owners/42/pets/21 的URI 。

@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {

    @RequestMapping("/pets/{petId}")
    public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
        // implementation omitted
    }

}

@PathVariable 參數可以是任意的簡單類型(如 int,long,Date 等),Spring 會自動將其進行類型轉換,轉換出錯會拋出 TypeMismatchException。你也可以注冊支持解析其他數據類型,這個內容后面會涉及到。

在 URI 模版上使用正則表達式

偶爾,在URI 模版變量里,你會需要用到更加精確的控制。比如 "/spring-web/spring-web-3.0.5.jar" 這樣的URI,該如何拆分成多個部分?

@RequestMapping 注解支持在 URI 模版變量里使用正則表達式。語法 {變量名:正則表達式},第一個部分定義變量的名稱,第二部分是正則表達式。如

@RequestMapping("/spring-web/{symbolicName:[a-z-]}-{version:\\d\\.\\d\\.\\d}{extension:\\.[a-z]}")
    public void handle(@PathVariable String version, @PathVariable String extension) {
        // ...
    }
}
路徑模式比較

當一個 URL 與多個模式匹配時,會設法找出最具體的那一個路徑。

當模式中的 URI 模版變量和通配符的數量相對較少,會認為其相對具體。如:/hotels/{hotel}/* 相對 /hotels/{hotel}/** 更加合適,因為 /hotels/{hotel}/* 只有一個URI 模版變量和一個通配符,而 hotels/{hotel}/**` 有一個 URI 模版變量和兩個通配符。

當兩個模式中的 URI 模版變量和通配符數量相同時,更詳細的那一個會認為相對適合。如 /foo/bar* 比 /foo/* 更為詳細。

一些額外的特別規定:

  • 任意模式都比默認全匹配 /** 模式具體。如:/api/{a}/{b}/{c} 比 /** 更加具體。
  • 任意不包含兩個通配符的模式都比前綴模式(如 /public/) 更加具體。/public/path3/{a}/{b}/{c} 比 /public/ 更加具體。
路徑模式的后綴匹配

Spring MVC 默認自動執行 "." 的后綴匹配,所以當一個控制器匹配 /person 時,其也隱式匹配 /person.。這樣的設計允許通過文件擴展名來說明內容的類型名比如 /person.pdf, /person.xml 等。然而,這里會有一個常犯的陷阱,當路徑最后的片段是 URI 模版變量時(如 /person/{id}),請求 /person/1.json 可以正確匹配路徑,變量 id=1,拓展名為 json,可當 id 自身包含 . (如 /person/joe@email.com),那匹配結果就不是我們所期望的,顯然 ".com" 不是文件擴展名。

解決這個問題的正確方法是配置 Spring MVC 只對注冊的文件擴展名做后綴匹配,這要求內容(擴展名)協商好。

矩陣變量

矩陣變量是我之前完全沒有接觸過的一種內容,后來讀了文檔之后才知道竟然還有這種操作,這里也特別記錄一下矩陣變量。
矩陣變量可以出現在任何path上面,矩陣變量之間通過";"(英文分號)來區分,舉個例子"/cars;color=red;year=2012",矩陣變量如果有多個值的話,值之間可以通過,(英文逗號)來分割,例如"color=red,green,blue"。
如果一個URL中出現矩陣變量的話,那么請求映射模式必須用URI模板表示它們。這確保了無論矩陣變量是否存在,以及它們以什么順序被提供,請求都能被正確匹配。

// GET /pets/42;q=11;r=22

@RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET)
public void findPet(@PathVariable String petId, @MatrixVariable int q) {

    // petId == 42
    // q == 11

}

由于所有路徑段都可能包含矩陣變量,因此在某些情況下,您需要更具體地確定變量的預期位置:

// GET /owners/42;q=11/pets/21;q=22

@RequestMapping(value = "/owners/{ownerId}/pets/{petId}", method = RequestMethod.GET)
public void findPet(
        @MatrixVariable(value="q", pathVar="ownerId") int q1,
        @MatrixVariable(value="q", pathVar="petId") int q2) {

    // q1 == 11
    // q2 == 22

}
-----------------------------------------
// GET /pets/42

@RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET)
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {

    // q == 1

}
------------------------------------------
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@RequestMapping(value = "/owners/{ownerId}/pets/{petId}", method = RequestMethod.GET)
public void findPet(
        @MatrixVariable Map<String, String> matrixVars,
        @MatrixVariable(pathVar="petId"") Map<String, String> petMatrixVars) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 11, "s" : 23]

}

默認情況下矩陣變量是不被spring激活的,如果你想使用矩陣變量的話,你需要在配置Spring的時候激活一下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven enable-matrix-variables="true"/>

</beans>
可消費的媒體類型

你可以指定一系列可消費的媒體類型來壓縮主要映射。這樣只用當 Content-Type 請求頭匹配可消費的媒體類型,才認為這個請求是可映射的。如:

@Controller
@RequestMapping(value = "/pets", method = RequestMethod.POST, consumes="application/json")
public void addPet(@RequestBody Pet pet, Model model) {
    // 實現省略
}
請求參數和頭字段值

你可以通過請求參數條件來壓縮請求匹配范圍,如使用 "myParam", "!myParam", 或 "myParam=myValue"。前兩種情況表示 存在/不存在,第三種指定了參數值。如下給出指定參數值的例子:

@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {

    @RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET, params="myParam=myValue")
    public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
        // 省略實現
    }

}

類似的,頭字段也支持 存在/不存在 和基于指定頭字段值的匹配:

@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {

    @RequestMapping(value = "/pets", method = RequestMethod.GET, headers="myHeader=myValue")
    public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
        // 省略實現
    }

}
16.3.3 定義 @RequestMapping 處理方法

@RequestMapping 處理方法允許非常靈活的簽名,其支持方法參數和返回值(在這一節談到)。除了 BindingResult 參數,其他類型參數順序隨意.
如下是可以支持的方法參數:

  • Request 或 response 對象 (Servlet API). 選擇任意指定的 request 或 response 類型,如ServletRequest o或 HttpServletRequest.
  • Session 對象 (Servlet API):需要是 HttpSession 類型. 這種類型的參數會強制合適 session 的存在。因此,這個參數永遠不會為 null。
  • org.springframework.web.context.request.WebRequest 或 org.springframework.web.context.request.NativeWebRequest.允許通過請求參數訪問和 request/session 屬性訪問
  • java.util.Locale 給當前請求本地化,取決于最具體的本地化解析器,實際上取決與是 Servlet 環境配置的 LocaleResolver 。
  • java.io.InputStream / java.io.Reader 可訪問請求的內容。這是 Servlet API 暴露的原生 InputStream/Reader 。
  • java.io.OutputStream / java.io.Writer 用于 產生 response 的內容。這是 Servlet API 暴露的原生 OutputStream/Writer.
  • org.springframework.http.HttpMethod 可訪問 HTTP 請求方法。
  • @PathVariable 注解參數,可訪問 URI 模版變量。
  • @MatrixVariable 矩陣變量
  • @RequestParam 注解參數,可訪問指定 Servlet request 參數。參數值會被轉換為方法參數的類型。
  • @RequestHeader 注解參數,可訪問指定 Servlet request 的 HTTP 頭字段。參數值會被轉換為方法參數的類型。
  • @RequestBody 注解參數,可訪問 HTTP 請求體。參數值使用 HttpMessageConverter 轉換為方法參數類型
  • @RequestPart 注解參數,可訪問 "multipart/form-data" 請求的內容。
  • HttpEntity<?> 參數,可訪問 Servlet request 的HTTP 頭和內容。

以下是可支持的返回類型

  • 一個ModelAndView對象,該模型隱式地豐富了命令對象和@ModelAttribute注解引用數據訪問器方法的結果
  • 一個Model對象,其中view的名字是通過RequestToViewNameTranslator隱式聲明的,該模型隱式地豐富了命令對象和@ModelAttribute注解引用數據訪問器方法的結果
  • 一個Map對象,內容跟model對象基本一致。
  • 一個View對象,其中model的信息隱式地豐富了命令對象和@ModelAttribute注解引用數據訪問器方法的結果。
  • 一個String對象,內容基本跟View對象的內容差不多。
  • 返回void,如果方法內容已經自己處理了response內容或者想依賴RequestToViewNameTranslator來生成對應的view名稱的話,這種情況下可以返回void
  • 如果方法本身被@ResponseBody注釋了,這時候返回的類型會被直接寫入到響應體里面,這時候返回信息會根據方法聲明的類型,通過HttpMessageConverter來進行轉換。
  • 一個HttpHeaders對象,可以通過此對象拿到響應頭信息。
  • 一個Callable<?>對象,一個異步結果信息。
使用 @RequestParam 將請求參數綁定到方法參數

在控制器里,使用 @RequestParam 將請求參數綁定到方法參數。

@Controller
@RequestMapping("/pets")
public class EditPetForm {
    @RequestMapping(method = RequestMethod.GET)
    public String setupForm(@RequestParam("petId") int petId, ModelMap model) {
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }

}

使用 @RequestParam 的參數默認是必須提供的,當然,你可以指定其為可選的,將 @RequestParam 的 reqired 屬性設置 false 即可。(如, @RequestParam(value="id", required=false)).

如果方法參數的類型不是 String,類型轉換會自動執行,如果將 @RequestParam 用于 Map<String, String> 或 MultiValueMap<String, String> 參數,此參數 map 會填充所有的請求參數。

使用 @RequestBody 映射請求體

@RequestBody 注解參數表示該參數將與 HTTP 請求體綁定。例子:

@RequestMapping(value = "/something", method = RequestMethod.PUT)
public void handle(@RequestBody String body, Writer writer) throws IOException {
    writer.write(body);
}

@RequestBody 方法參數可添加 @Valid 注解,被注解的參數會使用配置的 Validator 來驗證。當使用 MVC 命名空間或 mvc Java 配置時,應用會自動配置 JSR-303 驗證器(前提是在類路徑能找到 JSR-303 的實現)。

類似于 @ModelAttribute 參數,Errors 參數也可用來檢測錯誤。當 Errore 參數沒有聲明時,或拋出 MethodArgumentNotValidException。此異常會被 DefaultHandlerExceptionResolver 處理 —— 向客戶端發送 400 錯誤。

使用 @ResponseBody 映射響應體

@ResponseBody 的使用類似于 @RequestBody。此注解用在方法上,用來表示直接將返回數據寫到 HTTP 響應體里。注意,不是將數據放到 Model 中,或解析為視圖名稱。例子:

@RequestMapping(value = "/something", method = RequestMethod.PUT)
@ResponseBody
public String helloWorld() {
    return "Hello World";
}

上述例子會將 Hello World 文本寫到 HTTP 響應流中。

使用 @RestController 創建 REST 控制器

一種比較常見的場景,控制器實現 REST API,只會返回 JSON、XML 或其他自定義媒體類型。為了方便,你可以在控制器上添加 @RestController 注解,而不是在每一個 @RequestMapping 上使用 @ResponseBody。

@RestController 是一個結合了 @ResponseBody 和 @Controller 的注解。不僅如此,@RestController 賦予了控制器更多的意義,在未來的版本中可能會攜帶額外的語義。。

使用 HttpEntity

HttpEntity 的用法類似于 @RequestBody 和 @ResponseBody 注解。除了可以訪問請求/響應體,HttpEntity(和特用與響應的子類 ResponseEntity) 還可以訪問 request 和 response 的頭字段。例子:

@RequestMapping("/something")
public ResponseEntity<String> handle(HttpEntity<byte[]> requestEntity) throws UnsupportedEncodingException {
    String requestHeader = requestEntity.getHeaders().getFirst("MyRequestHeader"));
    byte[] requestBody = requestEntity.getBody();

    // do something with request header and body

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("MyResponseHeader", "MyValue");
    return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED);
}

上述例子獲取了 MyRequestHeader 頭字段的值,以字節數組的形式讀取了請求體,隨后將 MyRequestHeader 添加到 response,將 Hello World 寫到響應流和設置響應狀態碼為 201(Created).

在方法上使用 @ModelAttribute

@ModelAttribute 可用欲方法或方法參數中。這一部分將介紹 @ModelAttribute 在方法中的使用,下一部分介紹其在方法啊參數中的使用。

在方法上使用 @ModelAttribute 注解,表示此方法的目的在于添加一個或多個模型屬性。這種方法所支持的參數類型與 @RequestMapping 一樣,不同的是,其不能直接映射到 request。另外,在同一個控制器里,@ModelAttribute 會在 @RequestMapping 之前調用。

// 添加一個屬性
// 方法的返回值會以 "account" 鍵添加到 model
// 可通過 @ModelAttribute("myAccount") 自定義

@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountManager.findAccount(number);
}

// 添加多個屬性

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountManager.findAccount(number));
    // 再添加多個……
}

第一種,在方法里隱式添加一個屬性并返回;第二種,方法里接收 Model 參數,并將任意個屬性添加到 Model中。一個控制器可以有多個 @ModelAttribute 方法。在同一個控制器中,所有 @ModelAttribute 方法都會在 @RequestMapping 方法之前調用。

@ModelAttribute 注解也可用在 @RequestMapping 方法中。這種情況下,@RequestMapping 方法的返回值將解析為模型屬性,而不是視圖名稱。相反,視圖名稱來源于視圖名稱的約定,就類似于方法返回 void

指定 redirect 和 flash 屬性

在重定向 URL 中,所有模型屬性默認暴露給 URI 模版變量,剩下的屬性(原始類型或原始類型集合/數組)會自動拼接到查詢參數中。

然而,在一個帶注解的控制器中,模型也許包含了額外的屬性(用于渲染,如下拉框屬性)。在重定向場景中,要準確控制這些屬性,可在 @RequestMapping 方法中聲明 RedirectAttributes 類型參數,并往其添加 RedirectView 使用的屬性。如果這個控制方法的確發生重定向,將使用 RedirectAttributes 的內容,否則使用默認 Model 的內容。

RequestMappingHandlerAdapter 提供了一個 "ignoreDefaultModelOnRedirect" 標志,用來設置在控制方法重定向時,默認Model 的內容是否從不使用。相反,控制器方法應該聲明 RedirectAttributes 類型屬性,否則會沒有任何屬性傳遞給 RedirectView。為了向后兼容,MVC 命名空間和 MVC Java 配置都將 "ignoreDefaultModelOnRedirect" 設置為 false。可我們還是建議你在新應用里將其設置為 true。

RedirectAttributes 接口也可以用來添加 flash 屬性。與其他重定向屬性(在重定向 URL 中銷毀)不同的是,flash 屬性會保存到 HTTP session(因此 flash 屬性也不會在 URL 上出現)。作用于重定向 URL 的控制器里的模型會自動接收這些 flash 屬性,之后,flash 屬性會從 session 中移除。

使用 @CookieValue 映射 cookie 值

@CookieValue 注解允許將方法參數與HTTP cookie 值綁定。

@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@CookieValue("JSESSIONID") String cookie) {
    //...
}

如果方法參數不是 String 類型,類型轉換會自動執行

使用 @RequestHeader 映射請求頭字段屬性

@RequestHeader 注解允許將方法參數與請求頭字段綁定。

如下一個請求頭字段值的樣例:

Host localhost:8080
Accept text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding gzip,deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive 300
如下代碼演示了如何獲取 Accept-Encoding 和 Keep-Alive 頭字段值:

@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@RequestHeader("Accept-Encoding") String encoding,
        @RequestHeader("Keep-Alive") long keepAlive) {
    //...
}
使用 @ControllerAdvice 注解增強控制器

@ControllerAdvice 注解可以讓實現類通過類路徑自動檢測出來。當使用 MVC 命名空間或 MVC Java 配置時,此此功能是默認啟動的。

帶有 @ControllerAdvice 注解的類,可以包含 @ExceptionHandler、@InitBinder, 和 @ModelAttribute 注解的方法,并且這些注解的方法會通過控制器層次應用到所有 @RequestMapping 方法中,而不用一一在控制器內部聲明。

// 應用到所有 @RestController 控制器
@ControllerAdvice(annotations = RestController.class)
public class AnnotationAdvice {}

// 應用到指定包下的控制器
@ControllerAdvice("org.example.controllers")
public class BasePackageAdvice {}

// 應用到指定類型的控制器
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class AssignableTypesAdvice {}
16.3.4 異步請求處理

Spring MVC 引入了基于異步請求的 Servlet 3。在異步請求中,控制器方法通常會返回 java.util.concurrent.Callable 對象后再使用一個獨立的線程產生返回值,而不是直接返回一個值。同時釋放 Servlet 容器的主線程和允許處理其他請求。Spring MVC 借助 TaskExecutor ,在一個獨立線程中調用 Callable,當 Callable 返回時,將請求轉發到 Servlet 容器并繼續處理 Callable 返回值。例子如下:

@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {

    return new Callable<String>() {
        public String call() throws Exception {
            // ...
            return "someView";
        }
    };

}
16.3.4 異步請求處理

Spring MVC 引入了基于異步請求的 Servlet 3。在異步請求中,控制器方法通常會返回 java.util.concurrent.Callable 對象后再使用一個獨立的線程產生返回值,而不是直接返回一個值。同時釋放 Servlet 容器的主線程和允許處理其他請求。Spring MVC 借助 TaskExecutor ,在一個獨立線程中調用 Callable,當 Callable 返回時,將請求轉發到 Servlet 容器并繼續處理 Callable 返回值。例子如下:

@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {

    return new Callable<String>() {
        public String call() throws Exception {
            // ...
            return "someView";
        }
    };

}

異步請求的另外一種方式,是讓控制器返回 DeferredResult 實例。這種情況下,依然是從一個獨立線程處理并產生返回值。然而,Spring MVC 并不知曉這個線程的后續處理。比如說,這個返回結果可以用來響應某些外部事件(如 JMS 信息,計劃任務等)。例子如下:

@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    // 將 deferredResult 保存到內存隊列
    return deferredResult;
}

// 在其他線程中...
deferredResult.setResult(data);

如果不了解 Servlet 3 異步處理的細節,理解起來可能會一定的難度.

  • 一個 ServletRequest 請求可通過調用 request.startAsync() 方法設置為異步模式。此步驟最主要的作用是,在此 Servlet 和其他過濾器退出的情況下,response 依然可以保持打開狀態,以便其他線程來完成處理。
  • 調用 request.startAsync() 方法返回一個 AsyncContext。在異步處理中,AsyncContext 可以用來做進一步的控制。比如說,AsyncContext 提供的 dispatch 方法,可以在應用線程中調用,將請求轉發回 Servlet 容器。異步 dispatch(轉發)類似于平時使用的 forward 方法。不同的是,異步 dispatch(轉發)是從應用里的一個線程轉發到 Servlet 容器中的另一個線程,而 forward 方法則是在 Servlet 容器里的同一個線程間轉發。
  • ServletRequest 可以定位當前的 DispatcherType(轉發類型),此功能可以用于判斷 Servlet 或 Filter 是在原始請求線程上處理請求,還是在異步轉發線程中處理。

記住以上事實之后,接著了解一下異步請求處理 Callable 的過程:(1) 控制器返回一個 Callable ,(2) Spring MVC 開始異步處理,將 Callable 提交給 TaskExecutor,TaskExecutor 在一個獨立線程中處理,(3) DispatcherServlet 和所有過濾器退出請求處理線程,不過保持 response 為打開狀態,(4) Callable 產生一個結果之后,Spring MVC 將這個請求轉發回 Servlet 容器,(5) 再次調用 DispatcherServlet,并重新處理 Callable 異步產生的結果。(2),(3),(4) 的準確順序在不同情況下可能有所不同,這個取決于并發線程的處理速度。

異步請求處理 DeferredResult 的事件順序大體上和處理 Callable 的順序相同。不同的是,這里是由應用程序的某些線程來處理異步結果:(1) 控制器返回一個 DeferredResult 對象,并將其保存到可訪問的內存隊列或列表中,(2) Spring MVC 開始異步處理,(3) DispatcherServlet 和所有過濾器退出請求處理線程,不過保持 response 為打開狀態,(4) 應用程序在某些線程中設置 DeferredResult,之后 Spring MVC 將這個請求轉發回 Servlet 容器,(5) 再次調用 DispatcherServlet,并重新處理異步產生的結果

當控制器返回的 Callable 在執行時反生了異常,會出現什么情況?這種情況類似于控制器發生異常時的情況。所出現的異常會由同一控制器里的 @ExceptionHandler 方法處理,或由所配置的 HandlerExceptionResolver 實例來處理。如果是執行 DeferredResult 時出現異常,你可以選擇調用 DeferredResult 提供的 setErrorResult(Object) 方法,該方法須提供一個異常或其他你設置設置的對象。 當結果是一個 Exception 時,會由同一控制器里的 @ExceptionHandler 方法處理,或由所配置的 HandlerExceptionResolver 實例來處理。

16.4 Handler 映射

16.4.1 使用 HandlerInterceptor 攔截請求

Spring 的 handler 映射機制包含了 handler 攔截器。使用handler 攔截器,可以在某些的請求中應用的特殊的功能,比如說,檢查權限。

handler 映射的攔截器必須實現 HandlerInterceptor 接口(此節接口位于 org.springframework .web.servlet 包中)。這個接口定義了三個方法:preHandle(..) 在 handler 執行前調用;postHandle(..) 在handler 執行后調用;afterCompletion(..) 在整一個請求完成后調用。這三個方法基本足夠應對各種預處理和后處理的狀況。

preHandle(..) 方法返回一個 boolean 值。你可以使用這個方法來中斷或繼續處理 handler 執行鏈。當此方法返回 true 時,hadler 執行鏈會繼續執行;反之,DispatcherServlet 會認為此攔截器已處理完成該請求(和渲染一個視圖),之后不再執行余下的攔截器,也不在執行 handler 執行鏈。

可以使用 interceptors 屬性配置攔截器。所有從 AbstractHandlerMapping 繼承過來的 HandlerMapping 類都擁有此屬性。演示例子如下:

<beans>
    <bean id="handlerMapping"
            class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
        <property name="interceptors">
            <list>
                <ref bean="officeHoursInterceptor"/>
            </list>
        </property>
    </bean>

    <bean id="officeHoursInterceptor"
            class="samples.TimeBasedAccessInterceptor">
        <property name="openingTime" value="9"/>
        <property name="closingTime" value="18"/>
    </bean>
<beans>

public class TimeBasedAccessInterceptor extends HandlerInterceptorAdapter {

    private int openingTime;
    private int closingTime;

    public void setOpeningTime(int openingTime) {
        this.openingTime = openingTime;
    }

    public void setClosingTime(int closingTime) {
        this.closingTime = closingTime;
    }

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
            Object handler) throws Exception {
        Calendar cal = Calendar.getInstance();
        int hour = cal.get(HOUR_OF_DAY);
        if (openingTime <= hour && hour < closingTime) {
            return true;
        }
        response.sendRedirect("http://host.com/outsideOfficeHours.html");
        return false;
    }
}

注意,HandlerInterceptor 的 postHandle 方法不一定適用于@ResponseBody和ResponseEntity方法。在這種情況下,HttpMessageConverter 實例會在 postHandle 方法執行之前就將數據寫到 response 并提交 response,所以 postHandle 方法不可能再處理 response(如添加一個 Header)。相反,應用程序可以實現 ResponseBodyAdvice ,將其聲明為 @ControllerAdvice bean 或將其直接在 RequestMappingHandlerAdapter 中配置它。

16.5 視圖解析

所有 web 應用的 MVC 框架都會提了視圖解析的方案,Spring 提供的視圖解析,可以讓你在不指定特定視圖技術的前提下,便可在瀏覽器中渲染模型。Spring 支持使用 USP,Veloctiy 模板和 XSLT 視圖技術,這些視圖技術都是開箱即用的。查看Chapter 17, 視圖技術,可以了解到如何集成和使用多種不同的視圖技術。

ViewResolver 和 View 是 Spring 處理視圖的兩個重要接口。當中,ViewResolver 提供了視圖名稱和真實視圖之間的映射,View 則是負責解決某個視圖的技術的請求預處理和請求的后續處理。

16.5.1 使用 ViewResolver 接口解析視圖

Spring web MVC 中的所有 handler 方法都需要解析某一個邏輯視圖名稱,可以是顯式的,如如返回 String, View, 或 ModelAndView 實例,也可以是隱式的(這個需基于事先約定)。
舉個例子,解析 JSP 視圖技術,可以使用 UrlBasedViewResolver 解析器。此解析器會將視圖名稱轉換為 url,和傳遞請求到 RequestDispatcher,以便渲染視圖。

<bean id="viewResolver"
        class="org.springframework.web.servlet.view.UrlBasedViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>

當返回 test 邏輯邏輯視圖名時,此視圖解析器會將請求轉發到 RequestDispatcher,接著 RequestDispatcher 將請求發送到 /WEB-INF/jsp/test.jsp。

16.5.2 視圖解析器鏈

Spring 提供多種視圖技術。因此,你可以定義解析器鏈,比如,可在某些情況下覆蓋指定視圖。可通過在應用上下文中添加多個解析器來定義解析器鏈,如有需要的,也可指定這些解析器的順序。記住,order 屬性越高,解析器的鏈上位置約靠后。

如下例子,定義了包含兩個解析器的解析器鏈。當中一個是 InternalResourceViewResolver,此解析器總是自動定位到解析器鏈中最后一個;另外一個是 XmlViewResolver,用來指定 Excel 視圖。InternalResourceViewResolver 不支持 Excel 視圖。

<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>

<bean id="excelViewResolver" class="org.springframework.web.servlet.view.XmlViewResolver">
    <property name="order" value="1"/>
    <property name="location" value="/WEB-INF/views.xml"/>
</bean>


<beans>
    <bean name="report" class="org.springframework.example.ReportExcelView"/>
</beans>

如果一個視圖解析器不能導出一個視圖,Spring 會檢索上下文,查找其他視圖解析器。如果查找到其他視圖解析器,Spring 會繼續處理,直到有解析器導出一個視圖。如果沒有解析器返回一個視圖,Spring 會拋出 ServletException。

視圖解析協議規定視圖解析器可以返回 null,表示沒有找到指定的視圖。然而,不是所有的視圖解析器返回null,都表示沒有找到視圖。因為某些情況下,視圖解析器也無法檢測視圖是否存在。比如,InternalResourceViewResolver 在內部邏輯里使用 RequestDispatcher,如果 JSP 文件存在,那分發是唯一可以找到 JSP 文件的方式,可分發只能執行一次。VelocityViewResolver 和其他解析器也類似。

16.5.4 ContentNegotiatingViewResolver

ContentNegotiatingViewResolver 自身并沒有去解析視圖,而是將其委派給其他視圖解析器,選擇指定響應表述返回給客戶端。有以下兩種策略,允許客戶端請求指定表述方式的資源:

為了支持同一資源的多種表述,Spring 提供了 ContentNegotiatingViewResolver,可根據文件拓展名或 Accept 頭字段值選擇指定資源的表述.ContentNegotiatingViewResolver 自身并解釋視圖,而是將其轉發給通過 ViewResolvers 屬性配置的視圖解析器.
如下,是一個 ContentNegotiatingViewResolver 配置樣例:

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <property name="mediaTypes">
        <map>
            <entry key="atom" value="application/atom+xml"/>
            <entry key="html" value="text/html"/>
            <entry key="json" value="application/json"/>
        </map>
    </property>
    <property name="viewResolvers">
        <list>
            <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
            <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                <property name="prefix" value="/WEB-INF/jsp/"/>
                <property name="suffix" value=".jsp"/>
            </bean>
        </list>
    </property>
    <property name="defaultViews">
        <list>
            <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" />
        </list>
    </property>
</bean>

<bean id="content" class="com.foo.samples.rest.SampleContentAtomView"/>

InternalResourceViewResolver 處理試圖名稱的解析和 JSP 頁面,另外,BeanNameViewResolver會返回基于 bean 名稱的視圖(可參考 "使用 ViewResolver 解析視圖,了解 Spring 如何尋找和初始化一個視圖")。在上述例子中,content bean 是一個繼承 AbstractAtomFeedView 的類,這個bean 可以返回一個 RSS 原子.
在上述配置中,當請求是由 .html拓展名組成時,視圖解析器會去尋找一個匹配 text/html 的視圖。InternalResourceViewResolver 提供了 text/html 的映射。當請求是由 .atom 拓展名組成時,視圖解析器會去尋找一個匹配 .atom 的視圖。這個視圖 BeanNameViewResolver 有提供,若視圖名稱為 content,則映射到 SampleContentAtomView。當請求是由 .json拓展名組成時,會選擇 DefaultViews 提供的 MappingJackson2JsonView 接口,注意這個映射與視圖名稱無關。另外,客戶端的請求不帶拓展名,通過 Accept 頭字段指定媒體類型時,也會執行和文件拓展名一樣的處理邏輯。
可以根據 http://localhost/content.atomhttp://localhost/content和Accept字段值為 application/atom+xml 兩種情況,返回原子視圖的控制器代碼如下:

@Controller
public class ContentController {

    private List<SampleContent> contentList = new ArrayList<SampleContent>();

    @RequestMapping(value="/content", method=RequestMethod.GET)
    public ModelAndView getContent() {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("content");
        mav.addObject("sampleContentList", contentList);
        return mav;
    }

}

16.7 構建 URI

Spring MVC 提供了構建和編碼 URI 的機制,這種機制的使用需要通過 UriComponentsBuilder 和 UriComponents.

UriComponents uriComponents = UriComponentsBuilder.fromUriString(
        "http://example.com/hotels/{hotel}/bookings/{booking}").build();

URI uri = uriComponents.expand("42", "21").encode().toUri();

注意,UriComponents 是不可變的;如果有需要的,expand() 和 encode() 操作會返回一個新的實例。你可以單獨使用一個 URI 原件展開和編碼 URI 模版字符串:

UriComponents uriComponents = UriComponentsBuilder.newInstance()
        .scheme("http").host("example.com").path("/hotels/{hotel}/bookings/{booking}").build()
        .expand("42", "21")
        .encode();

Servlet 環境中,ServletUriComponentsBuilder 的子類提供了從 Servlet 請求復制 URL 信息的靜態方法:

HttpServletRequest request = ...
// 重用 host, scheme, port, path 和 query
// 替換 "accountId" 查詢參數
ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromRequest(request)
        .replaceQueryParam("accountId", "{id}").build()
        .expand("123")
        .encode();

16.10文件上傳

16.10.1文件上傳介紹

Spring本身也是支持在web模塊式使用文件上傳的,如果你想使用Spring的文件上傳模塊的話,就必須要自己生命一個處理文件上傳的MultipartResolver實現類,通常情況洗我們使用的是CommonsMultipartResolver。Spring會檢測每一個Http請求,檢測請求中是否包含文件上傳的內容,如果沒有文件上傳的內容被檢測到的話,Spring會照常處理這個請求,但是如果有上傳內容被檢測到的話,我們在Spring中聲明的MultipartResolver就會爬上用場了,我們在上傳文件時候附加的參數,可以像正常參數一樣被我們取到。

16.10.2通用的文件上傳邏輯
<bean id="multipartResolver"
        class="org.springframework.web.multipart.commons.CommonsMultipartResolver">

    <!-- 其中一個可以配置的屬性; 上傳文件的最大字節 -->
    <property name="maxUploadSize" value="100000"/>

</bean>

當然,為了multipart resolver 能夠正常運行,需要在類路徑添加一些jar包. 對于 CommonsMultipartResolver 而言, 你需要使用 commons-fileupload.jar.

當 Spring DispatcherServlet 檢測到一個 multi-part 請求時, 就激活在上下文定義的resolver 并移交請求. 然后,resolver 包裝當前 HttpServletRequest 成支持multipart文件上傳的 MultipartHttpServletRequest. 通過 MultipartHttpServletRequest, 你可以獲取當前請求所包含multiparts信息,實際上你也可以在controllers獲取多個multipart文件.

16.10.4 在表單中處理一個上傳文件

在完成添加 MultipartResolver 之后, 這個請求就會和普通請求一樣被處理. 首先, 創建一個帶上傳文件的表單. 設置( enctype="multipart/form-data") 告訴瀏覽器將表單編碼成 multipart request:

<html>
    <head>
        <title>Upload a file please</title>
    </head>
    <body>
        <h1>Please upload a file</h1>
        <form method="post" action="/form" enctype="multipart/form-data">
            <input type="text" name="name"/>
            <input type="file" name="file"/>
            <input type="submit"/>
        </form>
    </body>
</html>

下一步是創建一個 controller 處理上傳文件. 需要在請求參數中使用 MultipartHttpServletRequest 或者 MultipartFile, 這個 controller 和 normal annotated @Controller非常相似:

@Controller
public class FileUploadController {

    @RequestMapping(value = "/form", method = RequestMethod.POST)
    public String handleFormUpload(@RequestParam("name") String name,
            @RequestParam("file") MultipartFile file) {

        if (!file.isEmpty()) {
            byte[] bytes = file.getBytes();
            // 將bytes保存
            return "redirect:uploadSuccess";
        }

        return "redirect:uploadFailure";
    }

}

注意 @RequestParam 將方法參數映射輸入元素的聲明形式. 在這個例子中, 對 byte[] 并沒有做什么操作, 但是在實踐中你可以保存在數據庫, 存儲在文件系統, 等等.

16.11 異常處理

16.11.1 異常處理

Spring的 HandlerExceptionResolver 用來處理包括controller執行期間在內的異常. 一個 HandlerExceptionResolver 有點類似于 web 應用中定義在 web.xml 中的異常映射. 但是,它們提供了更加靈活的方式.比如說,在拋出異常的時候它提供了一些關于哪個異常處理器將被執行的信息.此外,在請求被轉發到另外一個URL之前,編程方式的異常處理提供了更多的處理方式.s

實現 HandlerExceptionResolver 接口的話, 只需實現 resolveException(Exception, Handler) 方法并且返回一個 ModelAndView, 也可以使用提供的 SimpleMappingExceptionResolver 或者創建一個 @ExceptionHandler 方法. SimpleMappingExceptionResolver 使你能夠獲取任何異常的類名,這些異常可以被拋出或者映射到一個視圖. 這和Servlet API的異常映射特性是等價的,但是它可以通過不同的異常處理器實現更好的異常處理. 另外 @ExceptionHandler 可以被注解在一個處理異常的方法上. 這個方法可以被定義在包含 @Controller 的類局部區域 或者定義在包含 @ControllerAdvice 的類里面應用于多個 @Controller 類.

16.11.2 @ExceptionHandler

HandlerExceptionResolver 接口 和 SimpleMappingExceptionResolver
實現類允許映射異常到具體的視圖,在轉發到視圖之前可以有Java邏輯代碼. 但是, 有些情況下,
尤其是注解 @ResponseBody 的方法而不是一個視圖的情況下,它可以更方便的直接設置返回的狀態和返回的錯誤內容.就像我們可以在HandlerExceptionResolver接口實現中在出現異常的情況下返回一個友好的提示頁面,對于@ResponseBody這種情況的話我們可以處理成返回一個默認的code值來供前段識別或者返回一個默認的數值等等。
可以通過 @ExceptionHandler 方法. 當它在一個 controller 內部聲明時,它將被用于那個controller(或它的子類)的 @RequestMapping 方法拋出的異常.

@Controller
public class SimpleController {

    // @RequestMapping methods omitted ...

    @ExceptionHandler(IOException.class)
    public ResponseEntity<String> handleIOException(IOException ex) {
        // prepare responseEntity
        return responseEntity;
    }

}

@ExceptionHandler 的value可以設置一個需要被處理的異常數組. 如果一個異常被拋出并且包含在這個異常列表中, 然后就會調用 @ExceptionHandler 方法. 如果沒有設置value,
那么就會使用參數里面的異常. 和標準controller的 @RequestMapping 方法很相似, @ExceptionHandler 方法的參數值和返回值相當靈活. 比如說, HttpServletRequest 可以在 Servlet 環境中被接收, PortletRequest 在 Portlet 環境中被接收. 返回值可以是 String, 它將解釋為一個視圖, 可以是 ModelAndView 對象, 可以是 ResponseEntity 對象, 或者你可以添加 @ResponseBody 方法直接返回消息.

16.11.5 處理默認的錯誤頁面

當相應的code是error狀態但是response的響應體是空的時候,容器通常需要渲染一個默認的錯誤頁面供用戶使用。為來定義這個通用的錯誤頁面,你可以在web.Xml中聲明<error-page>標簽,在Servlet3之前,你還需要為每個標簽聲明對應的error code,但是到來Servelt3之后你就不需要這么做了。

<error-page>
    <location>/error</location>
</error-page>

@Controller
public class ErrorController {

    @RequestMapping(value="/error", produces="application/json")
    @ResponseBody
    public Map<String, Object> handle(HttpServletRequest request) {

        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", request.getAttribute("javax.servlet.error.status_code"));
        map.put("reason", request.getAttribute("javax.servlet.error.message"));

        return map;
    }

}

Spring的靈活配置信息

16.13.2 ModelAndView

ModelMap 類本質上是一個好聽一點的 Map,它堅持一個通用的命名約定,把用于顯示在 View 上面的對象添加到其中。考慮下面的 Controller 實現;注意添加到 ModelAndView 中的對象沒有指定任意關聯的名稱。

 public class DisplayShoppingCartController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {

        List cartItems = ***;
        User user = ***;

        ModelAndView mav = new ModelAndView("displayShoppingCart"); 

        mav.addObject(cartItems); 
        mav.addObject(user); 

        return mav;
    }
}

ModelAndView 類使用了一個 ModelMap 類,ModelMap 是一個自定義的 Map 實現,它會為添加到其中的對象自動生成一個 key。決定被添加對象名稱的策略是,如果是一個 scalar 對象,會使用對象類的簡短類名。對于將 scalar 對象添加到 ModelMap 實例的情況,下面的例子展示了生成的名稱:

  • 添加的 x.y.User 實例會生成名稱 user。
  • 添加的 x.y.Registration 會生成名稱 registration
  • 添加的 x.y.Foo 實例會生成名稱 foo
  • 添加的 java.util.HashMap 實例會生成名稱 hashMap。在這種情況下,你可能想要顯式指定名稱,因為 hashMap 不夠直觀。
  • 添加 null 會導致拋出一個 IllegalArgumentException。如果你要添加的一個對象(或多個對象)為 null,那么你也想要顯式指定名稱。

在添加一個 Set 或 List 之后,生成名稱的策略是,使用集合中第一個對象的簡短類名,并在名稱后追加 List。對數組使用的也是該策略。下面的例子會讓你對集合的名稱生成的語義更加清楚:

  • 添加一個具有零個或多個 x.y.User 元素的 x.y.User[] 數組,會生成名稱 userList。
  • 添加一個具有零個或多個 x.y.User 元素的 x.y.Foo[] 數組,會生成名稱 fooList。
  • 添加一個具有零個或多個 x.y.User 元素的 java.util.ArrayList,會生成名稱 userList。
  • 添加一個具有零個或多個 x.y.Foo 元素的 java.util.HashSet,會生成名稱 fooList。
  • 根本不能添加一個空的 java.util.ArrayList(實際上,addObject(..) 調用基本上會是一個無效操作)。
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容