聲明:本篇文檔主要是用於參考幫助文檔,沒有實例,但幾乎包含了SpringMVC 4.2版本的所有核心技術
對於覺得篇幅長的文檔,建議大家使用快捷鍵crtl + F,搜索關鍵字查詢較為方便.
歡迎加群JAVA編程交流群 574337670
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)”一節。或者,你可以參考:
你無法增強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模板的模型等。
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相關的特性:
等)有些項目可能更傾向於使用非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作為一個中間層和/或數據存取層來使用。
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 theDispatcherServlet
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
。
Spring的DispatcherServlet
使用了特殊的bean來處理請求、渲染視圖等,這些特定的bean是Spring MVC框架的一部分。如果你想指定使用哪個特定的bean,你可以在web應用上下文WebApplicationContext
中簡單地配置它們。當然這只是可選的,Spring MVC維護了一個默認的bean列表,如果你沒有進行特別的配置,框架將會使用默認的bean。下一小節會介紹更多的細節,這裡,我們將先快速地看一下,DispatcherServlet
都依賴於哪些特殊的bean來進行它的初始化。
HandlerMapping
處理器映射。它會根據某些規則將進入容器的請求映射到具體的處理器以及一系列前處理器和後處理器(即處理器攔截器)上。具體的規則視HandlerMapping
類的實現不同而有所不同。其最常用的一個實現支持你在控制器上添加注解,配置請求路徑。當然,也存在其他的實現。
HandlerAdapter
處理器適配器。拿到請求所對應的處理器後,適配器將負責去調用該處理器,這使得DispatcherServlet
無需關心具體的調用細節。比方說,要調用的是一個基於注解配置的控制器,那麼調用前還需要從許多注解中解析出一些相應的信息。因此,HandlerAdapter
的主要任務就是對DispatcherServlet
屏蔽這些具體的細節。
HandlerExceptionResolver
處理器異常解析器。它負責將捕獲的異常映射到不同的視圖上去,此外還支持更復雜的異常處理代碼。
ViewResolver
視圖解析器。它負責將一個代表邏輯視圖名的字符串(String)映射到實際的視圖類型View
上。
LocaleResolver
& LocaleContextResolver
地區解析器 和 地區上下文解析器。它們負責解析客戶端所在的地區信息甚至時區信息,為國際化的視圖定制提供了支持。
ThemeResolver
主題解析器。它負責解析你web應用中可用的主題,比如,提供一些個性化定制的布局等。
MultipartResolver
解析multi-part的傳輸請求,比如支持通過HTML表單進行的文件上傳等。
FlashMapManager
FlashMap管理器。它能夠存儲並取回兩次請求之間的FlashMap
對象。後者可用於在請求之間傳遞數據,通常是在請求重定向的情境下使用。
上一小節講到,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應用提供了簡易的開始方式,也不需要你對框架實現細節有太多了解。當然,無論你選用何種方式開始配置,本節所介紹的一些概念都是基礎且普適的,它們 對你後續的學習都應有所助益。
配置好DispatcherServlet
以後,開始有請求會經過這個DispatcherServlet
。此時,DispatcherServlet
會依照以下的次序對請求進行處理:
WebApplicationContext
並把它作為一個屬性(attribute)綁定到該請求上,以便控制器和其他組件能夠使用它。屬性的鍵名默認為DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE
MultipartHttpServletRequest
對象,以便處理鏈中的其他組件對它做進一步的處理。關於Spring對multipart文件傳輸處理的支持,讀者可以參考21.10 Spring的multipart(文件上傳)支持一小節如果在處理請求的過程中拋出了異常,那麼上下文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
...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環境下最常被使用到的一些場景。
[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>
你可以使用 @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) 以縮小范圍。
有時,我們希望在運行時使用AOP代理來裝飾控制器,比如當你直接在控制器上使用 @Transactional 注解時。這種情況下,我們推薦使用類級別(在控制器上使用)的代理方式。這一般是代理控制器的默認做法。如果控制器必須實現一些接口,而該接口又不支持Spring Context的回調(比如 InitializingBean, *Aware 等接口),那要配置類級別的代理就必須手動配置了。比如,原來的配置文件 <tx:annotation-driven/> 需要顯式配置為 <tx:annotation-driven proxy-target-class="true"/> 。
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 aHandlerExceptionResolver
can now expect the Object-based handler to be aHandlerMethod
, 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
orBeanNameUrlHandlerMapping
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.
同時,也有其他的一些變化,比如有些事情就沒法這麼玩兒了:
[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
[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 yieldshttp://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 ofownerId
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 ofownerId
isfred
.
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初始化過程”一節
[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) { // 代碼部分省略... } }
[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} 這樣的用法等)。
[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
inAntPathMatcher
. Note that the PathMatcher can be customized (see Section 21.16.11, "Path Matching" in the section on configuring Spring MVC).
更多的細節請參考這兩個類:AntPatternComparator
和AntPathMatcher
。值得一提的是,PathMatcher類是可以配置的(見“配置Spring MVC”一節中的21.16.11 路徑的匹配一節)。
[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 thePropertyPlaceholderConfigurer
class.
@RequestMapping
注解支持在路徑中使用占位符,以取得一些本地配置、系統配置、環境變量等。這個特性有時很有用,比如說控制器的映射路徑需要通過配置來定制的場景。如果想了解更多關於占位符的信息,可以參考PropertyPlaceholderConfigurer
這個類的文檔。
[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小節 "內容協商"的內容。
[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的@ResponseBody
和ResponseEntity
方法是有風險的,因為它們會根據客戶的請求——包括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:
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 tofalse
.而使用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 asAPPLICATION_JSON_VALUE
andAPPLICATION_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 theUTF-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
andAPPLICATION_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-Type和 Accept的值,但我們更推薦獨立使用 consumes和 produces條件來篩選各自的請求。因為它們就是專門為區分這兩種不同的場景而生的。
使用 @RequestMapping 注解的處理方法可以擁有非常靈活的方法簽名,它支持的方法參數及返回值類型將在接下來的小節講述。大多數參數都可以任意的次序出現,除了唯一的一個例外: BindingResult 參數。這在下節也會詳細描述。
Spring 3.1中新增了一些類,用以增強注解了 @RequestMapping 的處理方法,分別是 RequestMappingHandlerMapping 類和 RequestMappingHandlerAdapter 類。我們鼓勵使用這組新的類,如果要使用Spring 3.1及以後版本的新特性,這組類甚至是必須使用的。這些增強類在MVC的命名空間配置和MVC的Java編程方式配置中都是默認開啟的,如果不是使用這兩種方法,那麼就需要顯式地配置。
下面列出所有支持的方法參數類型:
存 取session可能不是線程安全的,特別是在一個Servlet的運行環境中。如果應用可能有多個請求同時並發存取一個session場景,請考慮將 RequestMappingHandlerAdapter 類中的"synchronizeOnSession"標志設置為"true"。
@RequestPart
注解的參數,提供了對一個"multipart/form-data請求塊(request part)內容的存取。更多的信息請參考21.10.5 “處理客戶端文件上傳的請求”一節和21.10 “Spring對多部分文件上傳的支持”一節 java.util.Map/org.springframework.io.Model/org.springframework.ui.ModelMap
類型的參數,用以增強默認暴露給視圖層的模型(model)的功能 org.springframework.validation.Errors / org.springframework.validation.BindingResult
驗證結果對象,用於存儲前面的命令或表單對象的驗證結果(緊接其前的第一個方法參數)。在參數列表中, 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
類型的方法參數來增強modelString
對象,其值會被解析成一個邏輯視圖名。其中,model將默認填充了命令對象以及注解了@ModelAttribute
字段的存取器被調用所返回的值。handler方法也可以增加一個Model
類型的方法參數來增強modelvoid
。如果處理器方法中已經對response響應數據進行了處理(比如在方法參數中定義一個ServletResponse
或HttpServletResponse
類型的參數並直接向其響應體中寫東西),那麼方法可以返回void。handler方法也可以增加一個Model
類型的方法參數來增強modelResponseBody
,那麼返回類型將被寫到HTTP的響應體中,而返回值會被HttpMessageConverters
轉換成所方法聲明的參數類型。詳見使用"@ResponseBody注解映射響應體"一節HttpEntity<?>
或ResponseEntity<?>
對象,用於提供對Servlet HTTP響應頭和響應內容的存取。對象體會被HttpMessageConverters
轉換成響應流。詳見使用HttpEntity一節HttpHeaders
對象,返回一個不含響應體的responseCallable<?>
對象。當應用希望異步地返回方法值時使用,這個過程由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
@ModelAttribute
所注解的字段名(或者以返回類型的類名作為默認的屬性名)。model隱含填充了命令對象以及注解了@ModelAttribute
字段的存取器被調用所返回的值你可以使用 @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 注解暗示了方法參數應該被綁定了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 支持:
關於這些轉換器的更多信息,請參考"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 注解與 @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信息轉換器"一節。
當今讓控制器實現一個REST API是非常常見的,這種場景下控制器只需要提供JSON、XML或其他自定義的媒體類型內容即可。你不需要在每個 @RequestMapping 方法上都增加一個 @ResponseBody 注解,更簡明的做法是,給你的控制器加上一個 @RestController 的注解。
@RestController
是一個原生內置的注解,它結合了 @ResponseBody 與 @Controller 注解的功能。不僅如此,它也讓你的控制器更表義,而且在框架未來的發布版本中,它也可能承載更多的意義。
與普通的 @Controller 無異, @RestController 也可以與 @ControllerAdvicebean 配合使用。更多細節,請見使用@ControllerAdvice輔助控制器。
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 說明了方法的作用是用於添加一個或多個屬性到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 說 明了該方法參數的值將由model中取得。如果model中找不到,那麼該參數會先被實例化,然後被添加到model中。在model中存在以後,請求中 所有名稱匹配的參數都會填充到該參數中。這在Spring MVC中被稱為數據綁定,一個非常有用的特性,節約了你每次都需要手動從表格數據中轉換這些字段數據的時間。
@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST) public String processSubmit(@ModelAttribute Pet pet) { }
以上面的代碼為例,這個Pet類型的實例可能來自哪裡呢?有幾種可能:
@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 注解聲明了某個特定處理器所使用的會話屬性。通常它會列出該類型希望存儲到 session 或 converstaion 中的model屬性名或model的類型名,一般是用於在請求之間保存一些表單數據的 bean 。
以下的代碼段演示了該注解的用法,它指定了模型屬性的名稱
@Controller @RequestMapping("/editPet.do") @SessionAttributes("pet") public class EditPetForm { // ... }
上一小節講述了如何使用 @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字段格式化"一節)來做到。
如果想通過Spring的 WebDataBinder 在屬性編輯器中做請求參數的綁定,你可以使用在控制器內使用 @InitBinder @InitBinder 注解的方法、在注解了 @ControllerAdvice 的類中使用 @InitBinder 注解的方法,或者提供一個定制的 WebBindingInitializer 。更多的細節,請參考使用@ControllerAdvice輔助控制器一節。
使用 @InitBinder 注解控制器的方法,你可以直接在你的控制器類中定制應用的數據綁定。 @InitBinder 用來標記一些方法,這些方法會初始化一個WebDataBinder
並用以為處理器方法填充命令對象和表單對象的參數。
除了命令/表單對象以及相應的驗證結果對象,這樣的“綁定器初始化”方法能夠接收 @RequestMapping 所支持的所有參數類型。“綁定器初始化”方法不能有返回值,因此,一般將它們聲明為void
返回類型。特別地,當WebDataBinder
與WebRequest
或java.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")); } // ... }
為了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 是一個組件注解,它使得其實現類能夠被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
的文檔。
下面兩節,還看不太懂,待譯。
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 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"; } }
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.
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的異步請求處理特性沒有了解,理解這個特性可能會有點困難。因此,閱讀一下前者的文檔將會很有幫助。以下給出了這個機制運作背後的一些原理:
dispatch
方法,只不過它允許應用恢復Servlet容器的請求處理進程有了上面的知識,下面可以來看一下 Callable 的異步請求被處理時所依次發生的事件:
Callable
對象Callable
對象提交給另一個獨立線程的執行器TaskExecutor
處理DispatcherServlet
和所有過濾器都退出Servlet容器線程,但此時方法的響應對象仍未返回Callable
對象最終產生一個返回結果,此時Spring MVC會重新把請求分派回Servlet容器,恢復處理DispatcherServlet
再次被調用,恢復對Callable
異步處理所返回結果的處理對DeferredResult
異步請求的處理順序也非常類似,區別僅在於應用可以通過任何線程來計算返回一個結果:
DeferredResult
對象,並把它存取在內存(隊列或列表等)中以便存取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
實例中,後者提供了相關的支持。
如前所述,控制器可以使用DeferredResult
或Callable
對象來異步地計算其返回值,這可以用於實現一些有用的技術,比如 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.
SseEmitter 是 ResponseBodyEmitter 的一個子類,提供了對服務器端事件推送的技術的支持。服務器端事件推送其實只是一種HTTP Streaming的類似實現,只不過它服務器端所推送的事件遵循了W3C Server-Sent Events規范中定義的事件格式。
“服務器端事件推送”技術正如其名,是用於由服務器端向客戶端進行的事件推送。這在Spring MVC中很容易做到,只需要方法返回一個 SseEmitter 類型的對象即可。
需 要注意的是,Internet Explorer並不支持這項服務器端事件推送的技術。另外,對於更大型的web應用及更精致的消息傳輸場景——比如在線游戲、在線協作、金融應用等—— 來說,使用Spring的WebSocket(包含SockJS風格的實時WebSocket)更成熟一些,因為它支持的浏覽器范圍非常廣(包括IE), 並且,對於一個以消息為中心的架構中,它為服務器端-客戶端間的事件發布-訂閱模型的交互提供了更高層級的消息模式(messaging patterns)的支持。
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 體裡面使用,這可以對響應狀態和響應頭做一些定制。
對於那些使用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.xml
將DispatcherServlet
下的子元素 <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
過濾器實例變得非常簡單。
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
類型的參數。
spring-test
模塊對測試控制器@Controller
提供了最原生的支持。詳見14.6 "Spring MVC測試框架"一節。
在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>
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
方法不一定總是適用於注解了@ResponseBody
或ResponseEntity
的方法。這些場景中,HttpMessageConverter
會在攔截器的postHandle
方法被調之前就把信息寫回響應中。這樣攔截器就無法再改變響應了,比如要增加一個響應頭之類的。如果有這種需求,請讓你的應用實現ResponseBodyAdvice
接口,並將其定義為一個@ControllerAdvice
bean或直接在RequestMappingHandlerMapping
中配置。
所 有web應用的MVC框架都提供了視圖相關的支持。Spring提供了一些視圖解析器,它們讓你能夠在浏覽器中渲染模型,並支持你自由選用適合的視圖技術 而不必與框架綁定到一起。Spring原生支持JSP視圖技術、Velocity模板技術和XSLT視圖等。你可以閱讀文檔的第22章 視圖技術一章,裡面討論了如何集成並使用許多獨立的視圖技術。
有兩個接口在Spring處理視圖相關事宜時至關重要,分別是視圖解析器接口 ViewResolver 和視圖接口本身View
。視圖解析器 ViewResolver 負責處理視圖名與實際視圖之間的映射關系。視圖接口View
負責准備請求,並將請求的渲染交給某種具體的視圖技術實現。
正如在21.3 控制器的實現一節中所討論的,Spring MVC中所有控制器的處理器方法都必須返回一個邏輯視圖的名字,無論是顯式返回(比如返回一個String
、View
或者ModelAndView
)還是隱式返回(比如基於約定的返回)。Spring中的視圖由一個視圖名標識,並由視圖解析器來渲染。Spring有非常多內置的視圖解析器。下表列出了大部分,表後也給出了一些例子。
表21.3 視圖解析器
AbstractCachingViewResolver
一個抽象的視圖解析器類,提供了緩存視圖的功能。通常視圖在能夠被使用之前需要經過准備。繼承這個基類的視圖解析器即可以獲得緩存視圖的能力。
XmlViewResolver
視圖解析器接口ViewResolver
的一個實現,該類接受一個XML格式的配置文件。該XML文件必須與Spring XML的bean工廠有相同的DTD。默認的配置文件名是/WEB-INF/views.xml
。
ResourceBundleViewResolver
視圖解析器接口ViewResolver
的一個實現,采用bundle根路徑所指定的ResourceBundle
中的bean定義作為配置。一般bundle都定義在classpath路徑下的一個配置文件中。默認的配置文件名為views.properties
。
UrlBasedViewResolver
ViewResolver
接口的一個簡單實現。它直接使用URL來解析到邏輯視圖名,除此之外不需要其他任何顯式的映射聲明。如果你的邏輯視圖名與你真正的視圖資源名是直接對應的,那麼這種直接解析的方式就很方便,不需要你再指定額外的映射。
InternalResourceViewResolver
UrlBasedViewResolver
的一個好用的子類。它支持內部資源視圖(具體來說,Servlet和JSP)、以及諸如JstlView
和TilesView
等類的子類。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)
方法。`
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
永遠都會 返回一個視圖。
如前所述,控制器通常都會返回一個邏輯視圖名,然後視圖解析器會把它解析到一個具體的視圖技術上去渲染。對於一些可以由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
數據再發送一遍。
強制重定向的一種方法是,在控制器中創建並返回一個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的時候自動對應用可見,而不需要顯式地在Model
或RedirectAttributes
中再添加屬性。請看下面的例子:
@RequestMapping(path = "/files/{path}", method = RequestMethod.POST) public String upload(...) { // ... return "redirect:files/{path}"; }
另外一種向重定向目標傳遞數據的方法是通過 閃存屬性(Flash Attributes)。與其他重定向屬性不同,flash屬性是存儲在HTTP session中的(因此不會出現在URL中)。更多內容,請參考 21.6 使用閃存屬性一節。
盡管使用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
設置的響應狀態碼值。
對於最終會被UrlBasedViewResolver
或其子類解析的視圖名,你可以使用一個特殊的前綴:forward:
。這會導致一個InternalResourceView
視圖對象的創建(它最終會調用RequestDispatcher.forward()
方法),後者會認為視圖名剩下的部分是一個URL。因此,這個前綴在使用InternalResourceViewResolver
和InternalResourceView
時並沒有特別的作用(比如對於JSP來說)。但當你主要使用的是其他的視圖技術,而又想要強制把一個資源轉發給Servlet/JSP引擎進行處理時,這個前綴可能就很有用(或者,你也可能同時串聯多個視圖解析器)。
與redirect:
前綴一樣,如果控制器中的視圖名使用了forward:
前綴,控制器本身並不會發覺任何異常,它關注的仍然只是如何處理響應的問題。
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:
<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.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.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.
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; } }
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屬性。
在Spring MVC中,使用了UriComponentsBuilder
和UriComponents
兩個類來提供一種構造和加密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()
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();
Spring的架構中的很多層面都提供了對國際化的支持,同樣支持Spring MVC框架也能提供。DispatcherServlet
為你提供了自動使用用戶的地區信息來解析消息的能力。而這,是通過LocaleResolver
對象來完成的。
一個請求進入處理時,DispatcherServlet
會查找一個地區解析器。如果找到,就嘗試使用它來設置地區相關的信息。通過調用RequestContext.getLocale()
都能取到地區解析器所解析到的地區信息。
此外,如果你需要自動解析地區信息,你可以在處理器映射前加一個攔截器(關於更多處理器映射攔截器的知識,請參見21.4.1 使用HandlerInterceptor攔截請求一小節),並用它來根據條件或環境不同,比如,根據請求中某個參數值,來更改地區信息。
除了獲取客戶端的地區信息外,有時他們所在的時區信息也非常有用。LocaleContextResolver
接口為LocaleResolver
提供了拓展點,允許解析器在LocaleContext
中提供更多的信息,這裡面就可以包含時區信息。
如果用戶的時區信息能被解析到,那麼你總可以通過RequestContext.getTimeZone()
方法獲得。時區信息會自動被SpringConversionService
下注冊的日期/時間轉換器Converter
及格式化對象Formatter
所使用。
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支持的屬性
SessionLocaleResolver
允許你從session中取得可能與用戶請求相關聯的地區Locale
和時區TimeZone
信息。與CookieLocaleResolver
不同,這種存取策略僅將Servlet容器的HttpSession
中相關的地區信息存取到本地。因此,這些設置僅會為該會話(session)臨時保存,session結束後,這些設置就會失效。
不過請注意,該解析器與其他外部session管理機制,比如Spring的Session項目等,並沒有直接聯系。該SessionLocaleResolver
僅會簡單地從與當前請求HttpServletRequest
相關的HttpSession
對象中,取出對應的屬性,並修改其值,僅此而已。
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
攔截器來更改地區信息。它能檢測請求中的參數,並根據其值相應地更新地區信息。它通過調用LocaleResolver
的setLocale()
方法來更改地區。下面的代碼配置展示了如何為所有請求*.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>
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),這可以提高用戶體驗。主題 是指一系列靜態資源的集合,並且主要是樣式表和圖片,它們決定了你的應用的視覺風格。
要在你的應用中使用主題,你必須實現一個 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/classes
。ResourceBundleThemeSource
采用了Java的標准資源bundle加載機制,完全支持國際化主題。比如,你可以創建一個/WEB-INF/classes/cool_nl.properties
配置文件,並在其中引用一副有荷蘭文的背景圖片。
上一小節,我們講了如何定義主題,定義之後,你要決定使用哪個主題。DispatcherServlet
會查找一個名稱為themeResolver
的bean以確定使用哪個ThemeResolver
的實現。主題解析器的工作原理與地區解析器LocaleResolver
的工作原理大同小異。它會檢測,對於一個請求來說,應該使用哪個主題,同時它也可以修改一個請求所應應用的主題。Spring提供了下列的這些主題解析器:
表21.5. ThemeResolver接口的實現
FixedThemeResolver
選擇一個固定的主題,這是通過設置defaultThemeName
這個屬性值實現的
SessionThemeResolver
請求相關的主題保存在用戶的HTTP會話(session)中。對於每個會話來說,它只需要被設置一次,但它不能在會話之間保存
CookieThemeResolver
選中的主題被保存在客戶端的cookie中
Spring也提供了一個主題更改攔截器ThemeChangeInterceptor
,以支持主題的更換。這很容易做到,只需要在請求中攜帶一個簡單的請求參數即可。
Spring內置對多路上傳的支持,專門用於處理web應用中的文件上傳。你可以通過注冊一個可插拔的MultipartResolver
對象來啟用對文件多路上傳的支持。該接口在定義於 org.springframework.web.multipart 包下。Spring為一般的文件上傳提供了MultipartResolver
接口的一個實現,為Servlet 3.0多路請求的轉換提供了另一個實現。
默 認情況下,Spring的多路上傳支持是不開啟的,因為有些開發者希望由自己來處理多路請求。如果想啟用Spring的多路上傳支持,你需要在web應用 的上下文中添加一個多路傳輸解析器。每個進來的請求,解析器都會檢查是不是一個多部分請求。若發現請求是完整的,則請求按正常流程被處理;如果發現請求是 一個多路請求,則你在上下文中注冊的MultipartResolver
解析器會被用來處理該請求。之後,請求中的多路上傳屬性就與其他屬性一樣被正常對待了。【最後一句翻的不好,multipart翻譯成多路還是多部分還在斟酌中。望閱讀者注意此處。】
下面的代碼展示了如何使用一個通用的多路上傳解析器 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
對象,你不僅可以獲取該多路請求中的信息,還可以在你的控制器中獲得該多路請求的內容本身。
要使用基於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>
當解析器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"; } }
在 使用了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
被轉換成特定的對象。
Spring的處理器異常解析器 HandlerExceptionResolver 接口的實現負責處理各類控制器執行過程中出現的異常。某種程度上講,HandlerExceptionResolver
與你在web應用描述符web.xml
文 件中能定義的異常映射(exception mapping)很相像,不過它比後者提供了更靈活的方式。比如它能提供異常被拋出時正在執行的是哪個處理器這樣的信息。並且,一個更靈活 (programmatic)的異常處理方式可以為你提供更多選擇,使你在請求被直接轉向到另一個URL之前(與你使用Servlet規范的異常映射是一 樣的)有更多的方式來處理異常。
實現HandlerExceptionResolver
接口並非實現異常處理的唯一方式,它只是提供了resolveException(Exception, Hanlder)
方法的一個實現而已,方法會返回一個ModelAndView
。除此之外,你還可以框架提供的SimpleMappingExceptionResolver
或在異常處理方法上注解@ExceptionHandler
。SimpleMappingExceptionResolver
允許你獲取可能拋出的異常類的名字,並把它映射到一個視圖名上去。這與Servlet API提供的異常映射特性是功能等價的,但你也可以基於此實現粒度更精細的異常映射。而@ExceptionHandler
注解的方法則會在異常拋出時被調用以處理該異常。這樣的方法可以定義在@Controller
注解的控制器類裡,也可以定義在@ControllerAdvice
類中,後者可以使該異常處理方法被應用到更多的@Controller
控制器中。下一小節將提供更為詳細的信息。
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
注解以使用消息轉換器會轉換信息為特定類型的數據,然後把它們寫回到響應流中。
處理請求的過程中,Spring MVC可能會拋出一些的異常。SimpleMappingExceptionResolver
可以根據需要很方便地將任何異常映射到一個默認的錯誤視圖。但,如果客戶端是通過自動檢測響應的方式來分發處理異常的,那麼後端就需要為響應設置對應的狀態碼。根據拋出異常的類型不同,可能需要設置不同的狀態碼來標識是客戶端錯誤(4xx)還是服務器端錯誤(5xx)。
默認處理器異常解析器DefaultHandlerExceptionResolver
會將Spring MVC拋出的異常轉換成對應的錯誤狀態碼。該解析器在MVC命名空間配置或MVC Java配置的方式下默認已經被注冊了,另外,通過DispatcherServlet
注冊也是可行的(即不使用MVC命名空間或Java編程方式進行配置的時候)。下表列出了該解析器能處理的一些異常,及他們對應的狀態碼。
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.
業務異常可以使用@ResponseStatus
來注解。當異常被拋出時,ResponseStatusExceptionResolver
會設置相應的響應狀態碼。DispatcherServlet
會默認注冊一個ResponseStatusExceptionResolver
以供使用。
當響應的狀態碼被設置為錯誤狀態碼,並且響應體中沒有內容時,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") %> }
Spring Security項目為保護web應用免受惡意攻擊提供了一些特性。你可以去看看參考文檔的這幾小節:"CSRF保護"、"安全響應頭"以及"Spring MVC集成"。不過並非應用的所有特性都需要引入Spring Security。比如,需要CSRF保護的話,你僅需要簡單地在配置中添加一個過濾器CsrfFilter
和處理器CsrfRequestDataValueProcessor
。你可以參考Spring MVC Showcase一節,觀看一個完整的展示。
另一個選擇是使用其他專注於Web安全的框架。HDIV就是這樣的一個框架,並且它能與Spring MVC良好集成。
對於許多項目來說,不打破已有的約定,對於配置等有可預測的默認值是非常適合的。現在,Spring MVC對 約定優於配置 這個實踐已經有了顯式的支持。這意味著什麼呢?意味著如果你為項目選擇了一組命名上的約定/規范,你將能減少 大量 的配置項,比如一些必要的處理器映射、視圖解析器、ModelAndView
實例的配置等。這能幫你快速地建立起項目原型,此外在某種程度上(通常是好的方面)維護了整個代碼庫的一致性should you choose to move forward with it into production.
具體來說,支持“約定優於配置”涉及到MVC的三個核心方面:模型、視圖,和控制器。
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*
請求URLHomeController
將映射到/home*
請求URLIndexController
將映射到/index*
請求URLRegisterController
將映射到/register*
請求URL對於MultiActionController
處理器類,映射規則要稍微復雜一些。請看下面的代碼,假設這裡的控制器都是MultiActionController
的實現:
AdminController
將映射到/admin/*
請求URLCatalogController
將映射到/catalog/*
請求URL只要所有控制器Controller
實現都遵循xxxController
這樣的命名規范,那麼ControllerClassNameHandlerMapping
能把你從定義維護一個 長長長 SimpleUrlHandlerMapping
映射表的重復工作中拯救出來。
ControllerClassNameHandlerMapping
類繼承自 AbstractHandlerMapping
基類。因此,你可以視它與其他HandlerMapping
實現一樣,定義你所需要的攔截器HandlerInterceptor
實例及其他所有東西。
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
則根本不會被添加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
上。該邏輯視圖名又會由InternalResourceViewResolver
bean解析到/WEB-INF/jsp/registration.jsp
視圖上。
你無需顯式地定義一個
DefaultRequestToViewNameTranslator
bean。如果默認的DefaultRequestToViewNameTranslator
配置已能滿足你的需求,那麼你無需配置,Spring Web MVC的DispatcherServlet
會為你實例化這樣一個默認的對象。
當然,如果你需要更改默認的設置,那你就需要手動地配置自己的DefaultRequestToViewNameTranslator
bean。關於可配置屬性的一些詳細信息,你可以去咨詢DefaultRequestToViewNameTranslator
類詳細的java文檔。
一個好的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緩存的方法。
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();
為優化站點性能,靜態資源應該帶有恰當的'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>
控制器能處理帶有'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
(資源未修改)對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容器初始化一小節。
在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.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有更細粒度的定制空間。不過,我們還是從頭來看起吧。
要啟用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也會自動地注冊它們:
在MVC Java編程配置方式下,如果你想對默認配置進行定制,你可以自己實現WebMvcConfigurer
接口,要麼繼承WebMvcConfigurerAdapter
類並覆寫你需要定制的方法:
@Configuration @EnableWebMvc public class WebConfig extends WebMvcConfigurerAdapter { // Override configuration methods... }
在MVC XML命名空間下,如果你想對默認配置進行定制,請查看<mvc:annotation-driven/>
元素支持的屬性和子元素。你可以查看Spring MVC XML schema,或使用IDE的自動補全功能來查看有哪些屬性和子元素是可以配置的。
數字的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
的文檔。
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 fromWebMvcConfigurationSupport
and declare the method to explicitly returnLocalValidatorFactory
rather thanValidator
. 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視圖中被渲染。
你可以配置處理器攔截器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>
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.
以下的一段代碼相當於定義一個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"/>
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; } }
這些配置允許你將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"/>
這些配置允許你對許多與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"/>
使用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
用於為MappingJackson2HttpMessageConverter
和MappingJackson2XmlHttpMessageConverter
轉換器創建公共的配置,比如啟用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"/>
從 上面許多例子你可以看到,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
的實現都還是會被使用。
如果使用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