程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> SpingMVC 核心技術幫助文檔,spingmvc幫助文檔

SpingMVC 核心技術幫助文檔,spingmvc幫助文檔

編輯:JAVA綜合教程

SpingMVC 核心技術幫助文檔,spingmvc幫助文檔


                                          SpringMVC 框架使用技巧

聲明:本篇文檔主要是用於參考幫助文檔,沒有實例,但幾乎包含了SpringMVC 4.2版本的所有核心技術

        對於覺得篇幅長的文檔,建議大家使用快捷鍵crtl + F,搜索關鍵字查詢較為方便.

         歡迎加群JAVA編程交流群 574337670

   

21.1 Spring Web MVC框架簡介

Spring的模型-視圖-控制器(MVC)框架是圍繞一個 DispatcherServlet 來設計的,這個Servlet會把請求分發給各個處理器,並支持可配置的處理器映射、視圖渲染、本地化、時區與主題渲染等,甚至還能支持文件上傳。處理器是你的應用中注解了@Controller@RequestMapping的類和方法,Spring為處理器方法提供了極其多樣靈活的配置。Spring 3.0以後提供了 @Controller 注解機制、 @PathVariable 注解以及一些其他的特性,你可以使用它們來進行RESTful web站點和應用的開發。

“對擴展開放”是Spring Web MVC框架一個重要的設計原則,而對於Spring的整個完整框架來說,其設計原則則是“對擴展開放,對修改閉合”。

Spring Web MVC核心類庫中的一些方法被定義為final方法。作為開發人員,你不能覆寫這些方法以定制其行為。當然,不是說絕對不行,但請記住這條原則,絕大多數情況下不是好的實踐。

關於該原則的詳細解釋,你可以參考Seth Ladd等人所著的“深入解析Spring Web MVC與Web Flow”一書。相關信息在第117頁,“設計初探(A Look At Design)”一節。或者,你可以參考:

  • Bob Martin所寫的“開閉原則(The Open-Closed Principle)”(PDF)

你無法增強Spring MVC中的final方法,比如AbstractController.setSynchronizeOnSession()方法等。請參考10.6.1 理解AOP代理一節,其中解釋了AOP代理的相關知識,論述了為什麼你不能對final方法進行增強。

在Spring Web MVC中,你可以使用任何對象來作為命令對象或表單返回對象,而無須實現一個框架相關的接口或基類。Spring的數據綁定非常靈活:比如,它會把數據類 型不匹配當成可由應用自行處理的運行時驗證錯誤,而非系統錯誤。你可能會為了避免非法的類型轉換在表單對象中使用字符串來存儲數據,但無類型的字符串無法 描述業務數據的真正含義,並且你還需要把它們轉換成對應的業務對象類型。有了Spring的驗證機制,意味著你再也不需這麼做了,並且直接將業務對象綁定 到表單對象上通常是更好的選擇。

Spring的視圖解析也是設計得異常靈活。控制器一般負責准備一個Map模型、填充數據、返回一個合適的視圖名等,同時它也可以直接將數據寫到響應流中。視圖名的解析高度靈活,支持多種配置,包括通過文件擴展名、Accept內容頭、bean、配置文件等的配置,甚至你還可以自己實現一個視圖解析器 ViewResolver 。模型(MVC中的M,model)其實是一個Map類型的接口,徹底地把數據從視圖技術中抽象分離了出來。你可以與基於模板的渲染技術直接整合,如JSP、Velocity和Freemarker等,或者你還可以直接生成XML、JSON、Atom以及其他多種類型的內容。Map模型會簡單地被轉換成合適的格式,比如JSP的請求屬性(attribute),一個Velocity模板的模型等。

21.1.1 Spring Web MVC的新特性

Spring Web Flow

Spring Web Flow (SWF) 意在成為web應用中的頁面流(page flow)管理中最好的解決方案。

SWF在Servlet環境和Portlet環境下集成了現有的框架,如Spring MVC和JSF等。如果你的業務流程有一個貫穿始終的模型,而非單純分立的請求,那麼SWF可能是適合你的解決方案。

SWF 允許你將邏輯上的頁面流抽取成獨立可復用的模塊,這對於構建一個web應用的多個模塊是有益的。that guide the user through controlled navigations that drive business processes.

關於SWF的更多信息,請訪問Spring Web Flow的官網。

Spring的web模塊支持許多web相關的特性:

  • 清晰的職責分離。每個角色——控制器,驗證器,命令對象,表單對象,模型對象, DispatcherServlet ,處理器映射,視圖解析器,等等許多——的工作,都可以由相應的對象來完成。
  • 強大、直觀的框架和應用bean的配置。這種配置能力包括能夠從不同的上下文中進行簡單的引用,比如在web控制器中引用業務對象、驗證器等。
  • 強大的適配能力、非侵入性和靈活性。Spring MVC支持你定義任意的控制器方法簽名,在特定的場景下你還可以添加適合的注解(比如 @RequestParam、@RequestHeader、@PathVariable 等)
  • 可復用的業務代碼,使你遠離重復代碼。你可以使用已有的業務對象作為命令對象或表單對象,而不需讓它們去繼承一個框架提供的什麼基類。
  • 可定制的數據綁定和驗證。類型不匹配僅被認為是應用級別的驗證錯誤,錯誤值、本地化日期、數字綁定等會被保存。你不需要再在表單對象使用全String字段,然後再手動將它們轉換成業務對象。
  • 可定制的處理器映射和視圖解析。處理器映射和視圖解析策略從簡單的基於URL配置,到精細專用的解析策略,Spring全都支持。在這一點上,Spring比一些依賴於特定技術的web框架要更加靈活。
  • 靈活的模型傳遞。Spring使用一個名稱/值對的Map來做模型,這使得模型很容易集成、傳遞給任何類型的視圖技術。
  • 可定制的本地化信息、時區和主題解析。支持用/不用Spring標簽庫的JSP技術,支持JSTL,支持無需額外配置的Velocity模板,等等。;
  • 一個簡單但功能強大的JSP標簽庫,通常稱為Spring標簽庫,它提供了諸如數據綁定、主題支持等一些特性的支持。這些定制的標簽為標記(markup)你的代碼提供了最大程度的靈活性。關於標簽庫描述符(descriptor)的更多信息,請參考附錄第42章 Spring JSP標簽庫
  • 一個Spring 2.0開始引入的JSP表單標簽庫。它讓你在JSP頁面中編寫表單簡單許多。關於標簽庫描述符(descriptor)的更多信息,請參考附錄 第43章 Spring表單的JSP標簽庫
  • 新增生命周期僅綁定到當前HTTP請求或HTTP會話的Bean類型。嚴格來說,這不是Spring MVC自身的特性,而是Spring MVC使用的上下文容器 WebApplicationContext 所提供的特性。這些bean的scope在6.5.4 請求、會話及全局會話scope一節有詳細描述。

21.1.2 允許其他MVC實現

有些項目可能更傾向於使用非Spring的MVC框架。 許多團隊希望仍然使用現有的技術棧,比如JSF等,這樣他們掌握的技能和工具依然能發揮作用。

如果你確實不想使用Spring的Web MVC,但又希望能從Spring提供的一些解決方案中受益,那麼將你所使用的框架和Spring進行集成也很容易。只需要在 ContextLoaderListener 中啟動一個Spring的根應用上下文(root application context),然後你就可以在任何action對象中通過其 ServletContext 屬性(或通過Spring對應的helper方法)取得。不需要任何侵入性的插件,因此不需要復雜的集成。從應用層的視角來看,你只是將Spring當成依賴庫使用,並且將它的根應用上下文實例作為應用進入點。

即 使不用Spring的Web MVC框架,你配置的其他Spring的 bean 和服務也都能很方便地取得。在這種場景下,Spring與其他web框架的使用不沖突。Spring只是 在許多問題上提出了其他純web MVC框架未曾提出過的解決方案,比如 bean 的配置、數據存取、事務處理等,僅此而已。因此,如果你只是想使用Spring的一部分特性來增強你的應 用,比如Spring提供的JDBC/Hibernate事務抽象等,那麼你可以將Spring作為一個中間層和/或數據存取層來使用。

21.2 DispatcherServlet

Spring MVC框架,與其他很多web的MVC框架一樣:請求驅動;所有設計都圍繞著一個中央Servlet來展開,它負責把所有請求分發到控制器;同時提供其他web應用開發所需要的功能。不過Spring的中央處理器, DispatcherServlet ,能做的比這更多。它與Spring IoC容器做到了無縫集成,這意味著,Spring提供的任何特性,在Spring MVC中你都可以使用。

下圖展示了Spring Web MVC的 DispatcherServlet 處理請求的工作流。熟悉設計模式的朋友會發現, DispatcherServlet 應用的其實就是一個“前端控制器”的設計模式(其他很多優秀的web框架也都使用了這個設計模式)。

 DispatcherServlet 其實就是個 Servlet (它繼承自 HttpServlet 基類),同樣也需要在你web應用的web.xml配置文件下聲明。你需要在 web.xml 文件中把你希望DispatcherServlet處理的請求映射到對應的URL上去。這就是標准的Java EE Servlet配置;下面的代碼就展示了對 DispatcherServlet 和路徑映射的聲明:

<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>

 

In the preceding example, all requests starting with /example will be handled by the DispatcherServlet instance named example. In a Servlet 3.0+ environment, you also have the option of configuring the Servlet container programmatically. Below is the code based equivalent of the above web.xml example:

在上面的例子中,所有路徑以 /example 開頭的請求都會被名字為example的 DispatcherServlet 處理。在Servlet 3.0+的環境下,你還可以用編程的方式配置Servlet容器。下面是一段這種基於代碼配置的例子,它與上面定義的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/*");
    }

}

 

 WebApplicationInitializer 是Spring MVC提供的一個接口,它會查找你所有基於代碼的配置,並應用它們來初始化Servlet 3版本以上的web容器。它有一個抽象的實現 AbstractDispatcherServletInitializer ,用以簡化 DispatcherServlet 的注冊工作:你只需要指定其servlet映射(mapping)即可。若想了解更多細節,可以參考基於代碼的Servlet容器初始化一節。

上面只是配置Spring Web MVC的第一步,接下來你需要配置其他的一些bean(除了 DispatcherServlet 以外的其他bean),它們也會被Spring Web MVC框架使用到。

在6.15 應用上下文ApplicationContext的其他作用)一節中我們聊到,Spring中的 ApplicationContext 實例是可以有范圍(scope)的。在Spring MVC中,每個 DispatcherServlet 都持有一個自己的上下文對象 WebApplicationContext ,它又繼承了根(root) WebApplicationContext 對象中已經定義的所有bean。這些繼承的bean可以在具體的Servlet實例中被重載,在每個Servlet實例中你也可以定義其scope下的新bean。

DispatcherServlet的初始化過程中,Spring MVC會在你web應用的WEB-INF目錄下查找一個名為[servlet-name]-servlet.xml的配置文件,並創建其中所定義的bean。如果在全局上下文中存在相同名字的bean,則它們將被新定義的同名bean覆蓋。

看看下面這個DispatcherServlet的Servlet配置(定義於web.xml文件中):

<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>

 

有了以上的Servlet配置文件,你還需要在應用中的/WEB-INF/路徑下創建一個 golfing-servlet.xml 文件,在該文件中定義所有Spring MVC相關的組件(比如bean等)。你可以通過servlet初始化參數為這個配置文件指定其他的路徑(見下面的例子):

當你的應用中只需要一個 DispatcherServlet 時,只配置一個根 contex t對象也是可行的。

要配置一個唯一的根 context 對象,可以通過在 servlet 初始化參數中配置一個空的 contextConfigLocation 來做到,如下所示:

<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>

 

 WebApplicationContext 繼承自 ApplicationContext ,它提供了一些web應用經常需要用到的特性。它與普通的 ApplicationContext 不同的地方在於,它支持主題的解析(詳見21.9 主題Themes一小節),並且知道它關聯到的是哪個servlet(它持有一個該 ServletContext 的引用)。 WebApplicationContext 被綁定在 ServletContext 中。如果需要獲取它,你可以通過 RequestContextUtils 工具類中的靜態方法來拿到這個web應用的上下文WebApplicationContext

21.2.1 WebApplicationContext中特殊的bean類型

Spring的DispatcherServlet使用了特殊的bean來處理請求、渲染視圖等,這些特定的bean是Spring MVC框架的一部分。如果你想指定使用哪個特定的bean,你可以在web應用上下文WebApplicationContext中簡單地配置它們。當然這只是可選的,Spring MVC維護了一個默認的bean列表,如果你沒有進行特別的配置,框架將會使用默認的bean。下一小節會介紹更多的細節,這裡,我們將先快速地看一下,DispatcherServlet都依賴於哪些特殊的bean來進行它的初始化。

bean的類型作用 HandlerMapping 處理器映射。它會根據某些規則將進入容器的請求映射到具體的處理器以及一系列前處理器和後處理器(即處理器攔截器)上。具體的規則視HandlerMapping類的實現不同而有所不同。其最常用的一個實現支持你在控制器上添加注解,配置請求路徑。當然,也存在其他的實現。 HandlerAdapter 處理器適配器。拿到請求所對應的處理器後,適配器將負責去調用該處理器,這使得DispatcherServlet無需關心具體的調用細節。比方說,要調用的是一個基於注解配置的控制器,那麼調用前還需要從許多注解中解析出一些相應的信息。因此,HandlerAdapter的主要任務就是對DispatcherServlet屏蔽這些具體的細節。 HandlerExceptionResolver 處理器異常解析器。它負責將捕獲的異常映射到不同的視圖上去,此外還支持更復雜的異常處理代碼。 ViewResolver 視圖解析器。它負責將一個代表邏輯視圖名的字符串(String)映射到實際的視圖類型View上。 LocaleResolver & LocaleContextResolver 地區解析器 和 地區上下文解析器。它們負責解析客戶端所在的地區信息甚至時區信息,為國際化的視圖定制提供了支持。 ThemeResolver 主題解析器。它負責解析你web應用中可用的主題,比如,提供一些個性化定制的布局等。 MultipartResolver 解析multi-part的傳輸請求,比如支持通過HTML表單進行的文件上傳等。 FlashMapManager FlashMap管理器。它能夠存儲並取回兩次請求之間的FlashMap對象。後者可用於在請求之間傳遞數據,通常是在請求重定向的情境下使用。

21.2.2 默認的DispatcherServlet配置

上一小節講到,DispatcherServlet維護了一個列表,其中保存了其所依賴的所有bean的默認實現。這個列表保存在包org.springframework.web.servlet下的DispatcherServlet.properties文件中。

這些特殊的bean都有一些基本的默認行為。或早或晚,你可能需要對它們提供的一些默認配置進行定制。比如說,通常你需要配置InternalResourceViewResolver類提供的prefix屬性,使其指向視圖文件所在的目錄。  這裡需要理解的一個事情是,一旦你在web應用上下文WebApplicationContext中配置了某個特殊bean以後(比如InternalResourceViewResolver),實際上你也覆寫了該bean的默認實現。比方說,如果你配置了InternalResourceViewResolver,那麼框架就不會再使用beanViewResolver的默認實現。

在21.16節 Spring MVC的配置中, 我們介紹了其他配置Spring MVC的方式,比如通過Java編程配置或者通過MVC XML命名空間進行配置。它們為配置一個Spring MVC應用提供了簡易的開始方式,也不需要你對框架實現細節有太多了解。當然,無論你選用何種方式開始配置,本節所介紹的一些概念都是基礎且普適的,它們 對你後續的學習都應有所助益。

21.2.3 DispatcherServlet的處理流程

配置好DispatcherServlet以後,開始有請求會經過這個DispatcherServlet。此時,DispatcherServlet會依照以下的次序對請求進行處理:

  • 首先,搜索應用的上下文對象WebApplicationContext並把它作為一個屬性(attribute)綁定到該請求上,以便控制器和其他組件能夠使用它。屬性的鍵名默認為DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE
  • 將地區(locale)解析器綁定到請求上,以便其他組件在處理請求(渲染視圖、准備數據等)時可以獲取區域相關的信息。如果你的應用不需要解析區域相關的信息,忽略它即可
  • 將主題(theme)解析器綁定到請求上,以便其他組件(比如視圖等)能夠了解要渲染哪個主題文件。同樣,如果你不需要使用主題相關的特性,忽略它即可
  • 如果你配置了multipart文件處理器,那麼框架將查找該文件是不是multipart(分為多個部分連續上傳)的。若是,則將該請求包裝成一個MultipartHttpServletRequest對象,以便處理鏈中的其他組件對它做進一步的處理。關於Spring對multipart文件傳輸處理的支持,讀者可以參考21.10 Spring的multipart(文件上傳)支持一小節
  • 為該請求查找一個合適的處理器。如果可以找到對應的處理器,則與該處理器關聯的整條執行鏈(前處理器、後處理器、控制器等)都會被執行,以完成相應模型的准備或視圖的渲染
  • 如果處理器返回的是一個模型(model),那麼框架將渲染相應的視圖。若沒有返回任何模型(可能是因為前後的處理器出於某些原因攔截了請求等,比如,安全問題),則框架不會渲染任何視圖,此時認為對請求的處理可能已經由處理鏈完成了

如果在處理請求的過程中拋出了異常,那麼上下文WebApplicationContext對象中所定義的異常處理器將會負責捕獲這些異常。通過配置你自己的異常處理器,你可以定制自己處理異常的方式。

Spring的DispatcherServlet也允許處理器返回一個Servlet API規范中定義的 最後修改時間戳(last-modification-date) 值。決定請求最後修改時間的方式很直接:DispatcherServlet會先查找合適的處理器映射來找到請求對應的處理器,然後檢測它是否實現了 LastModified 接口。若是,則調用接口的long getLastModified(request)方法,並將該返回值返回給客戶端。

你可以定制DispatcherServlet的配置,具體的做法,是在web.xml文件中,Servlet的聲明元素上添加一些Servlet的初始化參數(通過init-param元素)。該元素可選的參數列表如下:

可選參數解釋 contextClass 任意實現了WebApplicationContext接口的類。這個類會初始化該servlet所需要用到的上下文對象。默認情況下,框架會使用一個XmlWebApplicationContext對象。 contextConfigLocation 一個指定了上下文配置文件路徑的字符串,該值會被傳入給contextClass所指定的上下文實例對象。該字符串內可以包含多個字符串,字符串之間以逗號分隔,以此支持你進行多個上下文的配置。在多個上下文中重復定義的bean,以最後加載的bean定義為准 namespace WebApplicationContext的命名空間。默認是[servlet-name]-servlet

21.3 控制器(Controller)的實現

...Spring implements a controller in a very abstract way, which enables you to create a wide variety of controllers.

控制器作為應用程序邏輯的處理入口,它會負責去調用你已經實現的一些服務。通常,一個控制器會接收並解析用戶的請求,然後把它轉換成一個模型交給視圖,由視圖渲染出頁面最終呈現給用戶。Spring對控制器的定義非常寬松,這意味著你在實現控制器時非常自由。

Spring 2.5以後引入了基於注解的編程模型,你可以在你的控制器實現上添加@RequestMapping@RequestParam@ModelAttribute等 注解。注解特性既支持基於Servlet的MVC,也可支持基於Portlet的MVC。通過此種方式實現的控制器既無需繼承某個特定的基類,也無需實現 某些特定的接口。而且,它通常也不會直接依賴於Servlet或Portlet的API來進行編程,不過你仍然可以很容易地獲取Servlet或 Portlet相關的變量、特性和設施等。

在Spring項目的官方Github上你可以找到許多項目,它們對本節所述以後的注解支持提供了進一步增強,比如說MvcShowcase,MvcAjax,MvcBasic,PetClinic,PetCare等。

@Controller
public class HelloWorldController {

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

 

你可以看到,@Controller注解和@RequestMapping注解支持多樣的方法名和方法簽名。在上面這個例子中,方法接受一個Model類型的參數並返回一個字符串String類型的視圖名。但事實上,方法所支持的參數和返回值有非常多的選擇,這個我們在本小節的後面部分會提及。@Controller@RequestMapping及其他的一些注解,共同構成了Spring MVC框架的基本實現。本節將詳細地介紹這些注解,以及它們在一個Servlet環境下最常被使用到的一些場景。

21.3.1 使用@Controller注解定義一個控制器

[Original] The @Controller annotation indicates that a particular class serves the role of a controller. Spring does not require you to extend any controller base class or reference the Servlet API. However, you can still reference Servlet-specific features if you need to.

@Controller注解表明了一個類是作為控制器的角色而存在的。Spring不要求你去繼承任何控制器基類,也不要求你去實現Servlet的那套API。當然,如果你需要的話也可以去使用任何與Servlet相關的特性和設施。

[Original] The @Controller annotation acts as a stereotype for the annotated class, indicating its role. The dispatcher scans such annotated classes for mapped methods and detects @RequestMapping annotations (see the next section).

@Controller注解可以認為是被標注類的原型(stereotype),表明了這個類所承擔的角色。分派器(DispatcherServlet)會掃描所有注解了@Controller的類,檢測其中通過@RequestMapping注解配置的方法(詳見下一小節)。

[Original] You can define annotated controller beans explicitly, using a standard Spring bean definition in the dispatcher’s context. However, the @Controller stereotype also allows for autodetection, aligned with Spring general support for detecting component classes in the classpath and auto-registering bean definitions for them.

當然,你也可以不使用@Controller注解而顯式地去定義被注解的bean,這點通過標准的Spring bean的定義方式,在dispather的上下文屬性下配置即可做到。但是@Controller原型是可以被框架自動檢測的,Spring支持classpath路徑下組件類的自動檢測,以及對已定義bean的自動注冊。

[Original] To enable autodetection of such annotated controllers, you add component scanning to your configuration. Use the spring-context schema as shown in the following XML snippet:

你需要在配置中加入組件掃描的配置代碼來開啟框架對注解控制器的自動檢測。請使用下面XML代碼所示的spring-context schema:

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

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

    <!-- ... -->

</beans>

 

 

21.3.2 使用@RequestMapping注解映射請求路徑

你可以使用 @RequestMapping 注解來將請求URL,如 /appointments 等, 映射到整個類上或某個特定的處理器方法上。一般來說,類級別的注解負責將一個特定(或符合某種模式)的請求路徑映射到一個控制器上,同時通過方法級別的注 解來細化映射,即根據特定的HTTP請求方法(“GET”“POST”方法等)、HTTP請求中是否攜帶特定參數等條件,將請求映射到匹配的方法上。

下面這段代碼示例來自Petcare,它展示了在Spring MVC中如何在控制器上使用 @RequestMapping 注解:

@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(path = "/{day}", method = RequestMethod.GET)
    public Map<String, Appointment> getForDay(@PathVariable @DateTimeFormat(iso=ISO.DATE) Date day, Model model) {
        return appointmentBook.getAppointmentsForDay(day);
    }

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

    @RequestMapping(method = RequestMethod.POST)
    public String add(@Valid AppointmentForm appointment, BindingResult result) {
        if (result.hasErrors()) {
            return "appointments/new";
        }
        appointmentBook.addAppointment(appointment);
        return "redirect:/appointments";
    }
}

 

在上面的示例中,許多地方都使用到了 @RequestMapping 注解。第一次使用點是作用於類級別的,它指示了所有 /appointments 開頭的路徑都會被映射到控制器下。 get() 方法上的 @RequestMapping 注解對請求路徑進行了進一步細化:它僅接受GET方法的請求。這樣,一個請求路徑為 /appointments 、HTTP方法為GET的請求,將會最終進入到這個方法被處理。 add() 方法也做了類似的細化,而 getNewForm() 方法則同時注解了能夠接受的請求的HTTP方法和路徑。這種情況下,一個路徑為 appointments/new 、HTTP方法為GET的請求將會被這個方法所處理。

 getForDay() 方法則展示了使用 @RequestMapping 注解的另一個技巧:URI模板。(關於URI模板,請見下小節)

類級別的 @RequestMapping 注解並不是必須的。不配置的話則所有的路徑都是絕對路徑,而非相對路徑。以下的代碼示例來自PetClinic,它展示了一個具有多個處理器方法的控制器:

@Controller
public class ClinicController {

    private final Clinic clinic;

    @Autowired
    public ClinicController(Clinic clinic) {
        this.clinic = clinic;
    }

    @RequestMapping("/")
    public void welcomeHandler() {
    }

    @RequestMapping("/vets")
    public ModelMap vetsHandler() {
        return new ModelMap(this.clinic.getVets());
    }
}

 

以上代碼沒有指定請求必須是GET方法還是 PUT/POST 或其他方法, @RequestMapping 注解默認會映射所有的HTTP請求方法。如果僅想接收某種請求方法,請在注解中指定之 @RequestMapping(method=GET) 以縮小范圍。

 @Controller 和面向切面(AOP)代理

有時,我們希望在運行時使用AOP代理來裝飾控制器,比如當你直接在控制器上使用 @Transactional 注解時。這種情況下,我們推薦使用類級別(在控制器上使用)的代理方式。這一般是代理控制器的默認做法。如果控制器必須實現一些接口,而該接口又不支持Spring Context的回調(比如 InitializingBean, *Aware 等接口),那要配置類級別的代理就必須手動配置了。比如,原來的配置文件 <tx:annotation-driven/> 需要顯式配置為 <tx:annotation-driven proxy-target-class="true"/> 。

Spring MVC 3.1中新增支持 @RequestMapping 的一些類

They are recommended for use and even required to take advantage of new features in Spring MVC 3.1 and going forward.

Spring 3.1中新增了一組類用以增強 @RequestMapping ,分別是 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter 。 我們推薦你用一用。有部分Spring MVC 3.1之後新增的特性,這兩個注解甚至是必須的。在MVC命名空間和MVC Java編程配置方式下,這組類及其新特性默認是開啟的。但若你使用其他配置方式,則該特性必須手動配置才能使用。本小節將簡要介紹一下,新類相比之前的 一些重要變化。

在Spring 3.1之前,框架會在兩個不同的階段分別檢查類級別和方法級別的請求映射——首先, DefaultAnnotationHanlderMapping 會先在類級別上選中一個控制器,然後再通過 AnnotationMethodHandlerAdapter 定位到具體要調用的方法。

[Original] With the new support classes in Spring 3.1, the RequestMappingHandlerMapping is the only place where a decision is made about which method should process the request. Think of controller methods as a collection of unique endpoints with mappings for each method derived from type and method-level @RequestMapping information.

現在有了Spring 3.1後引入的這組新類, RequestMappingHandlerMapping 成為了這兩個決策實際發生的唯一一個地方。你可以把控制器中的一系列處理方法當成是一系列獨立的服務節點,每個從類級別和方法級別的 @RequestMapping 注解中獲取到足夠請求1路徑映射信息。

[Original] This enables some new possibilities. For once a HandlerInterceptor or a HandlerExceptionResolver can now expect the Object-based handler to be a HandlerMethod, which allows them to examine the exact method, its parameters and associated annotations. The processing for a URL no longer needs to be split across different controllers.

這種新的處理方式帶來了新的可能性。之前的 HandlerInterceptor 或 HandlerExceptionResolver 現在可以確定拿到的這個處理器肯定是一個 HandlerMethod 類型,因此它能夠精確地了解這個方法的所有信息,包括它的參數、應用於其上的注解等。這樣,內部對於一個URL的處理流程再也不需要分隔到不同的控制器裡面去執行了。

[Original] There are also several things no longer possible: [Original] Select a controller first with a SimpleUrlHandlerMapping or BeanNameUrlHandlerMapping and then narrow the method based on @RequestMapping annotations. [Original] Rely on method names as a fall-back mechanism to disambiguate between two @RequestMapping methods that don’t have an explicit path mapping URL path but otherwise match equally, e.g. by HTTP method. In the new support classes @RequestMapping methods have to be mapped uniquely. [Original] * Have a single default method (without an explicit path mapping) with which requests are processed if no other controller method matches more concretely. In the new support classes if a matching method is not found a 404 error is raised.

同時,也有其他的一些變化,比如有些事情就沒法這麼玩兒了:

  • 先通過 SimpleUrlHandlerMapping 或 BeanNameUrlHandlerMapping 來拿到負責處理請求的控制器,然後通過 @RequestMapping 注解配置的信息來定位到具體的處理方法;
  • 依靠方法名稱來作為選擇處理方法的標准。比如說,兩個注解了 @RequestMapping 的方法除了方法名稱擁有完全相同的URL映射和HTTP請求方法。在新版本下, @RequestMapping 注解的方法必須具有唯一的請求映射;
  • 定義一個默認方法(即沒有聲明路徑映射),在請求路徑無法被映射到控制器下更精確的方法上去時,為該請求提供默認處理。在新版本中,如果無法為一個請求找到合適的處理方法,那麼一個404錯誤將被拋出;

[Original] The above features are still supported with the existing support classes. However to take advantage of new Spring MVC 3.1 features you’ll need to use the new support classes.

如果使用原來的類,以上的功能還是可以做到。但是,如果要享受Spring MVC 3.1版本帶來的方便特性,你就需要去使用新的類。

[Original] ## URI Template Patterns

URI模板

[Original] URI templates can be used for convenient access to selected parts of a URL in a @RequestMapping method.

URI模板可以為快速訪問@RequestMapping中指定的URL的一個特定的部分提供很大的便利。

[Original] A URI Template is a URI-like string, containing one or more variable names. When you substitute values for these variables, the template becomes a URI. The proposed RFC for URI Templates defines how a URI is parameterized. For example, the URI Template http://www.example.com/users/{userId} contains the variable userId. Assigning the value fred to the variable yields http://www.example.com/users/fred.

URI模板是一個類似於URI的字符串,只不過其中包含了一個或多個的變量名。當你使用實際的值去填充這些變量名的時候,模板就退化成了一個URI。在URI模板的RFC提議中定義了一個URI是如何進行參數化的。比如說,一個這個URI模板 http://www.example.com/users/{userId} 就包含了一個變量名userId。將值fred賦給這個變量名後,它就變成了一個URI: http://www.example.com/users/fred 。

[Original] In Spring MVC you can use the @PathVariable annotation on a method argument to bind it to the value of a URI template variable:

在Spring MVC中你可以在方法參數上使用 @PathVariable 注解,將其與URI模板中的參數綁定起來:

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

 

[Original] The URI Template " /owners/{ownerId} " specifies the variable name ownerId. When the controller handles this request, the value of ownerId is set to the value found in the appropriate part of the URI. For example, when a request comes in for /owners/fred, the value of ownerId is fred.

URI模板" /owners/{ownerId} "指定了一個變量,名為ownerId。當控制器處理這個請求的時候,ownerId的值就會被URI模板中對應部分的值所填充。比如說,如果請求的URI是

 /owners/fred ,此時變量ownerId的值就是fred. `

為了處理 @PathVariables 注解,Spring MVC必須通過變量名來找到URI模板中相對應的變量。你可以在注解中直接聲明:

@RequestMapping(path="/owners/{ownerId}}", method=RequestMethod.GET)
public String findOwner(@PathVariable("ownerId") String theOwner, Model model) {
    // 具體的方法代碼…
}

 

或者,如果URI模板中的變量名與方法的參數名是相同的,則你可以不必再指定一次。只要你在編譯的時候留下debug信息,Spring MVC就可以自動匹配URL模板中與方法參數名相同的變量名。

@RequestMapping(path="/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable String ownerId, Model model) {
    // 具體的方法代碼…
}

 

[Original] A method can have any number of @PathVariable annotations:

一個方法可以擁有任意數量的 @PathVariable 注解:

@RequestMapping(path="/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";
}

 

[Original] When a  @PathVariable  annotation is used on a  Map<String, String>  argument, the map is populated with all URI template variables.

當 @PathVariable 注解被應用於 Map<String, String> 類型的參數上時,框架會使用所有URI模板變量來填充這個map。

[Original] A URI template can be assembled from type and path level @RequestMapping annotations. As a result the findPet() method can be invoked with a URL such as /owners/42/pets/21.

URI模板可以從類級別和方法級別的  @RequestMapping  注解獲取數據。因此,像這樣的 findPet() 方法可以被類似於 /owners/42/pets/21 這樣的URL路由並調用到:

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

    @RequestMapping("/pets/{petId}")
    public void findPet(_@PathVariable_ String ownerId, _@PathVariable_ String petId, Model model) {
        // 方法實現體這裡忽略
    }

}

 

[Original] A  @PathVariable  argument can be of any simple type such as int, long, Date, etc. Spring automatically converts to the appropriate type or throws a  TypeMismatchException  if it fails to do so. You can also register support for parsing additional data types. See the section called "Method Parameters And Type Conversion" and the section called "Customizing WebDataBinder initialization".

 @PathVariable 可以被應用於所有 簡單類型 的參數上,比如int、long、Date等類型。Spring會自動地幫你把參數轉化成合適的類型,如果轉換失敗,就拋出一個 TypeMismatchException 。如果你需要處理其他數據類型的轉換,也可以注冊自己的類。若需要更詳細的信息可以參考“方法參數與類型轉換”一節和“定制WebDataBinder初始化過程”一節

帶正則表達式的URI模板

[Original] Sometimes you need more precision in defining URI template variables. Consider the URL " /spring-web/spring-web-3.0.5.jar ". How do you break it down into multiple parts?

有時候你可能需要更准確地描述一個URI模板的變量,比如說這個URL:" /spring-web/spring-web-3.0.5.jar 。你要怎麼把它分解成幾個有意義的部分呢?

[Original] The  @RequestMapping  annotation supports the use of regular expressions in URI template variables. The syntax is  {varName:regex}  where the first part defines the variable name and the second - the regular expression.For example:

@RequestMapping注解支持你在URI模板變量中使用正則表達式。語法是 {varName:regex} ,其中第一部分定義了變量名,第二部分就是你所要應用的正則表達式。比如下面的代碼樣例:

@RequestMapping("/spring-web/{symbolicName:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{extension:\\.[a-z]+}")
    public void handle(@PathVariable String version, @PathVariable String extension) {
        // 代碼部分省略...
    }
}

 

Path Patterns(不好翻,容易掉韻味)

[Original] In addition to URI templates, the  @RequestMapping  annotation also supports Ant-style path patterns (for example,  /myPath/*.do ). A combination of URI template variables and Ant-style globs is also supported (e.g.  /owners/*/pets/{petId} ).

除了URI模板外, @RequestMapping 注解還支持Ant風格的路徑模式(如 /myPath/*.do 等)。不僅如此,還可以把URI模板變量和Ant風格的glob組合起來使用(比如 /owners/*/pets/{petId} 這樣的用法等)。

路徑樣式的匹配(Path Pattern Comparison)

[Original] When a URL matches multiple patterns, a sort is used to find the most specific match.

當一個URL同時匹配多個模板(pattern)時,我們將需要一個算法來決定其中最匹配的一個。

[Original] A pattern with a lower count of URI variables and wild cards is considered more specific. For example  /hotels/{hotel}/*  has 1 URI variable and 1 wild card and is considered more specific than  /hotels/{hotel}/**  which as 1 URI variable and 2 wild cards.

URI模板變量的數目和通配符數量的總和最少的那個路徑模板更准確。舉個例子, /hotels/{hotel}/* 這個路徑擁有一個URI變量和一個通配符,而 /hotels/{hotel}/** 這個路徑則擁有一個URI變量和兩個通配符,因此,我們認為前者是更准確的路徑模板。

[Original] If two patterns have the same count, the one that is longer is considered more specific. For example  /foo/bar*  is longer and considered more specific than  /foo/* .

如果兩個模板的URI模板數量和通配符數量總和一致,則路徑更長的那個模板更准確。舉個例子,/foo/bar*就被認為比/foo/*更准確,因為前者的路徑更長。

[Original] When two patterns have the same count and length, the pattern with fewer wild cards is considered more specific. For example /hotels/{hotel} is more specific than /hotels/*.

如果兩個模板的數量和長度均一致,則那個具有更少通配符的模板是更加准確的。比如,/hotels/{hotel}就比/hotels/*更精確。

[Original] There are also some additional special rules:

除此之外,還有一些其他的規則:

[Original] The default mapping pattern `/*is less specific than any other pattern. For example/api/{a}/{b}/{c}` is more specific.

[Original] A prefix pattern such as `/public/*is less specific than any other pattern that doesn't contain double wildcards. For example/public/path3/{a}/{b}/{c}` is more specific.

  • 默認的通配模式/**比其他所有的模式都更“不准確”。比方說,/api/{a}/{b}/{c}就比默認的通配模式/**要更准確
  • 前綴通配(比如/public/**)被認為比其他任何不包括雙通配符的模式更不准確。比如說,/public/path3/{a}/{b}/{c}就比/public/**更准確

[Original] For the full details see AntPatternComparator in AntPathMatcher. Note that the PathMatcher can be customized (see Section 21.16.11, "Path Matching" in the section on configuring Spring MVC).

更多的細節請參考這兩個類:AntPatternComparatorAntPathMatcher。值得一提的是,PathMatcher類是可以配置的(見“配置Spring MVC”一節中的21.16.11 路徑的匹配一節)。

帶占位符的路徑模式(path patterns)

[Original] Patterns in @RequestMapping annotations support ${…} placeholders against local properties and/or system properties and environment variables. This may be useful in cases where the path a controller is mapped to may need to be customized through configuration. For more information on placeholders, see the javadocs of the PropertyPlaceholderConfigurer class.

@RequestMapping注解支持在路徑中使用占位符,以取得一些本地配置、系統配置、環境變量等。這個特性有時很有用,比如說控制器的映射路徑需要通過配置來定制的場景。如果想了解更多關於占位符的信息,可以參考PropertyPlaceholderConfigurer這個類的文檔。

Suffix Pattern Matching

後綴模式匹配

[Original] By default Spring MVC performs " .* " suffix pattern matching so that a controller mapped to /person is also implicitly mapped to /person.*. This makes it easy to request different representations of a resource through the URL path (e.g.  /person.pdf, /person.xml ).

Spring MVC默認采用".*"的後綴模式匹配來進行路徑匹配,因此,一個映射到/person路徑的控制器也會隱式地被映射到 /person.* 。這使得通過URL來請求同一資源文件的不同格式變得更簡單(比如 /person.pdf,/person.xml )。

[Original] Suffix pattern matching can be turned off or restricted to a set of path extensions explicitly registered for content negotiation purposes. This is generally recommended to minimize ambiguity with common request mappings such as  /person/{id}  where a dot might not represent a file extension, e.g.  /person/[email protected]  vs  /person/[email protected] ). Furthermore as explained in the note below suffix pattern matching as well as content negotiation may be used in some circumstances to attempt malicious attacks and there are good reasons to restrict them meaningfully.

你可以關閉默認的後綴模式匹配,或者顯式地將路徑後綴限定到一些特定格式上for content negotiation purpose。我們推薦這樣做,這樣可以減少映射請求時可以帶來的一些二義性,比如請求以下路徑 /person/{id} 時,路徑中的點號後面帶的可能不是描述內容格式,比如/person/[email protected] vs /person/[email protected]。而且正如下面馬上要提到的,後綴模式通配以及內容協商有時可能會被黑客用來進行攻擊,因此,對後綴通配進行有意義的限定是有好處的。

[Original] See Section 21.16.11, "Path Matching" for suffix pattern matching configuration and also Section 21.16.6, "Content Negotiation" for content negotiation configuration.

關於後綴模式匹配的配置問題,可以參考第21.16.11小節 "路徑匹配";關於內容協商的配置問題,可以參考第21.16.6小節 "內容協商"的內容。

後綴模式匹配與RFD

[Original] Reflected file download (RFD) attack was first described in a paper by Trustwave in 2014. The attack is similar to XSS in that it relies on input (e.g. query parameter, URI variable) being reflected in the response. However instead of inserting JavaScript into HTML, an RFD attack relies on the browser switching to perform a download and treating the response as an executable script if double-clicked based on the file extension (e.g. .bat, .cmd).

RFD(Reflected file download)攻擊最先是2014年在Trustwave的一篇論文中 被提出的。它與XSS攻擊有些相似,因為這種攻擊方式也依賴於某些特征,即需要你的輸入(比如查詢參數,URI變量等)等也在輸出(response)中 以某種形式出現。不同的是,RFD攻擊並不是通過在HTML中寫入JavaScript代碼進行,而是依賴於浏覽器來跳轉到下載頁面,並把特定格式(比 如.bat,.cmd等)的response當成是可執行腳本,雙擊它就會執行。

[Original] In Spring MVC  @ResponseBody  and  ResponseEntity  methods are at risk because they can render different content types which clients can request including via URL path extensions. Note however that neither disabling suffix pattern matching nor disabling the use of path extensions for content negotiation purposes alone are effective at preventing RFD attacks.

Spring MVC的@ResponseBodyResponseEntity方法是有風險的,因為它們會根據客戶的請求——包括URL的路徑後綴,來渲染不同的內容類型。因此,禁用後綴模式匹配或者禁用僅為內容協商開啟的路徑文件後綴名攜帶,都是防范RFD攻擊的有效方式。

[Original] For comprehensive protection against RFD, prior to rendering the response body Spring MVC adds a  Content-Disposition:inline;filename=f.txt  header to suggest a fixed and safe download file filename. This is done only if the URL path contains a file extension that is neither whitelisted nor explicitly registered for content negotiation purposes. However it may potentially have side effects when URLs are typed directly into a browser.

若要開啟對RFD更高級的保護模式,可以在Spring MVC渲染開始請求正文之前,在請求頭中增加一行配置 Content-Disposition:inline;filename=f.txt ,指定固定的下載文件的文件名。這僅在URL路徑中包含了一個文件符合以下特征的拓展名時適用:該擴展名既不在信任列表(白名單)中,也沒有被顯式地被注冊於內容協商時使用。並且這種做法還可以有一些副作用,比如,當URL是通過浏覽器手動輸入的時候。

[Original] Many common path extensions are whitelisted by default. Furthermore REST API calls are typically not meant to be used as URLs directly in browsers. Nevertheless applications that use custom  HttpMessageConverter  implementations can explicitly register file extensions for content negotiation and the Content-Disposition header will not be added for such extensions. See Section 21.16.6, "Content Negotiation".

很多常用的路徑文件後綴默認是被信任的。另外,REST的API一般是不應該直接用做URL的。不過,你可以自己定制 HttpMessageConverter 的實現,然後顯式地注冊用於內容協商的文件類型,這種情形下Content-Disposition頭將不會被加入到請求頭中。詳見第21.16.6節中“內容協商”的內容。

[Original] This was originally introduced as part of work for CVE-2015-5211. Below are additional recommendations from the report:

  • Encode rather than escape JSON responses. This is also an OWASP XSS recommendation. For an example of how to do that with Spring see spring-jackson-owasp.
  • Configure suffix pattern matching to be turned off or restricted to explicitly registered suffixes only.
  • Configure content negotiation with the properties "useJaf" and "ignoreUnknownPathExtensions" set to false which would result in a 406 response for URLs with unknown extensions. Note however that this may not be an option if URLs are naturally expected to have a dot towards the end.
  • Add X-Content-Type-Options: nosniff header to responses. Spring Security 4 does this by default.

感覺這節的翻譯質量還有限,需要繼續了解XSS攻擊和RFD攻擊的細節再翻。

矩陣變量

[Original] The URI specification RFC 3986 defines the possibility of including name-value pairs within path segments. There is no specific term used in the spec. The general "URI path parameters" could be applied although the more unique "Matrix URIs", originating from an old post by Tim Berners-Lee, is also frequently used and fairly well known. Within Spring MVC these are referred to as matrix variables.

原來的URI規范RFC 3986中允許在路徑段落中攜帶鍵值對,但規范沒有明確給這樣的鍵值對定義術語。有人叫“URI路徑參數”,也有叫“矩陣URI”的。後者是Tim Berners-Lee首先在其博客中提到的術語,被使用得要更加頻繁一些,知名度也更高些。而在Spring MVC中,我們稱這樣的鍵值對為矩陣變量。

[Original] Matrix variables can appear in any path segment, each matrix variable separated with a ";" (semicolon). For example: "/cars;color=red;year=2012". Multiple values may be either "," (comma) separated "color=red,green,blue" or the variable name may be repeated "color=red;color=green;color=blue".

矩陣變量可以在任何路徑段落中出現,每對矩陣變量之間使用一個分號“;”隔開。比如這樣的URI:" /cars;color=red;year=2012 "。多個值可以用逗號隔開" color=red,green,blue ",或者重復變量名多次" color=red;color=green;color=blue "

[Original] If a URL is expected to contain matrix variables, the request mapping pattern must represent them with a URI template. This ensures the request can be matched correctly regardless of whether matrix variables are present or not and in what order they are provided.

如果一個URL有可能需要包含矩陣變量,那麼在請求路徑的映射配置上就需要使用URI模板來體現這一點。這樣才能確保請求可以被正確地映射,而不管矩陣變量在URI中是否出現、出現的次序是怎樣等。

[Original] Below is an example of extracting the matrix variable "q":

下面是一個例子,展示了我們如何從矩陣變量中獲取到變量“q”的值:

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

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

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

}

 

[Original] Since all path segments may contain matrix variables, in some cases you need to be more specific to identify where the variable is expected to be:

由於任意路徑段落中都可以含有矩陣變量,在某些場景下,你需要用更精確的信息來指定一個矩陣變量的位置:

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

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

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

}

 

[Original] A matrix variable may be defined as optional and a default value specified:

你也可以聲明一個矩陣變量不是必須出現的,並給它賦一個默認值:

// GET /pets/42

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

    // q == 1

}

 

[Original] All matrix variables may be obtained in a Map:

也可以通過一個Map來存儲所有的矩陣變量:

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

@RequestMapping(path = "/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]

}

 

[Original] Note that to enable the use of matrix variables, you must set the  removeSemicolonContent  property of  RequestMappingHandlerMapping  to  false . By default it is set to  true .

如果要允許矩陣變量的使用,你必須把 RequestMappingHandlerMapping 類的 removeSemicolonContent 屬性設置為 false 。該值默認是 true 的。

[Original] The MVC Java config and the MVC namespace both provide options for enabling the use of matrix variables.

MVC的Java編程配置和命名空間配置都提供了啟用矩陣變量的方式。

[Original] If you are using Java config, The Advanced Customizations with MVC Java Config section describes how the RequestMappingHandlerMapping can be customized.

如果你是使用Java編程的方式,“MVC Java高級定制化配置”一節描述了如何對 RequestMappingHandlerMapping 進行定制。

[Original] In the MVC namespace, the  <mvc:annotation-driven>  element has an  enable-matrix-variables  attribute that should be set to true. By default it is set to false.

而使用MVC的命名空間配置時,你可以把 <mvc:annotation-driven> 元素下的  enable-matrix-variables  屬性設置為 true 。該值默認情況下是配置為 false 的。

<?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>

 

可消費的媒體類型

[Original] You can narrow the primary mapping by specifying a list of consumable media types. The request will be matched only if the Content-Type request header matches the specified media type. For example:

你可以指定一組可消費的媒體類型,縮小映射的范圍。這樣只有當請求頭中 Content-Type 的值與指定可消費的媒體類型中有相同的時候,請求才會被匹配。比如下面這個例子:

@Controller
@RequestMapping(path = "/pets", method = RequestMethod.POST, consumes="application/json")
public void addPet(@RequestBody Pet pet, Model model) {
    // 方法實現省略
}

 

[Original] Consumable media type expressions can also be negated as in !text/plain to match to all requests other than those with Content-Type of text/plain. Also consider using constants provided in MediaType such as APPLICATION_JSON_VALUE and APPLICATION_JSON_UTF8_VALUE.

指定可消費媒體類型的表達式中還可以使用否定,比如,可以使用 !text/plain 來匹配所有請求頭 Content-Type 中不含 text/plain 的請求。同時,在MediaType類中還定義了一些常量,比如 APPLICATION_JSON_VALUE、APPLICATION_JSON_UTF8_VALUE 等,推薦更多地使用它們。

[Original] The consumes condition is supported on the type and on the method level. Unlike most other conditions, when used at the type level, method-level consumable types override rather than extend type-level consumable types.

consumes 屬性提供的是方法級的類型支持。與其他屬性不同,當在類型級使用時,方法級的消費類型將覆蓋類型級的配置,而非繼承關系。

可生產的媒體類型

[Original] You can narrow the primary mapping by specifying a list of producible media types. The request will be matched only if the Accept request header matches one of these values. Furthermore, use of the produces condition ensures the actual content type used to generate the response respects the media types specified in the produces condition. For example:

你可以指定一組可生產的媒體類型,縮小映射的范圍。這樣只有當請求頭中 Accept 的值與指定可生產的媒體類型中有相同的時候,請求才會被匹配。而且,使用 produces 條件可以確保用於生成響應(response)的內容與指定的可生產的媒體類型是相同的。舉個例子:

@Controller
@RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Pet getPet(@PathVariable String petId, Model model) {
    // 方法實現省略
}

 

[Original] Be aware that the media type specified in the produces condition can also optionally specify a character set. For example, in the code snippet above we specify the same media type than the default one configured in MappingJackson2HttpMessageConverter, including the UTF-8 charset.

要注意的是,通過 condition 條件指定的媒體類型也可以指定字符集。比如在上面的小段代碼中,我們還是覆寫了 MappingJackson2HttpMessageConverter 類中默認配置的媒體類型,同時,還指定了使用UTF-8的字符集。

[Original] Just like with consumes, producible media type expressions can be negated as in !text/plain to match to all requests other than those with an Accept header value of text/plain. Also consider using constants provided in  MediaType  such as APPLICATION_JSON_VALUE and APPLICATION_JSON_UTF8_VALUE.

consumes 條件類似,可生產的媒體類型表達式也可以使用否定。比如,可以使用 !text/plain 來匹配所有請求頭 Accept 中不含 text/plain 的請求。同時,在 MediaType 類中還定義了一些常量,比如 APPLICATION_JSON_VALUE、APPLICATION_JSON_UTF8_VALUE 等,推薦更多地使用它們。

[Original] The produces condition is supported on the type and on the method level. Unlike most other conditions, when used at the type level, method-level producible types override rather than extend type-level producible types.

produces 屬性提供的是方法級的類型支持。與其他屬性不同,當在類型級使用時,方法級的消費類型將覆蓋類型級的配置,而非繼承關系。

請求參數與請求頭的值

[Original] You can narrow request matching through request parameter conditions such as "myParam", "!myParam", or "myParam=myValue". The first two test for request parameter presence/absence and the third for a specific parameter value. Here is an example with a request parameter value condition:

你可以篩選請求參數的條件來縮小請求匹配范圍,比如"myParam""!myParam""myParam=myValue"等。前兩個條件用於篩選存在/不存在某些請求參數的請求,第三個條件篩選具有特定參數值的請求。下面有個例子,展示了如何使用請求參數值的篩選條件:

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

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

}

 

[Original] The same can be done to test for request header presence/absence or to match based on a specific request header value:

同樣,你可以用相同的條件來篩選請求頭的出現與否,或者篩選出一個具有特定值的請求頭:

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

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

}

 

[Original] Although you can match to Content-Type and Accept header values using media type wild cards (for example "content-type=text/*" will match to "text/plain" and "text/html"), it is recommended to use the consumes and produces conditions respectively instead. They are intended specifically for that purpose.

盡管,你可以使用媒體類型的通配符(比如 "content-type=text/*")來匹配請求頭 Content-TypeAccept的值,但我們更推薦獨立使用 consumesproduces條件來篩選各自的請求。因為它們就是專門為區分這兩種不同的場景而生的。

21.3.3 定義 @RequestMapping 注解的處理方法(handler method)

使用 @RequestMapping 注解的處理方法可以擁有非常靈活的方法簽名,它支持的方法參數及返回值類型將在接下來的小節講述。大多數參數都可以任意的次序出現,除了唯一的一個例外: BindingResult 參數。這在下節也會詳細描述。

Spring 3.1中新增了一些類,用以增強注解了 @RequestMapping 的處理方法,分別是 RequestMappingHandlerMapping 類和 RequestMappingHandlerAdapter 類。我們鼓勵使用這組新的類,如果要使用Spring 3.1及以後版本的新特性,這組類甚至是必須使用的。這些增強類在MVC的命名空間配置和MVC的Java編程方式配置中都是默認開啟的,如果不是使用這兩種方法,那麼就需要顯式地配置。

支持的方法參數類型

下面列出所有支持的方法參數類型:

  • 請求或響應對象(Servlet API)。可以是任何具體的請求或響應類型的對象,比如, ServletRequest 或 HttpServletRequest 對象等。
  •  HttpSession 類型的會話對象(Servlet API)。使用該類型的參數將要求這樣一個 session 的存在,因此這樣的參數永不為 null 。

存 取session可能不是線程安全的,特別是在一個Servlet的運行環境中。如果應用可能有多個請求同時並發存取一個session場景,請考慮將  RequestMappingHandlerAdapter 類中的"synchronizeOnSession"標志設置為"true"。

  •  org.springframework.web.context.request.WebRequest 或 org.springframework.web.context.request.NativeWebRequest 。允許存取一般的請求參數和請求/會話范圍的屬性(attribute),同時無需綁定使用Servlet/Portlet的API
  • 當前請求的地區信息 java.util.Locale ,由已配置的最相關的地區解析器解析得到。在MVC的環境下,就是應用中配置的 LocaleResolver 或 LocaleContextResolver 
  • 與當前請求綁定的時區信息 java.util.TimeZone (java 6以上的版本)/ java.time.ZoneId (java 8),由 LocaleContextResolver 解析得到
  • 用於存取請求正文的 java.io.InputStream 或 java.io.Reader 。該對象與通過Servlet API拿到的輸入流/Reader是一樣的
  • 用於生成響應正文的 java.io.OutputStream 或 java.io.Writer 。該對象與通過Servlet API拿到的輸出流/Writer是一樣的
  •  org.springframework.http.HttpMethod 。可以拿到HTTP請求方法
  • 包裝了當前被認證用戶信息的 java.security.Principal 
  • 帶 @PathVariable 注解的方法參數,其存放了URI模板變量中的值。詳見“URI模板變量”一節
  • 帶 @MatrixVariable 注解的方法參數,其存放了URI路徑段中的鍵值對。詳見“矩陣變量”一節
  • 帶 @RequestParam 注解的方法參數,其存放了Servlet請求中所指定的參數。參數的值會被轉換成方法參數所聲明的類型。詳見“使用@RequestParam注解綁定請求參數至方法參數”一節
  • 帶 @RequestHeader 注解的方法參數,其存放了Servlet請求中所指定的HTTP請求頭的值。參數的值會被轉換成方法參數所聲明的類型。詳見“使用@RequestHeader注解映射請求頭屬性”一節.
  • 帶 @RequestBody 注解的參數,提供了對HTTP請求體的存取。參數的值通過 HttpMessageConverter 被轉換成方法參數所聲明的類型。詳見“使用@RequestBody注解映射請求體”一節"
  • @RequestPart注解的參數,提供了對一個"multipart/form-data請求塊(request part)內容的存取。更多的信息請參考21.10.5 “處理客戶端文件上傳的請求”一節和21.10 “Spring對多部分文件上傳的支持”一節
  •  HttpEntity<?> 類型的參數,其提供了對HTTP請求頭和請求內容的存取。請求流是通過 HttpMessageConverter 被轉換成entity對象的。詳見“HttpEntity”一節
  •  java.util.Map/org.springframework.io.Model/org.springframework.ui.ModelMap  類型的參數,用以增強默認暴露給視圖層的模型(model)的功能
  •  org.springframework.web.servlet.mvc.support.RedirectAttributes 類型的參數,用以指定重定向下要使用到的屬性集以及添加flash屬性(暫存在服務端的屬性,它們會在下次重定向請求的范圍中有效)。詳見“向重定向請求傳遞參數”一節
  • 命令或表單對象,它們用於將請求參數直接綁定到bean字段(可能是通過setter方法)。你可以通過 @InitBinder 注解和/或 HanderAdapter 的配置來定制這個過程的類型轉換。具體請參考 RequestMappingHandlerAdapter類webBindingInitializer 屬性的文檔。這樣的命令對象,以及其上的驗證結果,默認會被添加到模型model中,鍵名默認是該命令對象類的類名——比如, some.package.OrderAddress 類型的命令對象就使用屬性名 orderAddress 類獲取。 ModelAttribute 注解可以應用在方法參數上,用以指定該模型所用的屬性名
  •  org.springframework.validation.Errors / org.springframework.validation.BindingResult 驗證結果對象,用於存儲前面的命令或表單對象的驗證結果(緊接其前的第一個方法參數)。
  •  org.springframework.web.bind.support.SessionStatus 對象,用以標記當前的表單處理已結束。這將觸發一些清理操作: @SessionAttributes 在類級別注解的屬性將被移除
  •  org.springframework.web.util.UriComponentsBuilder 構造器對象,用於構造當前請求URL相關的信息,比如主機名、端口號、資源類型(scheme)、上下文路徑、servlet映射中的相對部分(literal part)等

在參數列表中, Errors 或 BindingResult 參數必須緊跟在其所綁定的驗證對象後面。這是因為,在參數列表中允許有多於一個的模型對象,Spring會為它們創建不同的 BindingResult 實例。因此,下面這樣的代碼是不能工作的:

BindingResult與@ModelAttribute錯誤的參數次序

@RequestMapping(method = RequestMethod.POST)
public String processSubmit(@ModelAttribute("pet") Pet pet, Model model, BindingResult result) { ... }

 

上例中,因為在模型對象Pet和驗證結果對象BindingResult中間還插了一個Model參數,這是不行的。要達到預期的效果,必須調整一下參數的次序:

@RequestMapping(method = RequestMethod.POST)
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result, Model model) { ... }

 

對於一些帶有required屬性的注解(比如 @RequestParam、@RequestHeader 等),JDK 1.8的 java.util.Optional 可以作為被它們注解的方法參數。在這種情況下,使用 java.util.Optional 與 required=false 的作用是相同的。

支持的方法返回類型

以下是handler方法允許的所有返回類型:

  • ModelAndView對象,其中model隱含填充了命令對象,以及注解了@ModelAttribute字段的存取器被調用所返回的值。
  • Model對象,其中視圖名稱默認由RequestToViewNameTranslator決定,model隱含填充了命令對象以及注解了@ModelAttribute字段的存取器被調用所返回的值
  • Map對象,用於暴露model,其中視圖名稱默認由RequestToViewNameTranslator決定,model隱含填充了命令對象以及注解了@ModelAttribute字段的存取器被調用所返回的值
  • View對象。其中model隱含填充了命令對象,以及注解了@ModelAttribute字段的存取器被調用所返回的值。handler方法也可以增加一個Model類型的方法參數來增強model
  • String對象,其值會被解析成一個邏輯視圖名。其中,model將默認填充了命令對象以及注解了@ModelAttribute字段的存取器被調用所返回的值。handler方法也可以增加一個Model類型的方法參數來增強model
  • void。如果處理器方法中已經對response響應數據進行了處理(比如在方法參數中定義一個ServletResponseHttpServletResponse類型的參數並直接向其響應體中寫東西),那麼方法可以返回void。handler方法也可以增加一個Model類型的方法參數來增強model
  • 如果處理器方法注解了ResponseBody,那麼返回類型將被寫到HTTP的響應體中,而返回值會被HttpMessageConverters轉換成所方法聲明的參數類型。詳見使用"@ResponseBody注解映射響應體"一節
  • HttpEntity<?>ResponseEntity<?>對象,用於提供對Servlet HTTP響應頭和響應內容的存取。對象體會被HttpMessageConverters轉換成響應流。詳見使用HttpEntity一節
  • HttpHeaders對象,返回一個不含響應體的response
  • Callable<?>對象。當應用希望異步地返回方法值時使用,這個過程由Spring MVC自身的線程來管理
  • DeferredResult<?>對象。當應用希望方法的返回值交由線程自身決定時使用
  • ListenableFuture<?>對象。當應用希望方法的返回值交由線程自身決定時使用
  • ResponseBodyEmitter對象,可用它異步地向響應體中同時寫多個對象,also supported as the body within a ResponseEntity
  • SseEmitter對象,可用它異步地向響應體中寫服務器端事件(Server-Sent Events),also supported as the body within a ResponseEntity
  • StreamingResponseBody對象,可用它異步地向響應對象的輸出流中寫東西。also supported as the body within a ResponseEntity
  • 其他任何返回類型,都會被處理成model的一個屬性並返回給視圖,該屬性的名稱為方法級的@ModelAttribute所注解的字段名(或者以返回類型的類名作為默認的屬性名)。model隱含填充了命令對象以及注解了@ModelAttribute字段的存取器被調用所返回的值

使用@RequestParam將請求參數綁定至方法參數

你可以使用 @RequestParam 注解將請求參數綁定到你控制器的方法參數上。

下面這段代碼展示了它的用法:

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

    // ,..
}

 

若參數使用了該注解,則該參數默認是必須提供的,但你也可以把該參數標注為非必須的:只需要將 @RequestParam 注解的 required 屬性設置為 false 即可(比如, @RequestParam(path="id", required=false) )。

若所注解的方法參數類型不是String,則類型轉換會自動地發生。詳見"方法參數與類型轉換"一節

若 @RequestParam 注解的參數類型是 Map<String, String> 或者 MultiValueMap<String, String> ,則該Map中會自動填充所有的請求參數。

使用@RequestBody注解映射請求體

方法參數中的 @RequestBody 注解暗示了方法參數應該被綁定了HTTP請求體的值。舉個例子:

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

 

請求體到方法參數的轉換是由 HttpMessageConverter 完成的。 HttpMessageConverter 負責將HTTP請求信息轉換成對象,以及將對象轉換回一個HTTP響應體。對於 @RequestBody 注解, RequestMappingHandlerAdapter 提供了以下幾種默認的 HttpMessageConverter 支持:

  •  ByteArrayHttpMessageConverter 用以轉換字節數組
  •  StringHttpMessageConverter 用以轉換字符串
  •  FormHttpMessageConverter 用以將表格數據轉換成 MultiValueMap<String, String> 或從 MultiValueMap<String, String> 中轉換出表格數據
  •  SourceHttpMessageConverter 用於 javax.xml.transform.Source 類的互相轉換

關於這些轉換器的更多信息,請參考"HTTP信息轉換器"一節。另外,如果使用的是MVC命名空間或Java編程的配置方式,會有更多默認注冊的消息轉換器。更多信息,請參考"啟用MVC Java編程配置或MVC XML命令空間配置"一節。

若你更傾向於閱讀和編寫XML文件,那麼你需要配置一個 MarshallingHttpMessageConverter 並為其提供 org.springframework.oxm 包下的一個 Marshaller 和 Unmarshaller 實現。下面的示例就為你展示如何直接在配置文件中配置它。但如果你的應用是使用MVC命令空間或MVC Java編程的方式進行配置的,則請參考"啟用MVC Java編程配置或MVC XML命令空間配置"這一節。

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="messageConverters">
        <util:list id="beanList">
            <ref bean="stringHttpMessageConverter"/>
            <ref bean="marshallingHttpMessageConverter"/>
        </util:list>
    </property
</bean>

<bean id="stringHttpMessageConverter" class="org.springframework.http.converter.StringHttpMessageConverter"/>

<bean id="marshallingHttpMessageConverter"
        class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
    <property name="marshaller" ref="castorMarshaller"/>
    <property name="unmarshaller" ref="castorMarshaller"/>
</bean>

<bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller"/>

 

注解了 @RequestBody 的方法參數還可以被 @Valid 注解,這樣框架會使用已配置的 Validator 實例來對該參數進行驗證。若你的應用是使用MVC命令空間或MVC Java編程的方式配置的,框架會假設在classpath路徑下存在一個符合JSR-303規范的驗證器,並自動將其作為默認配置。

與 @ModelAttribute 注解的參數一樣,Errors也可以被傳入為方法參數,用於檢查錯誤。如果沒有聲明這樣一個參數,那麼程序會拋出一個 MethodArgumentNotValidException 異常。該異常默認由 DefaultHandlerExceptionResolver 處理,處理程序會返回一個400錯誤給客戶端。

關於如何通過MVC命令空間或MVC Java編程的方式配置消息轉換器和驗證器,也請參考"啟用MVC Java編程配置或MVC XML命令空間配置"一節。

使用@ResponseBody注解映射響應體

 @ResponseBody 注解與 @RequestBody 注解類似。 @ResponseBody 注解可被應用於方法上,標志該方法的返回值(更正,原文是return type,看起來應該是返回值)應該被直接寫回到HTTP響應體中去(而不會被被放置到Model中或被解釋為一個視圖名)。舉個例子:

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

 

上面的代碼結果是文本Hello World將被寫入HTTP的響應流中。

與 @RequestBody 注解類似,Spring使用了一個 HttpMessageConverter 來將返回對象轉換到響應體中。關於這些轉換器的更多信息,請參考"HTTP信息轉換器"一節。

使用@RestController注解創建REST控制器

當今讓控制器實現一個REST API是非常常見的,這種場景下控制器只需要提供JSON、XML或其他自定義的媒體類型內容即可。你不需要在每個 @RequestMapping 方法上都增加一個 @ResponseBody 注解,更簡明的做法是,給你的控制器加上一個 @RestController 的注解。

@RestController是一個原生內置的注解,它結合了 @ResponseBody 與 @Controller 注解的功能。不僅如此,它也讓你的控制器更表義,而且在框架未來的發布版本中,它也可能承載更多的意義。

與普通的 @Controller 無異, @RestController 也可以與 @ControllerAdvicebean 配合使用。更多細節,請見使用@ControllerAdvice輔助控制器。

使用HTTP實體HttpEntity

 HttpEntity 與 @RequestBody 和 @ResponseBody 很相似。除了能獲得請求體和響應體中的內容之外, HttpEntity (以及專門負責處理響應的 ResponseEntity 子類)還可以存取請求頭和響應頭,像下面這樣:

@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 請求頭的值,然後讀取請求體的主體內容。讀完以後往影響頭中添加了一個自己的響應頭 MyResponseHeader ,然後向響應流中寫了字符串Hello World,最後把響應狀態碼設置為201(創建成功)。

與 @RequestBody 與 @ResponseBody 注解一樣,Spring使用了 HttpMessageConverter 來對請求流和響應流進行轉換。關於這些轉換器的更多信息,請閱讀上一小節以及"HTTP信息轉換器"這一節。

對方法使用@ModelAttribute注解

 @ModelAttribute 注解可被應用在方法或方法參數上。本節將介紹其被注解於方法上時的用法,下節會介紹其被用於注解方法參數的用法。

注解在方法上的 @ModelAttribute 說明了方法的作用是用於添加一個或多個屬性到model上。這樣的方法能接受與 @RequestMapping 注解相同的參數類型,只不過不能直接被映射到具體的請求上。在同一個控制器中,注解了 @ModelAttribute 的方法實際上會在 @RequestMappin g方法之前被調用。以下是幾個例子:

// Add one attribute
// The return value of the method is added to the model under the name "account"
// You can customize the name via @ModelAttribute("myAccount")

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

// Add multiple attributes

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountManager.findAccount(number));
    // add more ...
}

 

 @ModelAttribute 方法通常被用來填充一些公共需要的屬性或數據,比如一個下拉列表所預設的幾種狀態,或者寵物的幾種類型,或者去取得一個HTML表單渲染所需要的命令對象,比如Account等。

留意 @ModelAttribute 方法的兩種風格。在第一種寫法中,方法通過返回值的方式默認地將添加一個屬性;在第二種寫法中,方法接收一個Model對象,然後可以向其中添加任意數量的屬性。你可以在根據需要,在兩種風格中選擇合適的一種。

一個控制器可以擁有數量不限的 @ModelAttribute 方法。同個控制器內的所有這些方法,都會在 @RequestMapping 方法之前被調用。

@ModelAttribute方法也可以定義在 @ControllerAdvice 注解的類中,並且這些 @ModelAttribute 可以同時對許多控制器生效。具體的信息可以參考使用@ControllerAdvice輔助控制器。

屬性名沒有被顯式指定的時候又當如何呢?在這種情況下,框架將根據屬性的類型給予一個默認名稱。舉個例子,若方法返回一個Account類型的對象,則默認的屬性名為"account"。你可以通過設置 @ModelAttribute 注解的值來改變默認值。當向Model中直接添加屬性時,請使用合適的重載方法addAttribute(..)-即,帶或不帶屬性名的方法。

 @ModelAttribute 注解也可以被用在 @RequestMapping 方法上。這種情況下,@RequestMapping方法的返回值將會被解釋為model的一個屬性,而非一個視圖名。此時視圖名將以視圖命名約定來方式來決議,與返回值為void的方法所采用的處理方法類似——請見視圖:請求與視圖名的對應。

在方法參數上使用@ModelAttribute注解

如上一小節所解釋, @ModelAttribute 注解既可以被用在方法上,也可以被用在方法參數上。這一小節將介紹它注解在方法參數上時的用法。

注解在方法參數上的 @ModelAttribute 說 明了該方法參數的值將由model中取得。如果model中找不到,那麼該參數會先被實例化,然後被添加到model中。在model中存在以後,請求中 所有名稱匹配的參數都會填充到該參數中。這在Spring MVC中被稱為數據綁定,一個非常有用的特性,節約了你每次都需要手動從表格數據中轉換這些字段數據的時間。

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute Pet pet) { }

 

以上面的代碼為例,這個Pet類型的實例可能來自哪裡呢?有幾種可能:

  • 它可能因為 @SessionAttributes 注解的使用已經存在於model中——詳見"在請求之間使用@SessionAttributes注解,使用HTTP會話保存模型數據"一節
  • 它可能因為在同個控制器中使用了 @ModelAttribute 方法已經存在於model中——正如上一小節所敘述的
  • 它可能是由URI模板變量和類型轉換中取得的(下面會詳細講解)
  • 它可能是調用了自身的默認構造器被實例化出來的

 @ModelAttribute 方法常用於從數據庫中取一個屬性值,該值可能通過 @SessionAttributes 注解在請求中間傳遞。在一些情況下,使用URI模板變量和類型轉換的方式來取得一個屬性是更方便的方式。這裡有個例子:

@RequestMapping(path = "/accounts/{account}", method = RequestMethod.PUT)
public String save(@ModelAttribute("account") Account account) {

}

 

上面這個例子中,model屬性的名稱("account")與URI模板變量的名稱相匹配。如果你配置了一個可以將String類型的賬戶值轉換成Account類型實例的轉換器 Converter<String, Account> ,那麼上面這段代碼就可以工作的很好,而不需要再額外寫一個 @ModelAttribute 方法。

下一步就是數據的綁定。 WebDataBinder 類能將請求參數——包括字符串的查詢參數和表單字段等——通過名稱匹配到model的屬性上。成功匹配的字段在需要的時候會進行一次類型轉換(從String類型到目標字段的類型),然後被填充到model對應的屬性中。數據綁定和數據驗證的問題在第8章 驗證,數據綁定和類型轉換中提到。如何在控制器層來定制數據綁定的過程,在這一節 "定制WebDataBinder的初始化"中提及。

進行了數據綁定後,則可能會出現一些錯誤,比如沒有提供必須的字段、類型轉換過程的錯誤等。若想檢查這些錯誤,可以在注解了 @ModelAttribute 的參數緊跟著聲明一個 BindingResult 參數:

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {
    if (result.hasErrors()) {
        return "petForm";
    }

    // ...

}

 

拿到 BindingResult 參數後,你可以檢查是否有錯誤。有時你可以通過Spring的 <errors> 表單標簽來在同一個表單上顯示錯誤信息。

 BindingResult 被用於記錄數據綁定過程的錯誤,因此除了數據綁定外,你還可以把該對象傳給自己定制的驗證器來調用驗證。這使得數據綁定過程和驗證過程出現的錯誤可以被搜集到一處,然後一並返回給用戶:

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {

    new PetValidator().validate(pet, result);
    if (result.hasErrors()) {
        return "petForm";
    }

    // ...

}

 

又或者,你可以通過添加一個JSR-303規范的 @Valid 注解,這樣驗證器會自動被調用。

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) {

    if (result.hasErrors()) {
        return "petForm";
    }

    // ...

}

 

關於如何配置並使用驗證,可以參考第8.8小節 "Spring驗證"和第8章 驗證,數據綁定和類型轉換

在請求之間使用@SessionAttributes注解,使用HTTP會話保存模型數據

類型級別的 @SessionAttributes 注解聲明了某個特定處理器所使用的會話屬性。通常它會列出該類型希望存儲到 session 或 converstaion 中的model屬性名或model的類型名,一般是用於在請求之間保存一些表單數據的 bean 。

以下的代碼段演示了該注解的用法,它指定了模型屬性的名稱

@Controller
@RequestMapping("/editPet.do")
@SessionAttributes("pet")
public class EditPetForm {
    // ...
}

 

使用"application/x-www-form-urlencoded"數據

上一小節講述了如何使用 @ModelAttribute 支 持客戶端浏覽器的多次表單提交請求。對於不是使用的浏覽器的客戶端,我們也推薦使用這個注解來處理請求。但當請求是一個HTTP PUT方法的請求時,有一個事情需要注意。浏覽器可以通過HTTP的GET方法或POST方法來提交表單數據,非浏覽器的客戶端還可以通過HTTP的 PUT方法來提交表單。這就設計是個挑戰,因為在Servlet規范中明確規定, ServletRequest.getParameter*() 系列的方法只能支持通過HTTP POST方法的方式提交表單,而不支持HTTP PUT的方式。

為了支持HTTP的PUT類型和PATCH類型的請求,Spring的spring-web模塊提供了一個過濾器 HttpPutFormContentFilter 。你可以在web.xml文件中配置它:

 <filter>
        <filter-name>httpPutFormFilter</filter-name>
        <filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>httpPutFormFilter</filter-name>
        <servlet-name>dispatcherServlet</servlet-name>
    </filter-mapping>

    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>

 

上面的過濾器將會攔截內容類型(content type)為 application/x-www-form-urlencoded 、HTTP方法為PUT或PATCH類型的請求,然後從請求體中讀取表單數據,把它們包裝在 ServletRequest 中。這是為了使表單數據能夠通過 ServletRequest.getParameter*() 系列的方法來拿到。

因為 HttpPutFormContentFilter 會消費請求體的內容,因此,它不應該用於處理那些依賴於其他 application/x-www-form-urlencoded 轉換器的PUT和PATCH請求,這包括了 @RequestBodyMultiValueMap<String, String> 和 HttpEntity<MultiValueMap<String, String>> 。

 @CookieValue 注解能將一個方法參數與一個HTTP cookie的值進行綁定。

看一個這樣的場景:以下的這個cookie存儲在一個HTTP請求中:

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

下面的代碼演示了拿到JSESSIONID這個cookie值的方法:

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

 

若注解的目標方法參數不是String類型,則類型轉換會自動進行。詳見"方法參數與類型轉換"一節。

這個注解可以注解到處理器方法上,在Servlet環境和Portlet環境都能使用。

使用@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) {
    //...
}

 

若注解的目標方法參數不是String類型,則類型轉換會自動進行。"方法參數與類型轉換"一節。

如果 @RequestHeader 注解應用在 Map<String, String>、MultiValueMap<String, String> 或 HttpHeaders 類型的參數上,那麼所有的請求頭屬性值都會被填充到map中。

Spring內置支持將一個逗號分隔的字符串(或其他類型轉換系統所能識別的類型)轉換成一個String類型的列表/集合。舉個例子,一個注解了 @RequestHeader("Accept") 的方法參數可以是一個String類型,但也可以是String[]List<String>類型的。

這個注解可以注解到處理器方法上,在Servlet環境和Portlet環境都能使用。

方法參數與類型轉換

從請求參數、路徑變量、請求頭屬性或者cookie中抽取出來的String類型的值,可能需要被轉換成其所綁定的目標方法參數或字段的類型(比如,通過 @ModelAttribute 將請求參數綁定到方法參數上)。如果目標類型不是String,Spring會自動進行類型轉換。所有的簡單類型諸如int、long、Date都有內置的支持。如果想進一步定制這個轉換過程,你可以通過 WebDataBinder (詳見"定制WebDataBinder的初始化"一節),或者為 Formatters 配置一個 FormattingConversionService (詳見8.6節 "Spring字段格式化"一節)來做到。

定制WebDataBinder的初始化

如果想通過Spring的 WebDataBinder 在屬性編輯器中做請求參數的綁定,你可以使用在控制器內使用 @InitBinder  @InitBinder 注解的方法、在注解了 @ControllerAdvice 的類中使用 @InitBinder 注解的方法,或者提供一個定制的 WebBindingInitializer 。更多的細節,請參考使用@ControllerAdvice輔助控制器一節。

數據綁定的定制:使用@InitBinder

使用 @InitBinder 注解控制器的方法,你可以直接在你的控制器類中定制應用的數據綁定。 @InitBinder 用來標記一些方法,這些方法會初始化一個WebDataBinder並用以為處理器方法填充命令對象和表單對象的參數。

除了命令/表單對象以及相應的驗證結果對象,這樣的“綁定器初始化”方法能夠接收 @RequestMapping 所支持的所有參數類型。“綁定器初始化”方法不能有返回值,因此,一般將它們聲明為void返回類型。特別地,當WebDataBinderWebRequestjava.util.Locale一起作為方法參數時,你可以在代碼中注冊上下文相關的編輯器。

下面的代碼示例演示了如何使用@InitBinder來配置一個CustomerDateEditor,後者會對所有java.util.Date類型的表單字段進行操作:

@Controller
public class MyFormController {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

    // ...
}

 

或者,你可以使用Spring 4.2提供的 addCustomFormatter 來指定 Formatter 的實現,而非通過 PropertyEditor 實例。這在你擁有一個需要Formatter的setup方法,並且該方法位於一個共享的 FormattingConversionService中 時非常有用。這樣對於控制器級別的綁定規則的定制,代碼更容易被復用。

@Controller
public class MyFormController {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
    }

    // ...
}

 

配置定制的WebBindingInitializer

為了externalize數據綁定的初始化過程,你可以為 WebBindingInitializer 接口提供一個自己的實現,在其中你可以為AnnotationMethodHandlerAdapter提供一個默認的配置bean,以此來覆寫默認的配置。

以下的代碼來自PetClinic的應用,它展示了為WebBindingInitializer接口提供一個自定義實現: org.springframework.samples.petclinic.web.ClinicBindingInitializer  org.springframework.samples.petclinic.web.ClinicBindingInitializer 完整的配置過程。後者中配置了PetClinic應用中許多控制器所需要的屬性編輯器PropertyEditors。

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="cacheSeconds" value="0"/>
    <property name="webBindingInitializer">
        <bean class="org.springframework.samples.petclinic.web.ClinicBindingInitializer"/>
    </property>
</bean>

 

@InitBinder方法也可以定義在 @ControllerAdvice 注解的類上,這樣配置可以為許多控制器所共享。這提供了除使用WebBindingInitializer外的另外一種方法。更多細節請參考使用@ControllerAdvice輔助控制器一節。

使用@ControllerAdvice輔助控制器

 @ControllerAdvice 是一個組件注解,它使得其實現類能夠被classpath掃描自動發現。若應用是通過MVC命令空間或MVC Java編程方式配置,那麼該特性默認是自動開啟的。

注解 @ControllerAdvice 的類可以擁有 @ExceptionHandler、@InitBinder 或 @ModelAttribute 注解的方法,並且這些方法會被應用至控制器類層次??的所有 @RequestMapping 方法上。

你也可以通過@ControllerAdvice的屬性來指定其只對一個子集的控制器生效:

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class AnnotationAdvice {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class BasePackageAdvice {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class AssignableTypesAdvice {}

 

更多的細節,請查閱@ControllerAdvice的文檔。

下面兩節,還看不太懂,待譯。

Jackson Serialization View Support

It can sometimes be useful to filter contextually the object that will be serialized to the HTTP response body. In order to provide such capability, Spring MVC has built-in support for rendering with Jackson's Serialization Views.

To use it with an @ResponseBody controller method or controller methods that return ResponseEntity, simply add the @JsonView annotation with a class argument specifying the view class or interface to be used:

_@RestController_
public class UserController {

    _@RequestMapping(path = "/user", method = RequestMethod.GET)_
    _@JsonView(User.WithoutPasswordView.class)_
    public User getUser() {
        return new User("eric", "7!jd#h23");
    }
}

public class User {

    public interface WithoutPasswordView {};
    public interface WithPasswordView extends WithoutPasswordView {};

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    _@JsonView(WithoutPasswordView.class)_
    public String getUsername() {
        return this.username;
    }

    _@JsonView(WithPasswordView.class)_
    public String getPassword() {
        return this.password;
    }
}

 

\[Note\]Note  

Note that despite  @JsonView  allowing for more than one class to be specified, the use on a controller method is only supported with exactly one class argument. Consider the use of a composite interface if you need to enable multiple views.

For controllers relying on view resolution, simply add the serialization view class to the model:

_@Controller_
public class UserController extends AbstractController {

    _@RequestMapping(path = "/user", method = RequestMethod.GET)_
    public String getUser(Model model) {
        model.addAttribute("user", new User("eric", "7!jd#h23"));
        model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class);
        return "userView";
    }
}

 

Jackson JSONP Support

In order to enable JSONP support for  @ResponseBody  and  ResponseEntity  methods, declare an  @ControllerAdvice  bean that extends  AbstractJsonpResponseBodyAdvice  as shown below where the constructor argument indicates the JSONP query parameter name(s):

_@ControllerAdvice_
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {

    public JsonpAdvice() {
        super("callback");
    }
}

 

For controllers relying on view resolution, JSONP is automatically enabled when the request has a query parameter named  jsonp  or  callback . Those names can be customized through  jsonpParameterNames  property.

21.3.4 異步請求的處理

Spring MVC 3.2開始引入了基於Servlet 3的異步請求處理。相比以前,控制器方法已經不一定需要返回一個值,而是可以返回一個 java.util.concurrent.Callable 的對象,並通過Spring MVC所管理的線程來產生返回值。與此同時,Servlet容器的主線程則可以退出並釋放其資源了,同時也允許容器去處理其他的請求。通過一個TaskExecutor,Spring MVC可以在另外的線程中調用Callable。當Callable返回時,請求再攜帶Callable返回的值,再次被分配到Servlet容器中恢復處理流程。以下代碼給出了一個這樣的控制器方法作為例子:

@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>();
    // Save the deferredResult somewhere..
    return deferredResult;
}

// In some other thread...
deferredResult.setResult(data);

 

如果對Servlet 3.0的異步請求處理特性沒有了解,理解這個特性可能會有點困難。因此,閱讀一下前者的文檔將會很有幫助。以下給出了這個機制運作背後的一些原理:

  • 一個servlet請求 ServletRequest 可以通過調用 request.startAsync() 方法而進入異步模式。這樣做的主要結果就是該servlet以及所有的過濾器都可以結束,但其響應(response)會留待異步處理結束後再返回
  • 調用 request.startAsync() 方法會返回一個 AsyncContext 對象,可用它對異步處理進行進一步的控制和操作。比如說它也提供了一個與轉向(forward)很相似的dispatch方法,只不過它允許應用恢復Servlet容器的請求處理進程
  •  ServletRequest 提供了獲取當前 DispatherType 的方式,後者可以用來區別當前處理的是原始請求、異步分發請求、轉向,或是其他類型的請求分發類型。

有了上面的知識,下面可以來看一下 Callable  的異步請求被處理時所依次發生的事件:

  • 控制器先返回一個Callable對象
  • Spring MVC開始進行異步處理,並把該Callable對象提交給另一個獨立線程的執行器TaskExecutor處理
  • DispatcherServlet和所有過濾器都退出Servlet容器線程,但此時方法的響應對象仍未返回
  • Callable對象最終產生一個返回結果,此時Spring MVC會重新把請求分派回Servlet容器,恢復處理
  • DispatcherServlet再次被調用,恢復對Callable異步處理所返回結果的處理

DeferredResult異步請求的處理順序也非常類似,區別僅在於應用可以通過任何線程來計算返回一個結果:

  • 控制器先返回一個DeferredResult對象,並把它存取在內存(隊列或列表等)中以便存取
  • Spring MVC開始進行異步處理
  • DispatcherServlet和所有過濾器都退出Servlet容器線程,但此時方法的響應對象仍未返回
  • 由處理該請求的線程對 DeferredResult進行設值,然後Spring MVC會重新把請求分派回Servlet容器,恢復處理
  • DispatcherServlet再次被調用,恢復對該異步返回結果的處理

關於引入異步請求處理的背景和原因,以及什麼時候使用它、為什麼使用異步請求處理等問題,你可以從這個系列的博客中了解更多信息。

異步請求的異常處理

若控制器返回的Callable在執行過程中拋出了異常,又會發生什麼事情?簡單來說,這與一般的控制器方法拋出異常是一樣的。它會被正常的異常處理流程捕獲處理。更具體地說呢,當Callable拋出異常時,Spring MVC會把一個Exception對象分派給Servlet容器進行處理,而不是正常返回方法的返回值,然後容器恢復對此異步請求異常的處理。若方法返回的是一個DeferredResult對象,你可以選擇調Exception實例的setResult方法還是setErrorResult方法。

攔截異步請求

處理器攔截器 HandlerInterceptor 可以實現 AsyncHandlerInterceptor 接口攔截異步請求,因為在異步請求開始時,被調用的回調方法是該接口的 afterConcurrentHandlingStarted 方法,而非一般的 postHandle 和 afterCompletion 方法。

如果需要與異步請求處理的生命流程有更深入的集成,比如需要處理timeout的事件等,則 HandlerInterceptor 需要注冊一個 CallableProcessingInterceptor 或 DeferredResultProcessingInterceptor 攔截器。具體的細節可以參考 AsyncHandlerInterceptor 類的Java文檔。

 DeferredResult 類還提供了 onTimeout(Runnable) 和 onCompletion(Runnable) 等方法,具體的細節可以參考DeferredResult類的Java文檔。

Callable需要請求過期(timeout)和完成後的攔截時,可以把它包裝在一個WebAsyncTask實例中,後者提供了相關的支持。

HTTP streaming(不知道怎麼翻)

如前所述,控制器可以使用DeferredResultCallable對象來異步地計算其返回值,這可以用於實現一些有用的技術,比如 long polling技術,讓服務器可以盡可能快地向客戶端推送事件。

如果你想在一個HTTP響應中同時推送多個事件,怎麼辦?這樣的技術已經存在,與"Long Polling"相關,叫"HTTP Streaming"。Spring MVC支持這項技術,你可以通過讓方法返回一個 ResponseBodyEmitte r類型對象來實現,該對象可被用於發送多個對象。通常我們所使用的 @ResponseBody 只能返回一個對象,它是通過 HttpMessageConverter 寫到響應體中的。

下面是一個實現該技術的例子:

@RequestMapping("/events")
public ResponseBodyEmitter handle() {
    ResponseBodyEmitter emitter = new ResponseBodyEmitter();
    // Save the emitter somewhere..
    return emitter;
}

// In some other thread
emitter.send("Hello once");

// and again later on
emitter.send("Hello again");

// and done at some point
emitter.complete();

 

 ResponseBodyEmitter 也可以被放到 ResponseEntity 體裡面使用,這可以對響應狀態和響應頭做一些定制。

Note that ResponseBodyEmitter can also be used as the body in a ResponseEntity in order to customize the status and headers of the response.

使用“服務器端事件推送”的HTTP Streaming

 SseEmitter 是 ResponseBodyEmitter 的一個子類,提供了對服務器端事件推送的技術的支持。服務器端事件推送其實只是一種HTTP Streaming的類似實現,只不過它服務器端所推送的事件遵循了W3C Server-Sent Events規范中定義的事件格式。

“服務器端事件推送”技術正如其名,是用於由服務器端向客戶端進行的事件推送。這在Spring MVC中很容易做到,只需要方法返回一個 SseEmitter 類型的對象即可。

需 要注意的是,Internet Explorer並不支持這項服務器端事件推送的技術。另外,對於更大型的web應用及更精致的消息傳輸場景——比如在線游戲、在線協作、金融應用等—— 來說,使用Spring的WebSocket(包含SockJS風格的實時WebSocket)更成熟一些,因為它支持的浏覽器范圍非常廣(包括IE), 並且,對於一個以消息為中心的架構中,它為服務器端-客戶端間的事件發布-訂閱模型的交互提供了更高層級的消息模式(messaging patterns)的支持。

直接寫回輸出流OutputStream的HTTP Streaming

 ResponseBodyEmitter  ResponseBodyEmitter 也允許通過 HttpMessageConverter 向響應體中支持寫事件對象。這可能是最常見的情形,比如寫返回的JSON數據的時候。但有時,跳過消息轉換的階段,直接把數據寫回響應的輸出流 OutputStream 可能更有效,比如文件下載這樣的場景。這可以通過返回一個 StreamingResponseBody 類型的對象來實現。

以下是一個實現的例子:

@RequestMapping("/download")
public StreamingResponseBody handle() {
    return new StreamingResponseBody() {
        @Override
        public void writeTo(OutputStream outputStream) throws IOException {
            // write...
        }
    };
}

 

 ResponseBodyEmitter 也可以被放到 ResponseEntity 體裡面使用,這可以對響應狀態和響應頭做一些定制。

異步請求處理的相關配置

Servlet容器配置

對於那些使用web.xml配置文件的應用,請確保 web.xml 的版本更新到3.0:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance http://java.sun.com/xml/ns/javaee
                    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">

    ...

</web-app>

 

異步請求必須在web.xmlDispatcherServlet下的子元素 <async-supported>true</async-supported> 設置為true。此外,所有可能參與異步請求處理的過濾器Filter都必須配置為支持ASYNC類型的請求分派。在Spring框架中為過濾器啟用支持ASYNC類型的請求分派應是安全的,因為這些過濾器一般都繼承了基類OncePerRequestFilter,後者在運行時會檢查該過濾器是否需要參與到異步分派的請求處理中。

以下是一個例子,展示了web.xml的配置:

  <web-app xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
                http://java.sun.com/xml/ns/javaee
                http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
        version="3.0">

        <filter>
            <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
            <filter-class>org.springframework.~.OpenEntityManagerInViewFilter</filter-class>
            <async-supported>true</async-supported>
        </filter>

        <filter-mapping>
            <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
            <url-pattern>/*</url-pattern>
            <dispatcher>REQUEST</dispatcher>
            <dispatcher>ASYNC</dispatcher>
        </filter-mapping>

    </web-app>

 

如果應用使用的是Servlet 3規范基於Java編程的配置方式,比如通過 WebApplicationInitializer ,那麼你也需要設置"asyncSupported"標志和ASYNC分派類型的支持,就像你在web.xml中所配置的一樣。你可以考慮直接繼承 AbstractDispatcherServletInitializer 或 AbstractAnnotationConfigDispatcherServletInitializer 來簡化配置,它們都自動地為你設置了這些配置項,並使得注冊Filter過濾器實例變得非常簡單。

Spring MVC配置

MVC Java編程配置和MVC命名空間配置方式都提供了配置異步請求處理支持的選擇。WebMvcConfigurer提供了 configureAsyncSupport 方法,而 <mvc:annotation-driven> 有一個子元素 <async-support> ,它們都用以為此提供支持。

這些配置允許你覆寫異步請求默認的超時時間,在未顯式設置時,它們的值與所依賴的Servlet容器是相關的(比如,Tomcat設置的超時時間是10秒)。你也可以配置用於執行控制器返回值Callable的執行器AsyncTaskExecutor。Spring強烈推薦你配置這個選項,因為Spring MVC默認使用的是普通的執行器 SimpleAsyncTaskExecutor 。MVC Java編程配置及MVC命名空間配置的方式都允許你注冊自己的 CallableProcessingInterceptor 和 DeferredResultProcessingInterceptor 攔截器實例。

若你需要為特定的DeferredResult覆寫默認的超時時間,你可以選用合適的構造方法來實現。類似,對於Callable返回,你可以把它包裝在一個WebAsyncTask對象中,並使用合適的構造方法定義超時時間。WebAsyncTask類的構造方法同時也能接受一個任務執行器AsyncTaskExecutor類型的參數。

21.3.5 對控制器測試

spring-test模塊對測試控制器@Controller提供了最原生的支持。詳見14.6 "Spring MVC測試框架"一節。

21.4 處理器映射(Handler Mappings)

在Spring的上個版本中,用戶需要在web應用的上下文中定義一個或多個的 HandlerMappingbean ,用以將進入容器的web請求映射到合適的處理器方法上。允許在控制器上添加注解後,通常你就不必這麼做了,因為 RequestMappingHandlerMapping 類會自動查找所有注解了 @RequestMapping 的 @Controller 控制器bean。同時也請知道,所有繼承自AbstractHandlerMapping的處理器方法映射HandlerMapping類都擁有下列的屬性,你可以對它們進行定制:

  • 一個interceptors列表,指示了應用其上的一個攔截器列表。處理器方法攔截器會在 21.4.1小節 使用HandlerInterceptor攔截請求中討論。
  • defaultHandler,生效的默認處理器,when this handler mapping does not result in a matching handler.
  • order,根據order(見org.springframework.core.Ordered接口)屬性的值,Spring會對上下文可用的所有處理器映射進行排序,並應用第一個匹配成功的處理器
  • alwaysUseFullPath(總是使用完整路徑)。若設置為true,Spring將在當前Servlet上下文中總是使用完整路徑來查找合適的處理器。若設置為false(默認就為false),則使用當前Servlet的mapping路徑。舉個例子,若一個Servlet的mapping路徑是/testing/*,並且 alwaysUseFullPath 屬性被設置為 true ,此時用於查找處理器的路徑將是/testing/viewPage.html;而若alwaysUseFullPath屬性的值為false,則此時查找路徑是/viewPage.html
  • urlDecode,默認設置為true(也是Spring 2.5的默認設置)。若你需要比較加密過的路徑,則把此標志設為false。需要注意的是,HttpServletRequest永遠以未加密的方式存儲Servlet路徑。此時,該路徑將無法匹配到加密過的路徑

下面的代碼展示了配置一個攔截器的方法:

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

 

21.4.1 使用HandlerInterceptor攔截請求

Spring的處理器映射機制包含了處理器攔截器。攔截器在你需要為特定類型的請求應用一些功能時可能很有用,比如,檢查用戶身份等。

處理器映射處理過程配置的攔截器,必須實現  org.springframework.web.servlet 包下的  HandlerInterceptor 接口。這個接口定義了三個方法:  preHandle(..) ,它在處理器實際執行 之前 會被執行;  postHandle(..) ,它在處理器執行 完畢 以後被執行;  afterCompletion(..) ,它在 整個請求處理完成 之後被執行。這三個方法為各種類型的前處理和後處理需求提供了足夠的靈活性。

preHandle(..)方法返回一個boolean值。你可以通過這個方法來決定是否繼續執行處理鏈中的部件。當方法返回  true 時,處理器鏈會繼續執行;若方法返回  false , DispatcherServlet即認為攔截器自身已經完成了對請求的處理(比如說,已經渲染了一個合適的視圖),那麼其余的攔截器以及執行鏈中的其他處理器就不會再被執行了。

攔截器可以通過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>

 

package samples;

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;
    }
}

 

在上面的例子中,所有被此處理器處理的請求都會被 TimeBasedAccessInterceptor 攔截器攔截。如果當前時間在工作時間以外,那麼用戶就會被重定向到一個HTML文件提示用戶,比如顯示“你只有在工作時間才可以訪問本網站”之類的信息。

使用 RequestMappingHandlerMapping 時,實際的處理器是一個處理器方法 HandlerMethod 的實例,它標識了一個將被用於處理該請求的控制器方法。

如你所見,Spring的攔截器適配器 HandlerInterceptorAdapter 讓繼承 HandlerInterceptor 接口變得更簡單了。

上面的例子中,所有控制器方法處理的請求都會被配置的攔截器先攔截到。如果你想進一步縮小攔截的URL范圍,你可以通過MVC命名空間或MVC Java編程的方式來配置,或者,聲明一個 MappedInterceptor 類型的bean實例來處理。具體請見 21.16.1 啟用MVC Java編程配置或MVC命名空間配置一小節。

需要注意的是,HandlerInterceptor的後攔截postHandle方法不一定總是適用於注解了@ResponseBodyResponseEntity的方法。這些場景中,HttpMessageConverter會在攔截器的postHandle方法被調之前就把信息寫回響應中。這樣攔截器就無法再改變響應了,比如要增加一個響應頭之類的。如果有這種需求,請讓你的應用實現ResponseBodyAdvice接口,並將其定義為一個@ControllerAdvicebean或直接在RequestMappingHandlerMapping中配置。

21.5 視圖解析

所 有web應用的MVC框架都提供了視圖相關的支持。Spring提供了一些視圖解析器,它們讓你能夠在浏覽器中渲染模型,並支持你自由選用適合的視圖技術 而不必與框架綁定到一起。Spring原生支持JSP視圖技術、Velocity模板技術和XSLT視圖等。你可以閱讀文檔的第22章 視圖技術一章,裡面討論了如何集成並使用許多獨立的視圖技術。

有兩個接口在Spring處理視圖相關事宜時至關重要,分別是視圖解析器接口 ViewResolver 和視圖接口本身View。視圖解析器 ViewResolver 負責處理視圖名與實際視圖之間的映射關系。視圖接口View負責准備請求,並將請求的渲染交給某種具體的視圖技術實現。

21.5.1 使用ViewResolver接口解析視圖

正如在21.3 控制器的實現一節中所討論的,Spring MVC中所有控制器的處理器方法都必須返回一個邏輯視圖的名字,無論是顯式返回(比如返回一個StringView或者ModelAndView)還是隱式返回(比如基於約定的返回)。Spring中的視圖由一個視圖名標識,並由視圖解析器來渲染。Spring有非常多內置的視圖解析器。下表列出了大部分,表後也給出了一些例子。

表21.3 視圖解析器

視圖解析器描述 AbstractCachingViewResolver 一個抽象的視圖解析器類,提供了緩存視圖的功能。通常視圖在能夠被使用之前需要經過准備。繼承這個基類的視圖解析器即可以獲得緩存視圖的能力。 XmlViewResolver 視圖解析器接口ViewResolver的一個實現,該類接受一個XML格式的配置文件。該XML文件必須與Spring XML的bean工廠有相同的DTD。默認的配置文件名是/WEB-INF/views.xmlResourceBundleViewResolver 視圖解析器接口ViewResolver的一個實現,采用bundle根路徑所指定的ResourceBundle中的bean定義作為配置。一般bundle都定義在classpath路徑下的一個配置文件中。默認的配置文件名為views.propertiesUrlBasedViewResolver ViewResolver接口的一個簡單實現。它直接使用URL來解析到邏輯視圖名,除此之外不需要其他任何顯式的映射聲明。如果你的邏輯視圖名與你真正的視圖資源名是直接對應的,那麼這種直接解析的方式就很方便,不需要你再指定額外的映射。 InternalResourceViewResolver UrlBasedViewResolver的一個好用的子類。它支持內部資源視圖(具體來說,Servlet和JSP)、以及諸如JstlViewTilesView等類的子類。You can specify the view class for all views generated by this resolver by using setViewClass(..)。更多的細節,請見UrlBasedViewResolver類的java文檔。 VelocityViewResolver / FreeMarkerViewResolver UrlBasedViewResolver下的實用子類,支持Velocity視圖VelocityView(Velocity模板)和FreeMarker視圖FreeMarkerView以及它們對應子類。 ContentNegotiatingViewResolver 視圖解析器接口ViewResolver的一個實現,它會根據所請求的文件名或請求的Accept頭來解析一個視圖。更多細節請見21.5.4 內容協商視圖解析器"ContentNegotiatingViewResolver"一小節。

我們可以舉個例子,假設這裡使用的是JSP視圖技術,那麼我們可以使用一個基於URL的視圖解析器UrlBasedViewResolver。這個視圖解析器會將URL解析成一個視圖名,並將請求轉交給請求分發器來進行視圖渲染。

<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 ,後者會將請求交給 /WEB-INF/jsp/test.jsp 視圖去渲染。

如果需要在應用中使用多種不同的視圖技術,你可以使用 ResourceBundleViewResolver :

<bean id="viewResolver"
        class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <property name="basename" value="views"/>
    <property name="defaultParentView" value="parentView"/>
</bean>

 

 ResourceBundleViewResolver 會檢索由bundle根路徑下所配置的 ResourceBundle ,對於每個視圖而言,其視圖類由[viewname].(class)屬性的值指定,其視圖url由[viewname].url屬性的值指定。下一節將詳細講解視圖技術,你可以在那裡找到更多例子。你還可以看到,視圖還允許有基視圖,即properties文件中所有視圖都“繼承”的一個文件。通過繼承技術,你可以為眾多視圖指定一個默認的視圖基類。

AbstractCachingViewResolver的子類能夠緩存已經解析過的視圖實例。關閉緩存特性也是可以的,只需要將cache屬性設置為false即可。另外,如果實在需要在運行時刷新某個視圖(比如修改了Velocity模板時),你可以使用removeFromCache(String viewName, Locale loc)方法。`

21.5.2 視圖鏈

Spring支持同時使用多個視圖解析器。因此,你可以配置一個解析器鏈,並做更多的事比如,在特定條件下覆寫一個視圖等。你可以通過把多個視圖解析器設置到應用上下文(application context)中的方式來串聯它們。如果需要指定它們的次序,那麼設置order屬性即可。請記住,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>

<!-- in views.xml -->

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

 

如果一個視圖解析器不能返回一個視圖,那麼Spring會繼續檢查上下文中其他的視圖解析器。此時如果存在其他的解析器,Spring會繼續調用它們,直到產生一個視圖返回為止。如果最後所有視圖解析器都不能返回一個視圖,Spring就拋出一個ServletException

視圖解析器的接口清楚聲明了,一個視圖解析器是可以返回null值的,這表示不能找到任何合適的視圖。並非所有的視圖解析器都這麼做,但是也存在不得不如此的場景,即解析器確實無法檢測對應的視圖是否存在。比如,InternalResourceViewResolver在內部使用了RequestDispatcher,並且進入分派過程是檢測一個JSP視圖是否存在的唯一方法,但這個過程僅可能發生唯一一次。同樣的VelocityViewResolver和部分其他的視圖解析器也存在這樣的情況。具體的請查閱某個特定的視圖解析器的Java文檔,看它是否會report不存在的視圖。因此,如果不把InternalResourceViewResolver放置在解析器鏈的最後,將可能導致解析器鏈無法完全執行,因為InternalResourceViewResolver永遠都會 返回一個視圖。

21.5.3 視圖重定向

如前所述,控制器通常都會返回一個邏輯視圖名,然後視圖解析器會把它解析到一個具體的視圖技術上去渲染。對於一些可以由Servlet或JSP引擎來處理的視圖技術,比如JSP等,這個解析過程通常是由 InternalResourceViewResolver和InternalResourceView 協作來完成的,而這通常會調用Servlet的 APIRequestDispatcher.forward(..) 方法或 RequestDispatcher.include(..) 方法,並發生一次內部的轉發(forward)或引用(include)。而對於其他的視圖技術,比如Velocity、XSLT等,視圖本身的內容是直接被寫回響應流中的。

有時,我們想要在視圖渲染之前,先把一個HTTP重定向請求發送回客戶端。比如,當一個控制器成功地接受到了POST過來的數據,而響應僅僅是委托另一個控制器來處理(比如一次成功的表單提交)時,我們希望發生一次重定向。在這種場景下,如果只是簡單地使用內部轉發,那麼意味著下一個控制器也能看到這次POST請求攜帶的數據,這可能導致一些潛在的問題,比如可能會與其他期望的數據混淆,等。此外,另一種在渲染視圖前對請求進行重定向的需求是,防止用戶多次提交表單的數據。此時若使用重定向,則浏覽器會先發送第一個POST請求;請求被處理後浏覽器會收到一個重定向響應,然後浏覽器直接被重定向到一個不同的URL,最後浏覽器會使用重定向響應中攜帶的URL發起一次GET請求。因此,從浏覽器的角度看,當前所見的頁面並不是POST請求的結果,而是一次GET請求的結果。這就防止了用戶因刷新等原因意外地提交了多次同樣的數據。此時刷新會重新GET一次結果頁,而不是把同樣的POST數據再發送一遍。

重定向視圖 RedirectView

強制重定向的一種方法是,在控制器中創建並返回一個Spring重定向視圖RedirectView的實例。它會使得DispatcherServlet放棄使用一般的視圖解析機制,因為你已經返回一個(重定向)視圖給DispatcherServlet了,所以它會構造一個視圖來滿足渲染的需求。緊接著RedirectView會調用 HttpServletResponse.sendRedirect() 方法,發送一個HTTP重定向響應給客戶端浏覽器。

如果你決定返回RedirectView,並且這個視圖實例是由控制器內部創建出來的,那我們更推薦在外部配置重定向URL然後注入到控制器中來,而不是寫在控制器裡面。這樣它就可以與視圖名一起在配置文件中配置。關於如何實現這個解耦,請參考 重定向前綴——redirect:一小節。

向重定向目標傳遞數據

模 型中的所有屬性默認都會考慮作為URI模板變量被添加到重定向URL中。剩下的其他屬性,如果是基本類型或者基本類型的集合或數組,那它們將被自動添加到 URL的查詢參數中去。如果model是專門為該重定向所准備的,那麼把所有基本類型的屬性添加到查詢參數中可能是我們期望那個的結果。但是,在包含注解 的控制器中,model可能包含了專門作為渲染用途的屬性(比如一個下拉列表的字段值等)。為了避免把這樣的屬性也暴露在URL中,@RequestMapping方法可以聲明一個RedirectAttributes類型的方法參數,用它來指定專門供重定向視圖RedirectView取用的屬性。如果重定向成功發生,那麼RedirectAttributes對象中的內容就會被使用;否則則使用模型model中的數據。

RequestMappingHandlerAdapter提供了一個"ignoreDefaultModelOnRedirect"標志。它被用來標記默認Model中的屬性永遠不應該被用於控制器方法的重定向中。控制器方法應該聲明一個RedirectAttributes類的參數。如果不聲明,那就沒有參數被傳遞到重定向的視圖RedirectView中。在MVC命名空間或MVC Java編程配置方式中,為了維持向後的兼容性,這個標志都仍被保持為false。但如果你的應用是一個新的項目,那麼我們推薦把它的值設置成true

請注意,當前請求URI中的模板變量會在填充重定向URL的時候自動對應用可見,而不需要顯式地在ModelRedirectAttributes中再添加屬性。請看下面的例子:

@RequestMapping(path = "/files/{path}", method = RequestMethod.POST)
public String upload(...) {
    // ...
    return "redirect:files/{path}";
}

 

另外一種向重定向目標傳遞數據的方法是通過 閃存屬性(Flash Attributes)。與其他重定向屬性不同,flash屬性是存儲在HTTP session中的(因此不會出現在URL中)。更多內容,請參考 21.6 使用閃存屬性一節。

重定向前綴——redirect:

盡管使用RedirectView來做重定向能工作得很好,但如果控制器自身還是需要創建一個RedirectView, 那無疑控制器還是了解重定向這麼一件事情的發生。這還是有點不盡完美,不同范疇的耦合還是太強。控制器其實不應該去關心響應會如何被渲染。In general it should operate only in terms of view names that have been injected into it.

一個特別的視圖名前綴能完成這個解耦:redirect:。如果返回的視圖名中含有redirect:前綴,那麼UrlBasedViewResolver(及它的所有子類)就會接受到這個信號,意識到這裡需要發生重定向。然後視圖名剩下的部分會被解析成重定向URL。

這種方式與通過控制器返回一個重定向視圖RedirectView所達到的效果是一樣的,不過這樣一來控制器就可以只專注於處理並返回邏輯視圖名了。如果邏輯視圖名是這樣的形式:redirect:/myapp/some/resource,他們重定向路徑將以Servlet上下文作為相對路徑進行查找,而邏輯視圖名如果是這樣的形式:redirect:http://myhost.com/some/arbitrary/path,那麼重定向URL使用的就是絕對路徑。

注意的是,如果控制器方法注解了@ResponseStatus,那麼注解設置的狀態碼值會覆蓋RedirectView設置的響應狀態碼值。

重定向前綴——forward:

對於最終會被UrlBasedViewResolver或其子類解析的視圖名,你可以使用一個特殊的前綴:forward:。這會導致一個InternalResourceView視圖對象的創建(它最終會調用RequestDispatcher.forward()方法),後者會認為視圖名剩下的部分是一個URL。因此,這個前綴在使用InternalResourceViewResolverInternalResourceView時並沒有特別的作用(比如對於JSP來說)。但當你主要使用的是其他的視圖技術,而又想要強制把一個資源轉發給Servlet/JSP引擎進行處理時,這個前綴可能就很有用(或者,你也可能同時串聯多個視圖解析器)。

redirect:前綴一樣,如果控制器中的視圖名使用了forward:前綴,控制器本身並不會發覺任何異常,它關注的仍然只是如何處理響應的問題。

21.5.4 內容協商解析器ContentNegotiatingViewResolver

ContentNegotiatingViewResolver自己並不會解析視圖,而是委托給其他的視圖解析器去處理。

The ContentNegotiatingViewResolver does not resolve views itself but rather delegates to other view resolvers, selecting the view that resembles the representation requested by the client. Two strategies exist for a client to request a representation from the server:

  • Use a distinct URI for each resource, typically by using a different file extension in the URI. For example, the URI <http://www.example.com/users/fred.pdf> requests a PDF representation of the user fred, and <http://www.example.com/users/fred.xml> requests an XML representation.
  • Use the same URI for the client to locate the resource, but set the Accept HTTP request header to list the media types that it understands. For example, an HTTP request for <http://www.example.com/users/fred> with an Accept header set to application/pdf requests a PDF representation of the user fred, while <http://www.example.com/users/fred> with an Accept header set to text/xml requests an XML representation. This strategy is known as content negotiation.
\[Note\]Note  

One issue with the Accept header is that it is impossible to set it in a web browser within HTML. For example, in Firefox, it is fixed to:

    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

For this reason it is common to see the use of a distinct URI for each representation when developing browser based web applications.

To support multiple representations of a resource, Spring provides the ContentNegotiatingViewResolver to resolve a view based on the file extension or Accept header of the HTTP request. ContentNegotiatingViewResolver does not perform the view resolution itself but instead delegates to a list of view resolvers that you specify through the bean property ViewResolvers.

The ContentNegotiatingViewResolver selects an appropriate View to handle the request by comparing the request media type(s) with the media type (also known as Content-Type) supported by the View associated with each of its ViewResolvers. The first View in the list that has a compatible Content- Type returns the representation to the client. If a compatible view cannot be supplied by the ViewResolver chain, then the list of views specified through the DefaultViews property will be consulted. This latter option is appropriate for singleton Views that can render an appropriate representation of the current resource regardless of the logical view name. The Accept header may include wild cards, for example text/*, in which case a View whose Content-Type was text/xml is a compatible match.

To support custom resolution of a view based on a file extension, use a ContentNegotiationManager: see Section 21.16.6, "Content Negotiation".

Here is an example configuration of a ContentNegotiatingViewResolver:

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <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"/>

 

The InternalResourceViewResolver handles the translation of view names and JSP pages, while the BeanNameViewResolver returns a view based on the name of a bean. (See "[Resolving views with the ViewResolver interface](mvc.html

###mvc-viewresolver-resolver "21.5.1 Resolving views with the ViewResolver interface" )" for more details on how Spring looks up and instantiates a view.) In this example, the content bean is a class that inherits from AbstractAtomFeedView, which returns an Atom RSS feed. For more information on creating an Atom Feed representation, see the section Atom Views.

In the above configuration, if a request is made with an .html extension, the view resolver looks for a view that matches the text/html media type. The InternalResourceViewResolver provides the matching view for text/html. If the request is made with the file extension .atom, the view resolver looks for a view that matches the application/atom+xml media type. This view is provided by the BeanNameViewResolver that maps to the SampleContentAtomView if the view name returned is content. If the request is made with the file extension .json, the MappingJackson2JsonView instance from the DefaultViews list will be selected regardless of the view name. Alternatively, client requests can be made without a file extension but with the Accept header set to the preferred media-type, and the same resolution of request to views would occur.

\[Note\]Note  

If `ContentNegotiatingViewResolver's list of ViewResolvers is not configured explicitly, it automatically uses any ViewResolvers defined in the application context.

The corresponding controller code that returns an Atom RSS feed for a URI of the form <http://localhost/content.atom> or <http://localhost/content> with an Accept header of application/atom+xml is shown below.

@Controller
public class ContentController {

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

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

}

 

21.6 使用閃存屬性FlashAttributes

Flash屬性(flash attributes)提供了一個請求為另一個請求存儲有用屬性的方法。這在重定向的時候最常使用,比如常見的 POST/REDIRECT/GET 模式。Flash屬性會在重定向前被暫時地保存起來(通常是保存在session中),重定向後會重新被下一個請求取用並立即從原保存地移除。

為支持flash屬性,Spring MVC提供了兩個抽象。FlashMap被用來存儲flash屬性,而用FlashMapManager來存儲、取回、管理FlashMap的實例。

對flash屬性的支持默認是啟用的,並不需要顯式聲明,不過沒用到它時它絕不會主動地去創建HTTP會話(session)。對於每個請求,框架都會“傳進”一個FlashMap,裡面存儲了從上個請求(如果有)保存下來的屬性;同時,每個請求也會“輸出”一個FlashMap,裡面保存了要給下個請求使用的屬性。兩個FlashMap實例在Spring MVC應用中的任何地點都可以通過RequestContextUtils工具類的靜態方法取得。

控制器通常不需要直接接觸FlashMap。一般是通過@RequestMapping方法去接受一個RedirectAttributes類型的參數,然後直接地往其中添加flash屬性。通過RedirectAttributes對象添加進去的flash屬性會自動被填充到請求的“輸出”FlashMap對象中去。類似地,重定向後“傳進”的FlashMap屬性也會自動被添加到服務重定向URL的控制器參數Model中去。

匹配請求所使用的flash屬性

flash 屬性的概念在其他許多的Web框架中也存在,並且實踐證明有時可能會導致並發上的問題。這是因為從定義上講,flash屬性保存的時間是到下個請求接收到 之前。問題在於,“下一個”請求不一定剛好就是你要重定向到的那個請求,它有可能是其他的異步請求(比如polling請求或者資源請求等)。這會導致 flash屬性在到達真正的目標請求前就被移除了。

為了減少這個問題發生的可能性,重定向視圖RedirectView會自動為一個FlashMap實例記錄其目標重定向URL的路徑和查詢參數。然後,默認的FlashMapManager會在為請求查找其該“傳進”的FlashMap時,匹配這些信息。

這並不能完全解決重定向的並發問題,但極大程度地減少了這種可能性,因為它可以從重定向URL已有的信息中來做匹配。因此,一般只有在重定向的場景下,我們才推薦使用flash屬性。

21.7 URI構造

在Spring MVC中,使用了UriComponentsBuilderUriComponents兩個類來提供一種構造和加密URI的機制。

比如,你可以通過一個URI模板字符串來填充並加密一個URI:

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 = ...

// 主機名、schema, 端口號、請求路徑和查詢字符串都重用請求裡已有的值
// 替換了其中的"accountId"查詢參數

ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromRequest(request)
        .replaceQueryParam("accountId", "{id}").build()
        .expand("123")
        .encode();

 

或者,你也可以選擇只復用請求中一部分的信息:

  // 重用主機名、端口號和context path
    // 在路徑後添加"/accounts"

    ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromContextPath(request)
            .path("/accounts").build()

 

或者,如果你的DispatcherServlet是通過名字(比如,/main/*)映射請求的,you can also have the literal part of the servlet mapping included:

   // Re-use host, port, context path
    // Append the literal part of the servlet mapping to the path
    // Append "/accounts" to the path

    ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromServletMapping(request)
            .path("/accounts").build()

 

21.7.1 為控制器和方法指定URI

Spring MVC也提供了構造指定控制器方法鏈接的機制。以下面代碼為例子,假設我們有這樣一個控制器:

@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {

    @RequestMapping("/bookings/{booking}")
    public String getBooking(@PathVariable Long booking) {

    // ...

    }
}

 

你可以通過引用方法名字的辦法來准備一個鏈接:

UriComponents uriComponents = MvcUriComponentsBuilder
    .fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);

URI uri = uriComponents.encode().toUri();

 

在上面的例子中,我們為方法參數准備了填充值:一個long型的變量值21,以用於填充路徑變量並插入到URL中。另外,我們還提供了一個值42,以用於填充其他剩余的URI變量,比如從類層級的請求映射中繼承來的hotel變量。如果方法還有更多的參數,你可以為那些不需要參與URL構造的變量賦予null值。一般而言,只有@PathVariable@RequestParam注解的參數才與URL的構造相關。

還有其他使用MvcUriComponentsBuilder的方法。比如,你可以通過類似mock掉測試對象的方法,用代理來避免直接通過名字引用一個控制器方法(以下方法假設MvcUriComponentsBuilder.on方法已經被靜態導入):

UriComponents uriComponents = MvcUriComponentsBuilder
    .fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);

URI uri = uriComponents.encode().toUri();

 

上面的代碼例子中使用了MvcUriComponentsBuilder類的靜態方法。內部實現中,它依賴於ServletUriComponentsBuilder來 從當前請求中抽取schema、主機名、端口號、context路徑和servlet路徑,並准備一個基本URL。大多數情況下它能良好工作,但有時還不 行。比如,在准備鏈接時,你可能在當前請求的上下文(context)之外(比如,執行一個准備鏈接links的批處理),或你可能需要為路徑插入一個前 綴(比如一個地區性前綴,它從請求中被移除,然後又重新被插入到鏈接中去)。

對於上面所提的場景,你可以使用重載過的靜態方法fromXxx,它接收一個UriComponentsBuilder參數,然後從中獲取基本URL以便使用。或你也可以使用一個基本URL創建一個MvcUriComponentsBuilder對象,然後使用實例對象的fromXxx方法。如下面的示例:

UriComponentsBuilder base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en");
MvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(base);
builder.withMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);

URI uri = uriComponents.encode().toUri();

 

在視圖中為控制器和方法指定URI

21.8 地區信息(Locales)

Spring的架構中的很多層面都提供了對國際化的支持,同樣支持Spring MVC框架也能提供。DispatcherServlet為你提供了自動使用用戶的地區信息來解析消息的能力。而這,是通過LocaleResolver對象來完成的。

一個請求進入處理時,DispatcherServlet會查找一個地區解析器。如果找到,就嘗試使用它來設置地區相關的信息。通過調用RequestContext.getLocale()都能取到地區解析器所解析到的地區信息。

此外,如果你需要自動解析地區信息,你可以在處理器映射前加一個攔截器(關於更多處理器映射攔截器的知識,請參見21.4.1 使用HandlerInterceptor攔截請求一小節),並用它來根據條件或環境不同,比如,根據請求中某個參數值,來更改地區信息。

21.8.1 獲取時區信息

除了獲取客戶端的地區信息外,有時他們所在的時區信息也非常有用。LocaleContextResolver接口為LocaleResolver提供了拓展點,允許解析器在LocaleContext中提供更多的信息,這裡面就可以包含時區信息。

如果用戶的時區信息能被解析到,那麼你總可以通過RequestContext.getTimeZone()方法獲得。時區信息會自動被SpringConversionService下注冊的日期/時間轉換器Converter及格式化對象Formatter所使用。

21.8.2 Accept請求頭解析器AcceptHeaderLocaleResolver

AcceptHeaderLocaleResolver解析器會檢查客戶端(比如,浏覽器,等)所發送的請求中是否攜帶accept-language請求頭。通常,該請求頭字段中包含了客戶端操作系統的地區信息。不過請注意,該解析器不支持時區信息的解析。

CookieLocaleResolver解析會檢查客戶端是否有Cookie,裡面可能存放了地區Locale或時區TimeZone信息。如果檢查到相應的值,解析器就使用它們。通過該解析器的屬性,你可以指定cookie的名稱和其最大的存活時間。請見下面的例子,它展示了如何定義一個CookieLocaleResolver

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">

    <property name="cookieName" value="clientlanguage"/>

    <!-- 單位為秒。若設置為-1,則cookie不會被持久化(客戶端關閉浏覽器後即被刪除) -->
    <property name="cookieMaxAge" value="100000">

</bean>

 

表21.4. CookieLocaleResolver支持的屬性

屬性默認值描述 cookieName classname + LOCALE cookie名 cookieMaxAge Integer.MAX_INT cookie被保存在客戶端的最長時間。如果該值為-1,那麼cookie將不會被持久化,在客戶端浏覽器關閉之後就失效了 cookiePath / 限制了cookie僅對站點下的某些特定路徑可見。如果指定了cookiePath,那麼cookie將僅對該路徑及其子路徑下的所有站點可見

21.8.4 Session解析器SessionLocaleResolver

SessionLocaleResolver允許你從session中取得可能與用戶請求相關聯的地區Locale和時區TimeZone信息。與CookieLocaleResolver不同,這種存取策略僅將Servlet容器的HttpSession中相關的地區信息存取到本地。因此,這些設置僅會為該會話(session)臨時保存,session結束後,這些設置就會失效。

不過請注意,該解析器與其他外部session管理機制,比如Spring的Session項目等,並沒有直接聯系。該SessionLocaleResolver僅會簡單地從與當前請求HttpServletRequest相關的HttpSession對象中,取出對應的屬性,並修改其值,僅此而已。

21.8.5 地區更改攔截器LocaleChangeInterceptor

You can enable changing of locales by adding the LocaleChangeInterceptor to one of the handler mappings (see [Section 21.4, "Handler mappings"](mvc.html

###mvc-handlermapping "21.4 Handler mappings" )). It will detect a parameter in the request and change the locale. It calls setLocale() on the LocaleResolver that also exists in the context. The following example shows that calls to all *.view resources containing a parameter named siteLanguage will now change the locale. So, for example, a request for the following URL, <http://www.sf.net/home.view?siteLanguage=nl> will change the site language to Dutch.

你可以在處理器映射(詳見21.4 處理器映射(Handler mappings)小節)前添加一個LocaleChangeInterceptor攔截器來更改地區信息。它能檢測請求中的參數,並根據其值相應地更新地區信息。它通過調用LocaleResolversetLocale()方法來更改地區。下面的代碼配置展示了如何為所有請求*.view路徑並且攜帶了siteLanguage參數的資源請求更改地區。舉個例子,一個URL為<http://www.sf.net/home.view?siteLanguage=nl>的請求將會將站點語言更改為荷蘭語。

<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
    <property name="paramName" value="siteLanguage"/>
</bean>

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>

<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="interceptors">
        <list>
            <ref bean="localeChangeInterceptor"/>
        </list>
    </property>
    <property name="mappings">
        <value>/**/*.view=someController</value>
    </property>
</bean>

 

21.9 主題 themes

  • 21.9.1 關於主題:概覽
  • 21.9.2 定義主題
  • 21.9.3 主題解析器

21.9.1 關於主題:概覽

You can apply Spring Web MVC framework themes to set the overall look-and-feel of your application, thereby enhancing user experience. A theme is a collection of static resources, typically style sheets and images, that affect the visual style of the application.

你可以使用Spring Web MVC框架提供的主題來為整站的應用設置皮膚/主題(look-and-feel),這可以提高用戶體驗。主題 是指一系列靜態資源的集合,並且主要是樣式表和圖片,它們決定了你的應用的視覺風格。

21.9.2 定義主題

要在你的應用中使用主題,你必須實現一個 org.springframework.ui.context.ThemeSource 接口。WebApplicationContext接口繼承了ThemeSource接口,但主要的工作它還是委托給接口具體的實現來完成。默認的實現是org.springframework.ui.context.support.ResourceBundleThemeSource,它會從classpath的根路徑下去加載配置文件。如果需要定制ThemeSource的實現,或要配置ResourceBundleThemeSource的基本前綴名(base name prefix),你可以在應用上下文(application context)下注冊一個名字為保留名themeSource的bean,web應用的上下文會自動檢測名字為themeSource的bean並使用它。

使用的是ResourceBundleThemeSource時,一個主題可以定義在一個簡單的配置文件中。該配置文件會列出所有組成了該主題的資源。下面是個例子:

 styleSheet=/themes/cool/style.css
    background=/themes/cool/img/coolBg.jpg

 

屬性的鍵(key)是主題元素在視圖代碼中被引用的名字。對於JSP視圖來說,一般通過spring:theme這個定制化的標簽(tag)來做,它與 spring:message 標簽很相似。以下的JSP代碼即使用了上段代碼片段中定義的主題,用以定制整體的皮膚:

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
    <head>
        <link rel="stylesheet" href="<spring:theme code=''styleSheet''/>" type="text/css"/>
    </head>
    <body >
        ...
    </body>
</html>

 

默認情況下ResourceBundleThemeSource使用的基本名前綴(base name prefix)是空值。也即是說,配置文件是從根classpath路徑下加載的。因此,你需要把主題的定義文件cool.properties放在classpath的根路徑目錄下,比如,/WEB-INF/classesResourceBundleThemeSource采用了Java的標准資源bundle加載機制,完全支持國際化主題。比如,你可以創建一個/WEB-INF/classes/cool_nl.properties配置文件,並在其中引用一副有荷蘭文的背景圖片。

21.9.3 主題解析器

上一小節,我們講了如何定義主題,定義之後,你要決定使用哪個主題。DispatcherServlet會查找一個名稱為themeResolver的bean以確定使用哪個ThemeResolver的實現。主題解析器的工作原理與地區解析器LocaleResolver的工作原理大同小異。它會檢測,對於一個請求來說,應該使用哪個主題,同時它也可以修改一個請求所應應用的主題。Spring提供了下列的這些主題解析器:

表21.5. ThemeResolver接口的實現

類名描述 FixedThemeResolver 選擇一個固定的主題,這是通過設置defaultThemeName這個屬性值實現的 SessionThemeResolver 請求相關的主題保存在用戶的HTTP會話(session)中。對於每個會話來說,它只需要被設置一次,但它不能在會話之間保存 CookieThemeResolver 選中的主題被保存在客戶端的cookie中

Spring也提供了一個主題更改攔截器ThemeChangeInterceptor,以支持主題的更換。這很容易做到,只需要在請求中攜帶一個簡單的請求參數即可。

21.10 Spring的multipart(文件上傳)支持

  • 概述
  • 使用MultipartResolver與Commons FileUpload傳輸文件
  • Servlet 3.0下的MultipartResolver
  • 處理表單中的文件上傳
  • 處理客戶端發起的文件上傳請求

21.10.1 概述

Spring內置對多路上傳的支持,專門用於處理web應用中的文件上傳。你可以通過注冊一個可插拔的MultipartResolver對象來啟用對文件多路上傳的支持。該接口在定義於 org.springframework.web.multipart 包下。Spring為一般的文件上傳提供了MultipartResolver接口的一個實現,為Servlet 3.0多路請求的轉換提供了另一個實現。

默 認情況下,Spring的多路上傳支持是不開啟的,因為有些開發者希望由自己來處理多路請求。如果想啟用Spring的多路上傳支持,你需要在web應用 的上下文中添加一個多路傳輸解析器。每個進來的請求,解析器都會檢查是不是一個多部分請求。若發現請求是完整的,則請求按正常流程被處理;如果發現請求是 一個多路請求,則你在上下文中注冊的MultipartResolver解析器會被用來處理該請求。之後,請求中的多路上傳屬性就與其他屬性一樣被正常對待了。【最後一句翻的不好,multipart翻譯成多路還是多部分還在斟酌中。望閱讀者注意此處。】

21.10.2 使用MultipartResolver與Commons FileUpload傳輸文件

下面的代碼展示了如何使用一個通用的多路上傳解析器 CommonsMultipartResolver :

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">

    <!-- 支持的其中一個屬性,支持的最大文件大小,以字節為單位 -->
    <property name="maxUploadSize" value="100000"/>

</bean>

 

當然,要讓多路解析器正常工作,你需要在classpath路徑下准備必須的jar包。如果使用的是通用的多路上傳解析器CommonsMultipartResolver,你所需要的jar包是commons-fileupload.jar

當Spring的DispatcherServlet檢測到一個多部分請求時,它會激活你在上下文中聲明的多路解析器並把請求交給它。解析器會把當前的HttpServletRequest請求對象包裝成一個支持多路文件上傳的請求對象MultipartHttpServletRequest。有了MultipartHttpServletRequest對象,你不僅可以獲取該多路請求中的信息,還可以在你的控制器中獲得該多路請求的內容本身。

21.10.3 Servlet 3.0下的MultipartResolver

要使用基於Servlet 3.0的多路傳輸轉換功能,你必須在web.xml中為DispatcherServlet添加一個multipart-config元素,或者通過Servlet編程的方法使用javax.servlet.MultipartConfigElement進行注冊,或你自己定制了自己的Servlet類,那你必須使用javax.servlet.annotation.MultipartConfig對其進行注解。其他諸如最大文件大小或存儲位置等配置選項都必須在這個Servlet級別進行注冊,因為Servlet 3.0不允許在解析器MultipartResolver的層級配置這些信息。

當你通過以上任一種方式啟用了Servlet 3.0多路傳輸轉換功能,你就可以把一個 StandardServletMultipartResolver 解析器添加到你的Spring配置中去了:

<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
</bean>

 

21.10.4 處理表單中的文件上傳

當解析器MultipartResolver完成處理時,請求便會像其他請求一樣被正常流程處理。首先,創建一個接受文件上傳的表單將允許用於直接上傳整個表單。編碼屬性( enctype="multipart/form-data" )能讓浏覽器知道如何對多路上傳請求的表單進行編碼(encode)。

<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
public class FileUploadController {

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

        if (!file.isEmpty()) {
            byte[] bytes = file.getBytes();
            // store the bytes somewhere
            return "redirect:uploadSuccess";
        }

        return "redirect:uploadFailure";
    }

}

 

請留意@RequestParam注解是如何將方法參數對應到表單中的定義的輸入字段的。在上面的例子中,我們拿到了byte[]文件數據,只是沒對它做任何事。在實際應用中,你可能會將它保存到數據庫、存儲在文件系統上,或做其他的處理。

當使用Servlet 3.0的多路傳輸轉換時,你也可以使用javax.servlet.http.Part作為方法參數:

@Controller
public class FileUploadController {

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

        InputStream inputStream = file.getInputStream();
        // store bytes from uploaded file somewhere

        return "redirect:uploadSuccess";
    }

}

 

21.10.5 處理客戶端發起的文件上傳請求

在 使用了RESTful服務的場景下,非浏覽器的客戶端也可以直接提交多路文件請求。上一節講述的所有例子與配置在這裡也都同樣適用。但與浏覽器不同的是, 提交的文件和簡單的表單字段,客戶端發送的數據可以更加復雜,數據可以指定為某種特定的內容類型(content type)——比如,一個多路上傳請求可能第一部分是個文件,而第二部分是個JSON格式的數據:

 POST /someUrl
    Content-Type: multipart/mixed

    --edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
    Content-Disposition: form-data; name="meta-data"
    Content-Type: application/json; charset=UTF-8
    Content-Transfer-Encoding: 8bit

    {
        "name": "value"
    }
    --edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
    Content-Disposition: form-data; name="file-data"; filename="file.properties"
    Content-Type: text/xml
    Content-Transfer-Encoding: 8bit
    ... File Data ...

 

對於名稱為meta-data的部分,你可以通過控制器方法上的@RequestParam("meta-data") String metadata參數來獲得。但對於那部分請求體中為JSON格式數據的請求,你可能更想通過接受一個對應的強類型對象,就像@RequestBody通過HttpMessageConverter將一般請求的請求體轉換成一個對象一樣。

這是可能的,你可以使用@RequestPart注解來實現,而非@RequestParam。該注解將使得特定多路請求的請求體被傳給HttpMessageConverter,並且在轉換時考慮多路請求中不同的內容類型參數'Content-Type'

@RequestMapping(path = "/someUrl", method = RequestMethod.POST)
public String onSubmit(@RequestPart("meta-data") MetaData metadata, @RequestPart("file-data") MultipartFile file) {

    // ...

}

 

請注意MultipartFile方法參數是如何能夠在@RequestParam@RequestPart注解下互用的,兩種方法都能拿到數據。但,這裡的方法參數@RequestPart("meta-data") MetaData則會因為請求中的內容類型請求頭'Content-Type'被讀入成為JSON數據,然後再通過MappingJackson2HttpMessageConverter被轉換成特定的對象。

21.11 異常處理

  • 處理器異常解析器HandlerExceptionHandler
  • @ExceptionHandler注解
  • 處理一般的Spring MVC異常
  • 使用@ResponseStatus注解業務異常
  • Servlet默認容器錯誤頁面的定制化

21.11.1 處理器異常解析器HandlerExceptionHandler

Spring的處理器異常解析器 HandlerExceptionResolver 接口的實現負責處理各類控制器執行過程中出現的異常。某種程度上講,HandlerExceptionResolver與你在web應用描述符web.xml文 件中能定義的異常映射(exception mapping)很相像,不過它比後者提供了更靈活的方式。比如它能提供異常被拋出時正在執行的是哪個處理器這樣的信息。並且,一個更靈活 (programmatic)的異常處理方式可以為你提供更多選擇,使你在請求被直接轉向到另一個URL之前(與你使用Servlet規范的異常映射是一 樣的)有更多的方式來處理異常。

實現HandlerExceptionResolver接口並非實現異常處理的唯一方式,它只是提供了resolveException(Exception, Hanlder)方法的一個實現而已,方法會返回一個ModelAndView。除此之外,你還可以框架提供的SimpleMappingExceptionResolver或在異常處理方法上注解@ExceptionHandlerSimpleMappingExceptionResolver允許你獲取可能拋出的異常類的名字,並把它映射到一個視圖名上去。這與Servlet API提供的異常映射特性是功能等價的,但你也可以基於此實現粒度更精細的異常映射。而@ExceptionHandler注解的方法則會在異常拋出時被調用以處理該異常。這樣的方法可以定義在@Controller注解的控制器類裡,也可以定義在@ControllerAdvice類中,後者可以使該異常處理方法被應用到更多的@Controller控制器中。下一小節將提供更為詳細的信息。

21.11.2 @ExceptionHandler注解

HandlerExceptionResolver接口以及SimpleMappingExceptionResolver解析器類的實現使得你能聲明式地將異常映射到特定的視圖上,還可以在異常被轉發(forward)到對應的視圖前使用Java代碼做些判斷和邏輯。不過在一些場景,特別是依靠@ResponseBody返回響應而非依賴視圖解析機制的場景下,直接設置響應的狀態碼並將客戶端需要的錯誤信息直接寫回響應體中,可能是更方便的方法。

你也可以使用@ExceptionHandler方法來做到這點。如果@ExceptionHandler方法是在控制器內部定義的,那麼它會接收並處理由控制器(或其任何子類)中的@RequestMapping方法拋出的異常。如果你將@ExceptionHandler方法定義在@ControllerAdvice類中,那麼它會處理相關控制器中拋出的異常。下面的代碼就展示了一個定義在控制器內部的@ExceptionHandler方法:

@Controller
public class SimpleController {

    // @RequestMapping methods omitted ...

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

}

 

此外,@ExceptionHandler注解還可以接受一個異常類型的數組作為參數值。若拋出了已在列表中聲明的異常,那麼相應的@ExceptionHandler方法將會被調用。如果沒有給注解任何參數值,那麼默認處理的異常類型將是方法參數所聲明的那些異常。

與標准的控制器@RequestMapping注解處理方法一樣,@ExceptionHandler方法的方法參數和返回值也可以很靈活。比如,在Servlet環境下方法可以接收HttpServletRequest參數,而Portlet環境下方法可以接收PortletRequest參數。返回值可以是String類型——這種情況下會被解析為視圖名——可以是ModelAndView類型的對象,也可以是ResponseEntity。或者你還可以在方法上添加@ResponseBody注解以使用消息轉換器會轉換信息為特定類型的數據,然後把它們寫回到響應流中。

21.11.3 處理一般的Spring MVC異常

處理請求的過程中,Spring MVC可能會拋出一些的異常。SimpleMappingExceptionResolver可以根據需要很方便地將任何異常映射到一個默認的錯誤視圖。但,如果客戶端是通過自動檢測響應的方式來分發處理異常的,那麼後端就需要為響應設置對應的狀態碼。根據拋出異常的類型不同,可能需要設置不同的狀態碼來標識是客戶端錯誤(4xx)還是服務器端錯誤(5xx)。

默認處理器異常解析器DefaultHandlerExceptionResolver會將Spring MVC拋出的異常轉換成對應的錯誤狀態碼。該解析器在MVC命名空間配置或MVC Java配置的方式下默認已經被注冊了,另外,通過DispatcherServlet注冊也是可行的(即不使用MVC命名空間或Java編程方式進行配置的時候)。下表列出了該解析器能處理的一些異常,及他們對應的狀態碼。

異常HTTP狀態碼 BindException 400 (無效請求) ConversionNotSupportedException 500 (服務器內部錯誤) HttpMediaTypeNotAcceptableException 406 (不接受) HttpMediaTypeNotSupportedException 415 (不支持的媒體類型) HttpMessageNotReadableException 400 (無效請求) HttpMessageNotWritableException 500 (服務器內部錯誤) HttpRequestMethodNotSupportedException 405 (不支持的方法) MethodArgumentNotValidException 400 (無效請求) MissingServletRequestParameterException 400 (無效請求) MissingServletRequestPartException 400 (無效請求) NoHandlerFoundException 404 (請求未找到) NoSuchRequestHandlingMethodException 404 (請求未找到) TypeMismatchException 400 (無效請求) MissingPathVariableException 500 (服務器內部錯誤) NoHandlerFoundException 404 (請求未找到)

以下待翻譯。

The DefaultHandlerExceptionResolver works transparently by setting the status of the response. However, it stops short of writing any error content to the body of the response while your application may need to add developer- friendly content to every error response for example when providing a REST API. You can prepare a ModelAndView and render error content through view resolution -- i.e. by configuring a ContentNegotiatingViewResolver, MappingJackson2JsonView, and so on. However, you may prefer to use @ExceptionHandler methods instead.

If you prefer to write error content via @ExceptionHandler methods you can extend ResponseEntityExceptionHandler instead. This is a convenient base for @ControllerAdvice classes providing an @ExceptionHandler method to handle standard Spring MVC exceptions and return ResponseEntity. That allows you to customize the response and write error content with message converters. See the ResponseEntityExceptionHandler javadocs for more details.

21.11.4 使用@ResponseStatus注解業務異常

業務異常可以使用@ResponseStatus來注解。當異常被拋出時,ResponseStatusExceptionResolver會設置相應的響應狀態碼。DispatcherServlet會默認注冊一個ResponseStatusExceptionResolver以供使用。

21.11.5 Servlet默認容器錯誤頁面的定制化

當響應的狀態碼被設置為錯誤狀態碼,並且響應體中沒有內容時,Servlet容器通常會渲染一個HTML錯誤頁。若需要定制容器默認提供的錯誤頁,你可以在web.xml中定義一個錯誤頁面<error-page>元素。在Servlet 3規范出來之前,該錯誤頁元素必須被顯式指定映射到一個具體的錯誤碼或一個異常類型。從Servlet 3開始,錯誤頁不再需要映射到其他信息了,這意味著,你指定的位置就是對Servlet容器默認錯誤頁的自定制了。

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

 

這裡錯誤頁的位置所在可以是一個JSP頁面,或者其他的一些URL,只要它指定容器裡任意一個@Controller控制器下的處理器方法:

寫回HttpServletResponse的錯誤信息和錯誤狀態碼可以在控制器中通過請求屬性來獲取:

@Controller
public class ErrorController {

    @RequestMapping(path = "/error", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @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;
    }

}

 

或者在JSP中這麼使用:

<%@ page contentType="application/json" pageEncoding="UTF-8"%>
{
    status:<%=request.getAttribute("javax.servlet.error.status_code") %>,
    reason:<%=request.getAttribute("javax.servlet.error.message") %>
}

 

21.12 Web安全

Spring Security項目為保護web應用免受惡意攻擊提供了一些特性。你可以去看看參考文檔的這幾小節:"CSRF保護"、"安全響應頭"以及"Spring MVC集成"。不過並非應用的所有特性都需要引入Spring Security。比如,需要CSRF保護的話,你僅需要簡單地在配置中添加一個過濾器CsrfFilter和處理器CsrfRequestDataValueProcessor。你可以參考Spring MVC Showcase一節,觀看一個完整的展示。

另一個選擇是使用其他專注於Web安全的框架。HDIV就是這樣的一個框架,並且它能與Spring MVC良好集成。

21.13 "約定優於配置"的支持

對於許多項目來說,不打破已有的約定,對於配置等有可預測的默認值是非常適合的。現在,Spring MVC對 約定優於配置 這個實踐已經有了顯式的支持。這意味著什麼呢?意味著如果你為項目選擇了一組命名上的約定/規范,你將能減少 大量 的配置項,比如一些必要的處理器映射、視圖解析器、ModelAndView實例的配置等。這能幫你快速地建立起項目原型,此外在某種程度上(通常是好的方面)維護了整個代碼庫的一致性should you choose to move forward with it into production.

具體來說,支持“約定優於配置”涉及到MVC的三個核心方面:模型、視圖,和控制器。

21.13.1 控制器類名-處理器映射ControllerClassNameHandlerMapping

ControllerClassNameHandlerMapping類是HandlerMapping接口的一個實現,它是通過一個約定來解析請求URL及處理該請求的@Controller控制器實例之間的映射關系。

請看下面一個簡單的控制器實現。請注意留意該類的 名稱

 

public class **ViewShoppingCartController** implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
        // 這個例子中方法的具體實現並不重要,故忽略。
    }

}

對應的Spring Web MVC配置文件如下所示:

<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>

<bean id="**viewShoppingCart**" class="x.y.z.ViewShoppingCartController">
    <!-- 注入需要的依賴 -->
</bean>

 

 

 

ControllerClassNameHandlerMapping會查找當前應用上下文中注冊的所有處理器(也即控制器)bean,並去除類名的Controller後綴作為決定處理器映射的依據。因此,類名ViewShoppingCartController會被映射到匹配/viewshoppingcart*的請求URL上。

讓我們多看幾個例子,這樣你對於核心的思想會馬上熟悉起來(注意URL中路徑是全小寫,而Controller控制器類名符合駝峰命名法):

  • WelcomeController將映射到/welcome*請求URL
  • HomeController 將映射到/home*請求URL
  • IndexController 將映射到/index*請求URL
  • RegisterController 將映射到/register*請求URL

對於MultiActionController處理器類,映射規則要稍微復雜一些。請看下面的代碼,假設這裡的控制器都是MultiActionController的實現:

  • AdminController將映射到/admin/*請求URL
  • CatalogController將映射到/catalog/*請求URL

只要所有控制器Controller實現都遵循xxxController這樣的命名規范,那麼ControllerClassNameHandlerMapping能把你從定義維護一個 長長長 SimpleUrlHandlerMapping映射表的重復工作中拯救出來。

ControllerClassNameHandlerMapping類繼承自 AbstractHandlerMapping基類。因此,你可以視它與其他HandlerMapping實現一樣,定義你所需要的攔截器HandlerInterceptor實例及其他所有東西。

21.13.2 模型ModelMap(ModelAndView)

ModelMap類其實就是一個豪華版的 Map,它使得你為視圖展示需要所添加的對象都遵循一個顯而易見的約定被命名。請看下面這個 Controller實現,並請注意,添加到ModelAndView中去的對象都沒有顯式地指定鍵名。

public class DisplayShoppingCartController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {

        List cartItems = // 拿到一個CartItem對象的列表
        User user = // 拿到當前購物的用戶User

        ModelAndView mav = new ModelAndView("displayShoppingCart"); <-- 邏輯視圖名

        mav.addObject(cartItems); <-- 啊哈,直接添加的對象,沒有指定名稱
        mav.addObject(user); <-- 啊哈再來一次

        return mav;
    }
}

 

ModelAndView內部使用了一個ModelMap類,它是Map的一個實現,會自動為添加進來的對象生成一個鍵名。為添加對象生成名稱的策略是,若添加對象是一個純Java bean(a scalar object),比如User,那麼使用對象類的短類名(short class name)來作為該對象的名稱。下面是一些例子,展示了為添加到ModelMap實例中的純Java對象所生成的名稱:

  • 添加一個x.y.User實例,為其生成的名稱為user
  • 添加一個x.y.Registration實例,為其生成的名稱為registration
  • 添加一個x.y.Foo實例,為其生成的名稱為foo
  • 添加一個java.util.HashMap實例,為其生成的名稱為hashMap。這種情況下,顯式地聲明一個鍵名可能更好,因為hashMap的約定並非那麼符合直覺
  • 添加一個null值將導致程序拋出一個IllegalArgumentException參數非法異常。若你所添加的(多個)對象有可能為null值,那你也需要顯式地指定它(們)的名字

啥?鍵名不能自動變復數形式麼?

Spring Web MVC的約定優於配置支持尚不能支持自動復數轉換。這意思是,你不能期望往ModelAndView中添加一個Person對象的List列表時,框架會自動為其生成一個名稱people

這個決定是經過許多爭論後的結果,最終“最小驚喜原則”勝出並為大家所接受。

為集合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則根本不會被添加

21.13.3 視圖-請求與視圖名的映射

RequestToViewNameTranslator接口可以在邏輯視圖名未被顯式提供的情況下,決定一個可用的邏輯視圖View名。

DefaultRequestToViewNameTranslator能夠將請求URL映射到邏輯視圖名上去,如下面代碼例子所示:



public class RegistrationController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
        // 處理請求……
        ModelAndView mav = new ModelAndView();
        // 向Model中添加需要的數據
        return mav;
        // 請注意這裡,沒有設置任何View對象或邏輯視圖名
    }

}

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    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">

    <!-- 這個眾人皆知的bean將為我們自動生成視圖名 -->
    <bean id="viewNameTranslator" class="org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator"/>

    <bean class="x.y.RegistrationController">
        <!-- 如果需要,注入依賴 -->
    </bean>

    <!-- 請請求URL映射到控制器名 -->
    <bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>

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

</beans>

 

 

請注意在handleRequest(...)方法實現中,返回的ModelAndView對象上自始至終未設置任何View對象或邏輯視圖名。這是由DefaultRequestToViewNameTranslator完成的,它的任務就是從請求的URL中生成一個邏輯視圖名。在上面的例子中,RegistrationController與配置的ControllerClassNameHandlerMapping一起使用的結果是,一個URL為<http://localhost/registration.html>的請求,會經由DefaultRequestToViewNameTranslator生成並對應到一個邏輯視圖名registration上。該邏輯視圖名又會由InternalResourceViewResolverbean解析到/WEB-INF/jsp/registration.jsp視圖上。

你無需顯式地定義一個DefaultRequestToViewNameTranslatorbean。如果默認的DefaultRequestToViewNameTranslator配置已能滿足你的需求,那麼你無需配置,Spring Web MVC的DispatcherServlet會為你實例化這樣一個默認的對象。

當然,如果你需要更改默認的設置,那你就需要手動地配置自己的DefaultRequestToViewNameTranslatorbean。關於可配置屬性的一些詳細信息,你可以去咨詢DefaultRequestToViewNameTranslator類詳細的java文檔。

21.14 HTTP緩存支持

一個好的HTTP緩存策略可以極大地提高一個web應用的性能及客戶端的體驗。談到HTTP緩存,它主要是與HTTP的響應頭'Cache-Control'相關,其次另外的一些響應頭比如'Last-Modified''ETag'等也會起一定的作用。

HTTP的響應頭'Cache-Control'主要幫助私有緩存(比如浏覽器端緩存)和公共緩存(比如代理端緩存)了解它們應該如果緩存HTTP響應,以便後用。

ETag(實體標簽)是一個HTTP響應頭,可由支持HTTP/1.1的web應用服務器設置返回,主要用於標識給定的URL下的內容有無變化。可以認為它是Last-Modified頭的一個更精細的後續版本。當服務器端返回了一個ETag頭的資源表示時,客戶端就可以在後續的GET請求中使用這個表示,一般是將它放在If-None-Match請求頭中。此時若內容沒有變化,服務器端會直接返回304: 內容未更改

這一節將講解其他一些在Spring Web MVC應用中配置HTTP緩存的方法。

21.14.1 HTTP請求頭Cache-Control

Spring MVC提供了許多方式來配置"Cache-Control"請求頭,支持在許多場景下使用它。關於該請求頭完整詳盡的所有用法,你可以參考RFC 7234的第5.2.2小節,這裡我們只講解最常用的幾種用法。

Spring MVC的許多API中都使用了這樣的慣例配置:setCachePeriod(int seconds),若返回值為:

  • -1,則框架不會生成一個'Cache-Control'緩存控制指令響應頭
  • 0,則指示禁止使用緩存,服務器端返回緩存控制指令'Cache-Control: no-store'
  • 任何n > 0的值,則響應會被緩存n秒,並返回緩存控制指令'Cache-Control: max-age=n'

CacheControl構造器類被簡單的用來描述"Cache-Control"緩存控制指令,使你能更容易地創建自己的HTTP緩存策略。創建完了以後,CacheControl類的實例就可以在Spring MVC的許多API中被傳入為方法參數了。

// 緩存一小時 - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);

// 禁止緩存 - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();

// 緩存十天,對所有公共緩存和私有緩存生效
// 響應不能被公共緩存改變
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS)
                                    .noTransform().cachePublic();

 

21.14.2 對靜態資源的HTTP緩存支持

為優化站點性能,靜態資源應該帶有恰當的'Cache-Control'值與其他必要的頭。配置一個ResourceHttpRequestHandler處理器服務靜態資源請求不僅會讀取文件的元數據並填充'Last-Modified'頭的值,正確配置時'Cache-Control'頭也會被填充。【這段翻得還不是很清晰】

你可以設置ResourceHttpRequestHandler上的cachePeriod屬性值,或使用一個CacheControl實例來支持更細致的指令:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public-resources/")
                .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic());
    }

}

 

XML中寫法則如下:

<mvc:resources mapping="/resources/**" location="/public-resources/">
    <mvc:cache-control max-age="3600" cache-public="true"/>
</mvc:resources>

 

21.14.3 在控制器中設置Cache-Control、ETag和Last-Modified響應頭

控制器能處理帶有'Cache-Control''ETag'及/或'If-Modified-Since'頭的請求,如果服務端在響應中設置了'Cache-Control'響應頭,那麼我們推薦在控制器內對這些請求頭進行處理。這涉及一些工作:計算最後更改時間long和/或請求的ETag值、與請求頭的'If-Modified-Since'值做比較,並且在資源未更改的情況下在響應中返回一個304(資源未更改)狀態碼。

正如在"使用HttpEntity"一節中所講,控制器可以通過HttpEntity類與請求/響應交互。返回ResponseEntity的控制器可以在響應中包含HTTP緩存的信息,如下代碼所示:

@RequestMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {

    Book book = findBook(id);
    String version = book.getVersion();

    return ResponseEntity
                .ok()
                .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
                .eTag(version) // 這裡也能操作最後修改時間lastModified,只不過沒有一一展示
                .body(book);
}

 

這樣做不僅會在響應頭中設置'ETag''Cache-Control'相關的信息,同時也會 嘗試將響應狀態碼設置為HTTP 304 Not Modified(資源未修改)及將響應體置空——如果客戶端攜帶的請求頭信息與控制器設置的緩存信息能夠匹配的話。

如果希望在@RequestMapping方法上也能完成同樣的事,那麼你可以這樣做:

@RequestMapping
public String myHandleMethod(WebRequest webRequest, Model model) {

    long lastModified = // 1. 應用相關的方式計算得到(application-specific calculation)

    if (request.checkNotModified(lastModified)) {
        // 2. 快速退出 — 不需要更多處理了
        return null;
    }

    // 3. 若資源更改了,那麼再進行請求處理階段,一般而言是准備響應內容
    model.addAttribute(...);
    return "myViewName";
}

 

這裡最重要的兩個地方是:調用request.checkNotModified(lastModified)方法,以及返回null。前者(方法調用)在返回true之前會將響應狀態碼設為304;而後者,在檢查是否更改的方法調用返回true的基礎上直接將方法返回,這會通知Spring MVC不再對請求做任何處理。

另外要注意的是,檢查資源是否發生了更改有3種方式:

  • request.checkNotModified(lastModified)方法會將傳入的參數值(最後修改時間)與請求頭'If-Modified-Since'的值進行比較
  • request.checkNotModified(eTag)方法會將傳入的參數值與請求頭'ETag'的值進行比較
  • request.checkNotModified(eTag, lastModified)方法會同時進行以上兩種比較。也即是說,只有在兩個比較都被判定為未修改時,服務器才會返回一個304響應狀態碼HTTP 304 Not Modified(資源未修改)

21.14.4 弱ETag(Shallow ETag)

對ETag的支持是由Servlet的過濾器ShallowEtagHeaderFilter提供的。它是純Servlet技術實現的過濾器,因此,它可以與任何web框架無縫集成。ShallowEtagHeaderFilter過 濾器會創建一個我們稱為弱ETag(與強ETag相對,後面會詳述)的對象。過濾器會將渲染的JSP頁面的內容(包括其他類型的內容)緩存起來,然後以此 生成一個MD5哈希值,並把這個值作為ETag頭的值寫回響應中。下一次客戶端再次請求這個同樣的資源時,它會將這個ETag的值寫到If-None-Match頭中。過濾器會檢測到這個請求頭,然後再次把視圖渲染出來並比較兩個哈希值。如果比較的結果是相同的,那麼服務器會返回一個304。正如你所見,視圖仍然會被渲染,因此本質上這個過濾器並非節省任何計算資源。唯一節省的東西,是帶寬,因為被渲染的響應不會被整個發送回客戶端。

請注意,這個策略節省的是網絡帶寬,而非CPU。因為對於每個請求,完整的響應仍然需要被整個計算出來。而其他在控制器層級實現的策略(上幾節所述的)可以同時節省網絡帶寬及避免多余計算。

你可以在web.xml中配置ShallowEtagHeaderFilter

<filter>
    <filter-name>etagFilter</filter-name>
    <filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>etagFilter</filter-name>
    <servlet-name>petclinic</servlet-name>
</filter-mapping>

 

如果是在Servlet 3.0以上的環境下,可以這麼做:

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    // ...

    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] { new ShallowEtagHeaderFilter() };
    }

}

 

更多配置細節,請參考第21.15 基於代碼的Servlet容器初始化一小節。

21.15 基於代碼的Servlet容器初始化

在Servlet 3.0以上的環境下,你可以通過編程的方式來配置Servlet容器了。你可以完全放棄web.xml,也可以兩種配置方式同時使用。以下是一個注冊DispatcherServlet的例子:

import org.springframework.web.WebApplicationInitializer;

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {
        XmlWebApplicationContext appContext = new XmlWebApplicationContext();
        appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");

        ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
        registration.setLoadOnStartup(1);
        registration.addMapping("/");
    }

}

 

Spring MVC提供了一個WebApplicationInitializer接口,實現這個接口能保證你的配置能自動被檢測到並應用於Servlet 3容器的初始化中。WebApplicationInitializer有一個實現,是一個抽象的基類,名字叫AbstractDispatcherServletInitializer。有了它,要配置DispatcherServlet將變得更簡單,你只需要覆寫相應的方法,在其中提供servlet映射、DispatcherServlet所需配置的位置即可:

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { MyWebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

}

 

以上的例子適用於使用基於Java配置的Spring應用。如果你使用的是基於XML的Spring配置方式,那麼請直接繼承AbstractDispatcherServletInitializer這個類:

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }

    @Override
    protected WebApplicationContext createServletApplicationContext() {
        XmlWebApplicationContext cxt = new XmlWebApplicationContext();
        cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
        return cxt;
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

}

 

AbstractDispatcherServletInitializer同樣也提供了便捷的方式來添加過濾器Filter實例並使他們自動被映射到DispatcherServlet下:

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    // ...

    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] { new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
    }

}

 

每個過濾器被添加時,默認的名稱都基於其類類型決定,並且它們會被自動地映射到DispatcherServlet下。

關於異步支持,AbstractDispatcherServletInitializer的保護方法isAsyncSupported提供了一個集中的地方來開關DispatcherServlet上的這個配置,它會對所有映射到這個分發器上的過濾器生效。默認情況下,這個標志被設為true

最後,如果你需要對DispatcherServlet做進一步的定制,你可以覆寫createDispatcherServlet這個方法。

21.16 配置Spring MVC

21.2.1 WebApplicationContext中特殊的bean類型小節和21.2.2 默認的DispatcherServlet配置小節解釋了何謂Spring MVC的特殊bean,以及DispatcherServlet所使用的默認實現。在這小節中,你將了解配置Spring MVC的其他兩種方式:MVC Java編程配置,以及MVC XML命名空間。

MVC Java編程配置和MVC命名空間都提供了相似的默認配置,以覆寫DispatcherServlet的默認值。目標在於為大多數應用軟件免去創建相同配置的麻煩,同時也想為配置Spring MVC提供一個更易理解的指南、一個簡單的開始點,它只需要很少或不需任何關於底層配置的知識。

你 可以選用MVC Java編程配置或MVC命名空間的方式,這完全取決於你的喜好。若你能讀完後面的小節,你會發現使用MVC Java編程配置的方式能更容易看到底層具體的配置項,並且能對創建的Spring MVC bean有更細粒度的定制空間。不過,我們還是從頭來看起吧。

21.16.1 啟用MVC Java編程配置或MVC命名空間

要啟用MVC Java編程配置,你需要在其中一個注解了@Configuration的類上添加@EnableWebMvc注解:

@Configuration
@EnableWebMvc
public class WebConfig {

}

 

要啟用XML命名空間,請在你的DispatcherServlet上下文中(如果沒有定義任何DispatcherServlet上下文,那麼就在根上下文中)添加一個mvc:annotation-driven元素:

<?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/>

</beans>

 

上面的簡單的聲明代碼,就已經默認注冊了一個RequestMappingHandlerMapping、一個RequestMappingHandlerAdapter,以及一個ExceptionHandlerExceptionResolver,以支持對使用了@RequestMapping@ExceptionHandler及其他注解的控制器方法的請求處理。

同時,上面的代碼還啟用了以下的特性:

下面給出了一份由mvc:annotation-driven注冊可用的HTTP消息轉換器的完整列表:

你可以參考21.16.12 消息轉換器一小節,了解如何進一步定制這些默認的轉換器。

Jackson JSON和XML轉換器是通過Jackson2ObjectMapperBuilder創建的ObjectMapper實例創建的,目的在於提供更好的默認配置

該builder會使用以下的默認屬性對Jackson進行配置:

同時,如果檢測到在classpath路徑下存在這些模塊,該builder也會自動地注冊它們:

21.16.2 默認配置的定制化

在MVC Java編程配置方式下,如果你想對默認配置進行定制,你可以自己實現WebMvcConfigurer接口,要麼繼承WebMvcConfigurerAdapter類並覆寫你需要定制的方法:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    // Override configuration methods...

}

 

在MVC XML命名空間下,如果你想對默認配置進行定制,請查看<mvc:annotation-driven/>元素支持的屬性和子元素。你可以查看Spring MVC XML schema,或使用IDE的自動補全功能來查看有哪些屬性和子元素是可以配置的。

21.16.3 轉換與格式化

數字的Number類型和日期Date類型的格式化是默認安裝了的,包括@NumberFormat注解和@DateTimeFormat注解。如果classpath路徑下存在Joda Time依賴,那麼完美支持Joda Time的時間格式化庫也會被安裝好。如果要注冊定制的格式化器或轉換器,請覆寫addFormatters方法:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // Add formatters and/or converters
    }

}

 

使用MVC命名空間時,<mvc:annotation-driven>也會進行同樣的默認配置。要注冊定制的格式化器和轉換器,只需要提供一個轉換服務ConversionService

<?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 conversion-service="conversionService"/>

    <bean id="conversionService"
            class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean class="org.example.MyConverter"/>
            </set>
        </property>
        <property name="formatters">
            <set>
                <bean class="org.example.MyFormatter"/>
                <bean class="org.example.MyAnnotationFormatterFactory"/>
            </set>
        </property>
        <property name="formatterRegistrars">
            <set>
                <bean class="org.example.MyFormatterRegistrar"/>
            </set>
        </property>
    </bean>

</beans>

 

關於如何使用格式化管理器FormatterRegistrar,請參考 8.6.4 FormatterRegistrar SPI一節,以及FormattingConversionServiceFactoryBean的文檔。

21.16.4 驗證

Spring提供了一個驗證器Validator接口,應用的任何一層都可以使用它來做驗證。在Spring MVC中,你可以配置一個全局的Validator實例,用以處理所有注解了@Valid的元素或注解了@Validated的控制器方法參數、以及/或在控制器內的@InitBinder方法中用作局部的Validator。全局驗證器與局部驗證器實例可以結合起來使用,提供組合驗證。

Spring還支持JSR-303/JSR-349的Bean驗證。這是通過LocalValidatorFactoryBean類實現的,它為Spring的驗證器接口org.springframework.validation.Validator到Bean驗證的javax.validation.Validator接口做了適配。這個類可以插入到Spring MVC的上下文中,作為一個全局的驗證器,如下所述。

如果在classpath下存在Bean驗證器,諸如Hibernate Validator等,那麼@EnableWebMvc<mvc:annotation-driven>默認會自動使用LocalValidatorFactoryBean為Spring MVC應用提供Bean驗證的支持。

有時,能將LocalValidatorFactoryBean直接注入到控制器或另外一個類中會更方便。

Sometimes it's convenient to have a LocalValidatorFactoryBean injected into a controller or another class. The easiest way to do that is to declare your own @Bean and also mark it with @Primary in order to avoid a conflict with the one provided with the MVC Java config.

If you prefer to use the one from the MVC Java config, you'll need to override the mvcValidator method from WebMvcConfigurationSupport and declare the method to explicitly return LocalValidatorFactory rather than Validator. See Section 21.16.13, "Advanced Customizations with MVC Java Config" for information on how to switch to extend the provided configuration.

此外,你也可以配置你自己的全局Validator驗證器實例:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public Validator getValidator(); {
        // return "global" validator
    }

}

 

XML中做法如下:

<?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 validator="globalValidator"/>

</beans>

 

若要同時使用全局驗證和局部驗證,只需添加一個(或多個)局部驗證器即可:

@Controller
public class MyController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new FooValidator());
    }

}

 

做完這個最少的配置之後,任何時候只要方法中有參數注解了@Valid@Validated,配置的驗證器就會自動對它們做驗證。任何無法通過的驗證都會被自動報告為錯誤並添加到BindingResult對象中去,你可以在方法參數中聲明它並獲取這些錯誤,同時這些錯誤也能在Spring MVC的HTML視圖中被渲染。

21.16.5 攔截器

你可以配置處理器攔截器HandlerInterceptors或web請求攔截器WebRequestInterceptors等攔截器,並配置它們攔截所有進入容器的請求,或限定到符合特定模式的URL路徑。

在MVC Java編程配置下注冊攔截器的方法:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LocaleInterceptor());
        registry.addInterceptor(new ThemeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
        registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
    }

}

 

在MVC XML命名空間下,則使用<mvc:interceptors>元素:

<mvc:interceptors>
    <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/admin/**"/>
        <bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
    </mvc:interceptor>
    <mvc:interceptor>
        <mvc:mapping path="/secure/*"/>
        <bean class="org.example.SecurityInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

 

21.16.6 內容協商

You can configure how Spring MVC determines the requested media types from the request. The available options are to check the URL path for a file extension, check the "Accept" header, a specific query parameter, or to fall back on a default content type when nothing is requested. By default the path extension in the request URI is checked first and the "Accept" header is checked second.

The MVC Java config and the MVC namespace register json, xml, rss, atom by default if corresponding dependencies are on the classpath. Additional path extension-to-media type mappings may also be registered explicitly and that also has the effect of whitelisting them as safe extensions for the purpose of RFD attack detection (see the section called "Suffix Pattern Matching and RFD" for more detail).

Below is an example of customizing content negotiation options through the MVC Java config:

_@Configuration_
_@EnableWebMvc_
public class WebConfig extends WebMvcConfigurerAdapter {

    _@Override_
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.mediaType("json", MediaType.APPLICATION_JSON);
    }
}

 

In the MVC namespace, the <mvc:annotation-driven> element has a content- negotiation-manager attribute, which expects a ContentNegotiationManager that in turn can be created with a ContentNegotiationManagerFactoryBean:

<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/>

<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
    <property name="mediaTypes">
        <value>
            json=application/json
            xml=application/xml
        </value>
    </property>
</bean>

 

If not using the MVC Java config or the MVC namespace, you'll need to create an instance of ContentNegotiationManager and use it to configure RequestMappingHandlerMapping for request mapping purposes, and RequestMappingHandlerAdapter and ExceptionHandlerExceptionResolver for content negotiation purposes.

Note that ContentNegotiatingViewResolver now can also be configured with a ContentNegotiationManager, so you can use one shared instance throughout Spring MVC.

In more advanced cases, it may be useful to configure multiple ContentNegotiationManager instances that in turn may contain custom ContentNegotiationStrategy implementations. For example you could configure ExceptionHandlerExceptionResolver with a ContentNegotiationManager that always resolves the requested media type to "application/json". Or you may want to plug a custom strategy that has some logic to select a default content type (e.g. either XML or JSON) if no content types were requested.

21.16.7 視圖控制器

以下的一段代碼相當於定義一個ParameterizableViewController視圖控制器的快捷方式,該控制器會立即將一個請求轉發(forwards)給一個視圖。請確保僅在以下情景下才使用這個類:當控制器除了將視圖渲染到響應中外不需要執行任何邏輯時。

以下是一個例子,展示了如何在MVC Java編程配置方式下將所有"/"請求直接轉發給名字為"home"的視圖:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("home");
    }

}

 

在MVC XML命名空間下完成同樣的配置,則使用<mvc:view-controller>元素:

<mvc:view-controller path="/" view-name="home"/>

 

21.16.8 視圖解析器

MVC提供的配置簡化了視圖解析器的注冊工作。

以下的代碼展示了在MVC Java編程配置下,如何為內容協商配置FreeMarker HTML模板和Jackson作為JSON數據的默認視圖解析:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.enableContentNegotiation(new MappingJackson2JsonView());
        registry.jsp();
    }

}

 

在MVC XML命名空間下實現相同配置:

<mvc:view-resolvers>
    <mvc:content-negotiation>
        <mvc:default-views>
            <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
        </mvc:default-views>
    </mvc:content-negotiation>
    <mvc:jsp/>
</mvc:view-resolvers>

 

需要注意的是,使用FreeMarker, Velocity, Tiles, Groovy Markup及script模板作為視圖技術時,仍需要配置一些其他選項。

MVC命名空間為每種視圖都提供了相應的元素。比如下面代碼是FreeMarker需要的配置:

<mvc:view-resolvers>
    <mvc:content-negotiation>
        <mvc:default-views>
            <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
        </mvc:default-views>
    </mvc:content-negotiation>
    <mvc:freemarker cache="false"/>
</mvc:view-resolvers>

<mvc:freemarker-configurer>
    <mvc:template-loader-path location="/freemarker"/>
</mvc:freemarker-configurer>

 

在MVC Java編程配置方式下,添加一個視圖對應的“配置器”bean即可:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.enableContentNegotiation(new MappingJackson2JsonView());
        registry.freeMarker().cache(false);
    }

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("/WEB-INF/");
        return configurer;
    }

}

 

資源的服務

21.16.10 回到默認的Servlet來進行資源服務

這些配置允許你將DispatcherServlet映射到"/"路徑(也即覆蓋了容器默認Servlet的映射),但依然保留容器默認的Servlet以處理靜態資源的請求。這可以通過配置一個URL映射到"/**"的處理器DefaultServletHttpRequestHandler來實現,並且該處理器在其他所有URL映射關系中優先級應該是最低的。

該處理器會將所有請求轉發(forward)到默認的Servlet,因此需要保證它在所有URL處理器映射HandlerMappings的最後。如果你是通過<mvc:annotation-driven>的方式進行配置,或自己定制了HandlerMapping實例,那麼你需要確保該處理器order屬性的值比DefaultServletHttpRequestHandler的次序值Integer.MAXVALUE小。

使用默認的配置啟用該特性,你可以:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

}

 

XML命名空間只需一行:

 <mvc:default-servlet-handler/>

 

不過需要注意,覆寫了"/"的Servlet映射後,默認Servlet的RequestDispatcher就必須通過名字而非路徑來取得了。DefaultServletHttpRequestHandler會 嘗試在容器初始化的時候自動檢測默認Servlet,這裡它使用的是一份主流Servlet容器(包括Tomcat、Jetty、GlassFish、 JBoss、Resin、WebLogic,和WWebSphere)已知的名稱列表。如果默認Servlet被配置了一個其他的名字,或者使用了一個列 表裡未提供默認Servlet名稱的容器,那麼默認Servlet的名稱必須被顯式指定。正如下面代碼所示:

  @Configuration
    @EnableWebMvc
    public class WebConfig extends WebMvcConfigurerAdapter {

        @Override
        public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
            configurer.enable("myCustomDefaultServlet");
        }

    }

 

XML命名空間的配置方式:

  <mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>

 

21.16.11 路徑匹配

這些配置允許你對許多與URL映射和路徑匹配有關的設置進行定制。關於所有可用的配置選項,請參考PathMatchConfigurer類的API文檔。

下面是采用MVC Java編程配置的一段代碼:

@Configuration
    @EnableWebMvc
    public class WebConfig extends WebMvcConfigurerAdapter {

        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            configurer
                .setUseSuffixPatternMatch(true)
                .setUseTrailingSlashMatch(false)
                .setUseRegisteredSuffixPatternMatch(true)
                .setPathMatcher(antPathMatcher())
                .setUrlPathHelper(urlPathHelper());
        }

        @Bean
        public UrlPathHelper urlPathHelper() {
            //...
        }

        @Bean
        public PathMatcher antPathMatcher() {
            //...
        }

    }

 

在XML命名空間下實現同樣的功能,可以使用<mvc:path-matching>元素:

  <mvc:annotation-driven>
        <mvc:path-matching
            suffix-pattern="true"
            trailing-slash="false"
            registered-suffixes-only="true"
            path-helper="pathHelper"
            path-matcher="pathMatcher"/>
    </mvc:annotation-driven>

    <bean id="pathHelper" class="org.example.app.MyPathHelper"/>
    <bean id="pathMatcher" class="org.example.app.MyPathMatcher"/>

 

21.16.12 消息轉換器

使用MVC Java編程配置方式時,如果你想替換Spring MVC提供的默認轉換器,完全定制自己的HttpMessageConverter,這可以通過覆寫configureMessageConverters()方法來實現。如果你只是想定制一下,或者想在默認轉換器之外再添加其他的轉換器,那麼可以通過覆寫extendMessageConverters()方法來實現。

下面是一段例子,它使用定制的ObjectMapper構造了新的Jackson的JSON和XML轉換器,並用它們替換了默認提供的轉換器:

  @Configuration
    @EnableWebMvc
    public class WebConfiguration extends WebMvcConfigurerAdapter {

        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
                    .indentOutput(true)
                    .dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
                    .modulesToInstall(new ParameterNamesModule());
            converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
            converters.add(new MappingJackson2XmlHttpMessageConverter(builder.xml().build()));
        }

    }

 

在上面的例子中,Jackson2ObjectMapperBuilder用於為MappingJackson2HttpMessageConverterMappingJackson2XmlHttpMessageConverter轉換器創建公共的配置,比如啟用tab縮進、定制的日期格式,並注冊了一個模塊jackson-module-parameter-names用於獲取參數名(Java 8新增的特性)

除了jackson- dataformat-xml,要啟用Jackson XML的tab縮進支持,還需要一個woodstox-core-asl依賴。

還有其他有用的Jackson模塊可以使用:

在XML做同樣的事也是可能的:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <property name="objectMapper" ref="objectMapper"/>
        </bean>
        <bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter">
            <property name="objectMapper" ref="xmlMapper"/>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"
      p:indentOutput="true"
      p:simpleDateFormat="yyyy-MM-dd"
      p:modulesToInstall="com.fasterxml.jackson.module.paramnames.ParameterNamesModule"/>

<bean id="xmlMapper" parent="objectMapper" p:createXmlMapper="true"/>

 

21.16.13 使用MVC Java編程進行高級定制

從 上面許多例子你可以看到,MVC Java編程配置和MVC命名空間的方式都提供了更高抽象層級的應用配置,它不需要你對底下創建的bean有非常深入的了解,相反,這使得你能僅專注於應 用需要的配置。不過,有時你可能希望對應用的更精細控制,或你就是單純希望理解底下的配置和機制。

要做到更精細的控制,你要做的第一步就是看看底層都為你創建了哪些bean。若你使用MVC Java編程的方式進行配置,你可以看看java文檔,以及WebMvcConfigurationSupport類的@Bean方法。這個類有的配置都會自動被@EnableWebMvc注解導入。事實上,如果你打開@EnableWebMvc的聲明,你就會看到應用於其上的@Import注解。

精細控制的下一步是選擇一個WebMvcConfigurationSupport創建的bean,定制它的屬性,或你可以提供自己的一個實例。這確保做到以下兩步:移除@EnableWebMvc注解以避免默認配置被自動導入,然後繼承DelegatingWebMvcConfiguration類,它是WebMvcConfigurationSupport的一個子類。以下是一個例子:

@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        // ...
    }

    @Override
    @Bean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
        // 自己創建適配器,或者調用super讓基類處理
        // 然後在這裡定制bean的一些屬性
    }

}

 

應用應該只有一個繼承DelegatingWebMvcConfiguration的配置類,或只有一個@EnableWebMvc注解的類,因為它們背後注冊的bean都是相同的。

使用這個方式修改bean的屬性,與這節前面展示的任何高抽象層級的配置方式並不沖突。WebMvcConfigurerAdapter的子類和WebMvcConfigurer的實現都還是會被使用。

21.16.14 使用MVC命名空間進行高級的定制化

如果使用MVC命名空間,要在默認配置的基礎上實現粒度更細的控制,則要比使用MVC Java編程配置的方式難一些。

如果你確實需要這麼做,那也盡量不要復制默認提供的配置,請嘗試配置一個BeanPostProcessor後置處理器,用它來檢測你要定制的bean。可以通過bean的類型來找,找到以後再修改需要定制的屬性值。比如這樣:

@Component
public class MyPostProcessor implements BeanPostProcessor {

    public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
        if (bean instanceof RequestMappingHandlerAdapter) {
            // 修改適配器的屬性
        }
    }

}

 

注意,MyPostProcessor需要被包含在<component scan/>的路徑下,這樣它才能被自動檢測到;或者你也可以手動顯式地用一個XML的bean定義來聲明它。

歡迎加群JAVA編程交流群 574337670

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved