- 介紹Spring Web MVC 框架
- DispatcherServlet
- 實現Controller
- Handler mappings
- resolving views 解析視圖
- Spring 4 官方文檔學習(十一)Web MVC 框架之Flash Attributes
- Spring 4 官方文檔學習(十一)Web MVC 框架之URI Builder
- Spring 4 官方文檔學習(十一)Web MVC 框架之locales
- Spring 4 官方文檔學習(十一)Web MVC 框架之themes
- Spring 4 官方文檔學習(十一)Web MVC 框架之multipart(文件上傳)支持
- Spring 4 官方文檔學習(十一)Web MVC 框架之異常處理
- Spring 4 官方文檔學習(十一)Web MVC 框架之約定優於配置
- Spring 4 官方文檔學習(十一)Web MVC 框架之HTTP caching support
- Spring 4 官方文檔學習(十一)Web MVC 框架之編碼式Servlet容器初始化
- Spring 4 官方文檔學習(十一)Web MVC 框架之配置Spring MVC
Spring Web MVC 框架是圍繞DispatcherServlet設計的,所謂DispatcherServlet就是將請求分發到handler,需要有配置好的handler映射、視圖解析、本地化、時區、theme解決方案、還有上傳文件的支持。默認的handler是基於@Controller和@RequestMapping注解。自Spring 3.0 起,@Controller注解還能用於RESTful,需要配合@PathVariable以及其他特性。
Spring Web MVC 和 Spring的設計准則是“對擴展開放,對修改關閉”--可以擴展,不能修改。
Spring Web MVC中核心類的一些方法被標記為final。開發者不能覆蓋這些方法以提供自己的行為。這不是任性,而是遵從設計准則。
在Spring MVC中,你不能為final方法添加advice。
在Spring Web MVC中,你可以使用任何對象作為命令對象或者form-backing對象;你不需要實現框架的特定接口或者基類。Spring的數據綁定是高度彈性的:例如,它將類型錯誤匹配視為校驗錯誤,而非系統錯誤,從而可被應用evaluate。
Spring的view resolution是非常彈性的。Controller負責准備一個model Map,其中帶有數據,還負責挑選一個view name -- 也可以直接寫到響應流而完成一個請求。view name resolution是高度可定制的,使用文件擴展名或者Accept header content type negotiation,通過bean names、properties文件、或者,甚至一個自定義的ViewResolver實現。model(MVC中的M)是一個Map接口,允許view技術的完全抽象。你可以直接集成基於模板的rendering技術,例如JSP、Velocity和Freemarker、或者直接生成XML、JSON、Atom、以及很多其他內容類型。model Map會被簡單的變成合適的格式,如JSP請求屬性、如Velocity模板模型。
Spring Web Flow
目標是成為管理web應用頁面流程的最佳解決方案。
SWF集成了現有的框架,如Spring MVC和JSF,在Servlet和Portlet環境中。如果你有一個業務process(或多個)想從conversational model中受益(與純request model相比),那么SWF就是這種解決方案。
SWF允許你捕獲logical page flows,將其作為self-contained modules,可以在不同環境下復用,因此,這是構建web應用模塊的理想方式,能夠引導用戶-- 使用驅動業務processes的controlled navigations。
Spring的web模塊包含許多獨特的web支持特性:
- 角色完全隔離。 每個角色-- controller、validator、command object、form object、model object、DispatcherServlet、handler mapping、view resolver、等等 -- 都能使用特殊的對象來實現。
- 框架和應用classes的強大且條理清晰的配置。 這個配置能力包括不同contexts中的簡單引用,例如從web controllers到業務對象和validators的引用。
- 適用能力、非侵入式、彈性。 可以定義任何需要的controller 方法簽名,針對特定的情景 可能需要使用某個參數注解(如@RequestParam、@RequestHeader、@PathVariable等等)。
- 可復用的業務代碼,不需要重復。 將現有業務對象作為命令對象或者form對象,而非將它們鏡像到一個擴展類。
- 可定制的綁定和校驗。Type mismatches as application-level validation errors that keep the offending value, localized date and number binding, and so on instead of String-only form objects with manual parsing and conversion to business objects.
- 可定制的handler mapping和view resolution。 handler mapping 和 view resolution策略:從簡單的基於URL的配置,到復雜的構建目的的解決方案策略。相比只使用一種技術的web MVC框架來說,Spring更彈性
- 彈性的模型傳輸。 使用name/value Map的模型傳輸支持與任意view技術的簡單集成。
- 可定制的locale、time zone和theme resolution,支持JSP(使用/不使用Spring標簽),支持JSTL,支持Velocity--不需要額外的橋梁,等等。
- 簡單且強大的JSP標簽庫--Spring標簽庫,支持的特性包括數據綁定和themes。
- JSP form標簽庫,Spring 2.0引入的, 使得在JSP頁面中寫forms更簡單。
- lifecycle被限定在當前HTTP請求或HTTP Session的beans。這不是Spring MVC獨有的技術,而是Spring MVC使用的WebApplicationContext的特性。
對於某些項目來說,非Spring的MVC實現更適合。很多團隊希望借用已有的投資(囧,真抽象),例如,使用JSF。
如果你不想使用Spring Web MVC,而想使用Spring提供的其他解決方案,你可以將 你選擇的web MVC框架 集成到Spring中,這很簡單。通過Spring的ContextLoaderListener啟動Spring的root application context,可以在任意action object中通過它的ServletContext屬性來獲取它 -- 也可以使用Spring的相應幫助方法來獲取。
你注冊的beans和Spring的服務隨時可用 -- 哪怕沒有Spring MVC。這種情景下,Spring不會同其他web框架競爭。它只是簡單的致力於純web MVC框架沒有關注的地方,從bean配置到數據訪問和事務處理。所以,哪怕你只是想配合JDBC或Hibernate來使用Spring的事務抽象,仍然可以將Spring中間件 和/或 數據訪問中間件作為你應用的一部分。
像很多其他web MVC框架一樣,Spring MVC框架也是請求驅動的,圍繞一個中心Servlet來設計,該中心Servlet可以分發請求到Controllers,並提供其他功能。然而Spring的DispatcherServlet做的更多。它被徹底地與Spring IoC容器集成到了一起,所以,你可以使用Spring的所有特性。
Spring MVC DispatcherServlet處理請求的工作流程如下圖所示。聰明的讀者會認出DispatcherServlet是Front Controller設計模式的一種實現。
DispatcherServlet是一個具體的Servlet(繼承自HttpServlet基類),所以你需要使用一個URL mapping來映射請求 -- 就是你想讓DispatcherServlet處理的請求。這是一個標准Java EE Servlet配置,在Servlet 3.0+ 環境下可以這樣注冊該Servlet:
public class MyWebApplicationInitializer implements WebApplicationInitializer { // 這個接口,或者其抽象實現類 @Override public void onStartup(ServletContext container) { ServletRegistration.Dynamic registration = container.addServlet("example", new DispatcherServlet()); registration.setLoadOnStartup(1); registration.addMapping("/example/*"); } }
上面的例子中,所有以 /example開頭的請求 都會由名字為example的DispatcherServlet實例來處理。
WebApplicationInitializer是Spring MVC提供的接口,可以確保基於代碼的配置被探測到、且被自動用於初始化Servlet 3 容器。該接口的一個abstract基類是AbstractAnnotationConfigDispatcherServletInitializer
,該abstract基類注冊DispatcherServlet更簡便,只需要指定映射、列出配置類即可 -- 這是設置Spring MVC項目的一種推薦的方式(java config)。
或者,傳統模式,web.xml中的設置方式:
<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>
在前面曾講過,在Spring中,ApplicationContext實例可以被scoped (就是有scope)。
而在Spring MVC框架中,每個DispatcherServlet都有其自有的WebApplicationContext,它會繼承在root WebApplicationContext中定義的beans。 root WebApplicationContext應該包含所有基礎beans,以被其他contexts 和 Servlet實例分享。被繼承的beans可以被特定Servlet scope重寫,你可以定義針對給定Servlet實例(其scope)的beans。
在DispatcherServlet初始化過程中,Spring MVC會在web應用的/WEB-INF文件夾下查找名字為 [servlet-name]-servlet.xml 的文件,創建其中定義的bean,並會重寫在全局scope中已經定義的任何beans的定義。
看一下下面的DispatcherServlet配置:
<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特定的組件(beans)。當然,你可以更改該路徑,通過特定的Servlet初始化參數(詳見下面)。
對於單DispatcherServlet情景來說,也可以只有一個root context。
這可以通過設置一個空的contextConfigLocation servlet init 參數來配置,如下:
<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應用擁有一些額外的特性。
它不同於normal ApplicationContext的地方是它能夠resolve themes,它還知道關聯哪個Servlet(通過到ServletContext的連接)。
WebApplicationContext被束縛在ServletContext中,通過使用RequestContextUtils類的靜態方法,你可以隨時查找WebApplicationContext。
注意,我們可以使用基於Java的配置達到同樣的目的:
public class GolfingWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { // @Override protected Class<?>[] getRootConfigClasses() { // GolfingAppConfig defines beans that would be in root-context.xml return new Class[] { GolfingAppConfig.class }; } @Override protected Class<?>[] getServletConfigClasses() { // GolfingWebConfig defines beans that would be in golfing-servlet.xml return new Class[] { GolfingWebConfig.class }; } @Override protected String[] getServletMappings() { return new String[] { "/golfing/*" }; } }
2.1、在WebApplicationContext中的特殊的bean types
Spring DispatcherServlet使用特殊的beans來處理請求並渲染視圖。這些beans是Spring MVC的一部分。你可以選擇使用哪個 -- 只需要在WebApplicationContext中簡單的配置一些即可。
Spring MVC維護了一個默認beans列表以供使用,下一部分會講。
現在先來看看DispatcherServlet依賴的特殊的bean types:
bean type | 解釋 |
HandlerMapping | 基於criteria(不同的HandlerMapping實現有不同的criteria)將incoming requests映射到handlers以及一個pre和post-processors(handler interceptors)。最流行的實現支持注解controllers。 |
HandlerAdapter | 幫助DispatcherServlet調用映射到一個request的handler,無論handler是否實際被調用了。例如,調用一個注解Controller需要處理不同的注解。所以,HandlerAdapter的主要目的是讓DispatcherServlet遠離這些細節。 |
HandlerExceptionResolver | 將exceptions映射到views,也允許更復雜的exception處理代碼。 |
ViewResolver | 處理基於字符串的邏輯視圖的名字,將其轉成實際的View 類型。 |
LocaleResolver & LocaleContextResolver | Resolves the locale a client is using and possibly their time zone, in order to be able to offer internationalized views |
ThemeResolver | Resolves themes your web application can use, for example, to offer personalized layouts |
MultipartResolver | 解析multi-part請求,例如,支持處理來自HTML forms的文件上傳。 |
FlashMapManager | 存儲和獲取input 和 output的FlashMap -- 可用於從一個request將attributes傳遞至另一個,通常跨域一個redirect。 |
上面有提到,對每個特殊的bean,DispatcherServlet默認都維護了一個實現列表以供使用。這個信息保存在 DispatcherServlet.properties 中,在org.springframework.web.servlet包下。
所有的特殊的beans都有其合理的默認(bean還是設置?)。或早或晚,你總會需要定制這些beans提供的一個或多個properties。例如,配置InternalResourceViewResolver的prefix property 。
這里最需要理解的概念就是:一旦你在你的WebApplicationContext中配置了一個特殊的bean,如InternalResourceViewResolver,你就完全重寫了某個類型的默認的實現列表。例如,如果你配置了InternalResourceViewResolver,那默認的ViewResolver實現列表會被忽略!
當你set up了一個DispatcherServlet,然后一個由其負責的request進來了,這時該DispatcherServlet會以如下流程開始處理請求:
- 搜索WebApplicationContext,並將其綁定到該request,作為其attribute,controller和其他元素在該process中可能用到。默認鍵是
DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE
。 - 將locale resolver綁定到request,以讓process中的元素在processing the request時(如rendering the view、preparing data 等等)能夠resolve 需要使用的locale。如果你不需要locale resolving,你不需要這個。
- 將theme resolver綁定到request,讓元素(如views)決定使用哪個theme。如果你不使用themes,可以忽略它。
- 如果你指定了一個multipart file resolver,該request會被檢查有無multiparts;如果有,該請求會被封裝成MultipartHttpServletRequest以更進一步的處理。
- 查找恰當合適的handler。如果找到了,關聯到該handler的執行鏈(preprocessors、postprocessors、以及controllers)會被執行,以准備一個model或者rendering。
- 如果返回了model,view會被rendered。如果沒有返回model,(可能由於攔截等原因),沒有view會被渲染,因為request可能已經被完成了!
在WebApplicationContext中聲明的handler exception resolvers會pick up 處理request過程中拋出的exceptions。使用這些exception resolvers允許你定義自己的行為來處理exceptions。
SpringDispatcherServlet也支持返回 last-modification-date(最后修改日期),如同Servlet API中指定的一樣。決定特定request的last modification date的處理是簡單粗暴的:DispatcherServlet會查找合適的handler mapping,並測試找到的handler是否實現了LastModified接口。如果實現了,long getLastModified(request)方法的值會被返回。
你可以定制自己的DispatcherServlet實例,只要在web.xml的Servlet聲明中添加Servlet初始化參數(init-param elements)即可。下面列出了支持的參數。
DispatcherServlet 初始化參數
參數 | 解釋 |
contextClass | 實現了WebApplicationContext的類,實例化了該Servlet使用的context。默認,使用XmlWebApplicationContext。 |
contextConfigLocation | 傳遞給context實例(由contextClass指定)的字符串,用於指出在什么地方找到context config。如有多個,以逗號間隔。如果多個,且beans有重復定義,以最后一個為准。 |
namespace | WebApplicationContext的命名空間。默認是[servlet-name]-servlet。 |
Controller將用戶的input解釋並轉成一個model,該model可以通過view描述給用戶。
Spring 2.5 為MVC Controllers引入了基於注解的編程模型,使用諸如@RequestMapping、@RequestParam、@ModelAttribute之類的注解。在Servlet MVC和Portlet MVC中都可用。
在 spring-projects Org on Github 里,大量的web應用都使用了這種支持。例如MvcShowcase, MvcAjax, MvcBasic, PetClinic, PetCare, 以及其他。
@Controller // 一個注解即可Controller public class HelloWorldController { @RequestMapping("/helloWorld") // public String helloWorld(Model model) { model.addAttribute("message", "Hello World!"); return "helloWorld"; } }
3.1、使用@Controller定義一個Controller
@Controller注解用於標記一個class擁有controller的角色。
注意,這種注解Controller需要開啟自動探測才能使用。如下:
<?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>
3.2、使用@RequestMapping映射requests
使用@RequestMapping注解將URLs如/app映射到一個類或者特定的handler方法。示例:
@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"; } }
class級別上的@RequestMapping不是強制的。
@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()); } }
@RequestMapping 變體
Spring 4.3 引入了以下@RequestMapping注解的變體,方法級別的。
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
@Controller @RequestMapping("/appointments") public class AppointmentsController { private final AppointmentBook appointmentBook; @Autowired public AppointmentsController(AppointmentBook appointmentBook) { this.appointmentBook = appointmentBook; } @GetMapping public Map<String, Appointment> get() { return appointmentBook.getAppointmentsForToday(); } @GetMapping("/{day}") public Map<String, Appointment> getForDay(@PathVariable @DateTimeFormat(iso=ISO.DATE) Date day, Model model) { return appointmentBook.getAppointmentsForDay(day); } @GetMapping("/new") public AppointmentForm getNewForm() { return new AppointmentForm(); } @PostMapping public String add(@Valid AppointmentForm appointment, BindingResult result) { if (result.hasErrors()) { return "appointments/new"; } appointmentBook.addAppointment(appointment); return "redirect:/appointments"; } }
@Controller 和 AOP 代理
一些情況下,一個controller可能需要在運行時被AOP代理裝飾。一個例子是在controller上使用@Transactional。這時,對這些controllers,我們建議使用基於類的代理。這也是controller的默認選項。
然而,如果一個controller必須實現一個非Spring Context回調的接口(如InitializingBean、*Aware等等)的話,你可能需要顯式的配置基於類的代理。例如,將 <tx:annotation-driven/>
改成 <tx:annotation-driven proxy-target-class="true"/>
。
對Spring MVC 3.1中@RequestMapping methods的新的支持類(解析類?)
Spring 3.1 引入了針對RequestMapping methods的一組新的支持類,叫做:RequestMappingHandlerMapping
、RequestMappingHandlerAdapter
。推薦使用它們,甚至需要利用Spring MVC 3.1及以后的一些新特性。這些新的支持類默認就由MVC命名空間和MVC Java config啟用,其他情況必須顯式的配置。本部分會描述一下新舊支持類之間的一些重要區別。
Spring 3.1之前,type和method -level request mappings是在兩個獨立的階段中檢查的 -- controller先被DefaultAnnotationHandlerMapping
選中,具體的方法調用則由AnnotationMethodHandlerAdapter
負責。
Spring 3.1 中新的支持類,只需要使用RequestMappingHandlerMapping
。不妨將controller的那些方法看作一個集合,其內容是帶有映射的各種端點。
這使得一些新的可能成為現實。For once a HandlerInterceptor
or a HandlerExceptionResolver
can now expect the Object-based handler to be a HandlerMethod
, which allows them to examine the exact method, its parameters and associated annotations. The processing for a URL no longer needs to be split across different controllers.
下面這幾件事情已經不再有效:
- Select a controller first with a
SimpleUrlHandlerMapping
orBeanNameUrlHandlerMapping
and then narrow the method based on@RequestMapping
annotations. -- 先選擇一個controller,再根據其@RequestMapping注解窄化請求?為什么不行了???幾個意思? - 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. --不再支持基於方法名來區分沒有顯式指定映射URL的@RequestMapping方法。必須顯式的指定映射URL。 - 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. -- 不再支持默認方法處理請求!
上面的特性仍由現有的支持類支持。然而,如果想使用Spring 3.1的新特性,你需要使用新的支持類!
URI Template Patterns
URI Template是一個類似URI的字符串,包含一個或多個變量名字。當你使用值將其中的變量全部替換之后,該模板會變成一個URI。
在Spring MVC中,你可以在方法的一個參數上使用@PathVariable注解,就可以將實參綁定到URI模板變量的值。
@GetMapping("/owners/{ownerId}") public String findOwner(@PathVariable String ownerId, Model model) { Owner owner = ownerService.findOwner(ownerId); model.addAttribute("owner", owner); return "displayOwner"; }
注意,如果方法的參數名與URI模板變量的名字不符,那需要顯式的指定;否則可以省略。如下:
@GetMapping("/owners/{ownerId}") public String findOwner(@PathVariable("ownerId") String theOwner, Model model) { // implementation omitted }
一個方法可以擁有任意數量的@PathVariable注解:
@GetMapping("/owners/{ownerId}/pets/{petId}") 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"; }
當在Map<String, String>實參上使用@PathVariable時,map會被所有的URI模板變量填滿。
URI模板可以從type和method級別的@RequestMapping注解中組裝。
@Controller @RequestMapping("/owners/{ownerId}") // 這里,類中的方法可以使用 public class RelativePathUriTemplateController { @RequestMapping("/pets/{petId}") public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) { // implementation omitted } }
@PathVariable實參可以是任何簡單類型,如int、long、Date等。Spring會自動轉換到合適的類型,如果失敗會拋出TypeMishmatchException。-- 也可以注冊其他數據類型: 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”.
帶正則表達式的URI Template Patterns
有時候你需要更精確的定義URI模板變量。考慮下 URL "/spring-web/spring-web-3.0.5.jar"
,你怎么將其拆分成多個部分?
@RequestMapping支持在URI模板變量中使用正則表達式。語法是: {varName:regex}
。如下:
@RequestMapping("/spring-web/{symbolicName:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{extension:\\.[a-z]+}") public void handle(@PathVariable String version, @PathVariable String extension) { // ... }
Path Patterns
除了URI模板,@RequestMapping注解和其所有變體還支持ant-style的path patterns,如 /mypath/*.do。
Path Pattern Comparison
當一個URL匹配多個patterns時,會使用一種排序來查找最佳匹配。
帶有更少URI變量和通配符的pattern ,被認為更匹配。例如,/hotels/{hotel}/*
比 /hotels/{hotel}/**
更合適,因為其他一樣,通配符更少。
如果變量數量一樣,更長的被認為更匹配。例如,/foo/bar*
比 /foo/*
更匹配。
當兩個patterns擁有相同數量的變量和長度時,通配符少的更匹配。例如,/hotels/{hotel}
比 /hotels/*
更匹配。
另外,還有兩個特殊規則:
- /** 匹配度最差。
- 帶前綴的pattern,比其他所有不含雙通配符的pattern,更差。例如:/public/** 比 /public/path/{a} 更差。
帶有占位符的path patterns
@RequestMapping注解的patterns還支持 ${...} 占位符。
后綴pattern匹配
默認,Spring MVC會執行 “.*”的匹配,所以,當一個controller的被映射到/person的時候,實際上是隱式的被映射到/person.*。這樣可以使用URL輕松的請求不同的資源表現,如/person.pdf, /person.xml。
后綴pattern匹配可以被關閉,或者被限制在一組為了內容協商目的而注冊的路徑擴展名中。非常建議使用最普通的請求映射來最小化請求的模糊性,因為有時候“.”不一定代表擴展名,例如/person/{id},有可能是/person/joe@xx.com。
后綴pattern匹配和RFD
Reflected file download (RFD) 攻擊,最早由Trustwave在2014年指出。這種攻擊類似XSS,都依賴可以反射到響應的輸入。然而,不是將js插入到HTML中,RFD攻擊依賴於瀏覽器的下載,並依賴於瀏覽器將響應視為一個可執行腳本(雙擊能運行的那種)。
在Spring MVC中,@ResponseBody 和 @ResponseEntity方法同樣具備風險,因為它們可以渲染不同內容類型--客戶端可能通過URL路徑擴展名來請求的類型。需要注意,無論是單獨的禁用后綴pattern匹配還是單獨的禁用用於內容協商目的的路徑擴展名,都不能有效的組織RFD攻擊。
為了有效的防護RFD,Spring在渲染響應體之前添加了一個header(Content-Disposition:inline;filename=f.txt
)以建議一個固定和安全的下載文件名。這只有在URL路徑包含的擴展名既不在白名單中,也不是因內容協商目的而顯式注冊的文件擴展名時才有效。然而,這樣做也有其副作用,有時候可能會直接顯示在瀏覽器中!
默認,很多常用的路徑擴展名已經在白名單中。此外,REST API的調用通常不是用作在瀏覽器中使用的URL。盡管如此,使用自定義HttpMessageConverter實現的應用,可以顯式的注冊用於內容協商目的的文件擴展名,針對這些擴展名Content-Disposition header(Content-Disposition:inline;filename=f.txt
)不會被添加。
This was originally introduced as part of work for CVE-2015-5211. Below are additional recommendations from the report:
- Encode rather than escape JSON responses. This is also an OWASP XSS recommendation. For an example of how to do that with Spring see spring-jackson-owasp.
- Configure suffix pattern matching to be turned off or restricted to explicitly registered suffixes only.
- Configure content negotiation with the properties "useJaf" and "ignoreUnknownPathExtensions" set to false which would result in a 406 response for URLs with unknown extensions. Note however that this may not be an option if URLs are naturally expected to have a dot towards the end.
- Add
X-Content-Type-Options: nosniff
header to responses. Spring Security 4 does this by default.
Matrix Variables
URI specification RFC 3986定義了在path segments內包含name-value對的可行性。在Spring MVC中,它們被視為matrix Variables。
matrix variables可能出現在任意path segment中,每個matrix variable都由分號隔離。例如:"/cars;color=red;year=2012"
。多個value的時候,可以使用逗號拼接,如"color=red,green,blue"
,也可以重復name,如"color=red;color=green;color=blue"
。
如果希望一個URL包含matrix variables,請求映射pattern必須使用URI模板來代表它們。
下面是提取matrix variable ”q”的例子:
// GET /pets/42;q=11;r=22 @GetMapping("/pets/{petId}") public void findPet(@PathVariable String petId, @MatrixVariable int q) { // petId == 42 // q == 11 }
因為所有的path segments都可能含有matrix variables,某些情況下你需要更精確的信息來確定需要的變量:
// GET /owners/42;q=11/pets/21;q=22 @GetMapping("/owners/{ownerId}/pets/{petId}") public void findPet( @MatrixVariable(name="q", pathVar="ownerId") int q1, @MatrixVariable(name="q", pathVar="petId") int q2) { // q1 == 11 // q2 == 22 }
一個matrix variable可以被定義為可選項,可以擁有一個指定的默認值;
// GET /pets/42 @GetMapping("/pets/{petId}") public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) { // q == 1 }
所有的matrix variables可以用一個Map來獲取:
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23 @GetMapping("/owners/{ownerId}/pets/{petId}") public void findPet( @MatrixVariable MultiValueMap<String, String> matrixVars, @MatrixVariable(pathVar="petId"") MultiValueMap<String, String> petMatrixVars) { // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23] // petMatrixVars: ["q" : 11, "s" : 23] }
注意:為了啟用matrix variables,你必須設置RequestMappingHandlerMapping的removeSemicolonContent property為false。其默認是true。
The MVC Java config and the MVC namespace both provide options for enabling the use of matrix variables.
If you are using Java config, The Advanced Customizations with MVC Java Config section describes how the
RequestMappingHandlerMapping
can be customized.In the MVC namespace, the
<mvc:annotation-driven>
element has anenable-matrix-variables
attribute that should be set totrue
. By default it is set tofalse
.<?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>
Consumable Media Types
通過指定一個consumable media types列表來窄化映射。只有request header中的Content-Type符合指定的媒體類型時,請求才匹配。例如:
@PostMapping(path = "/pets", consumes = "application/json") public void addPet(@RequestBody Pet pet, Model model) { // implementation omitted }
注意,consumable media type表達式可以使用“!”來否定匹配的媒體類型,如使用“!text/plain”來匹配除了text/plain之外的Content-Type。建議使用MediaType中的常量,如 APPLICATION_JSON_VALUE、
APPLICATION_JSON_UTF8_VALUE
。
注意,雖然consumes條件支持type和method級別,但是,不同於其他條件,method級別的會覆蓋type級別的類型!!!
Producible Media Types
還可以通過指定一個producible media types列表來窄化請求。僅當request header的Accept匹配時,該request才會匹配。此外,使用produces條件會確保實際的內容類型。如下:
@GetMapping(path = "/pets/{petId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @ResponseBody public Pet getPet(@PathVariable String petId, Model model) { // implementation omitted }
注意produces條件指定的媒體類型,也可以選擇性的指定一個字符集。例如,在上面的代碼片段中,我們指定了與MappingJackson2HttpMessageConverter
中默認配置的媒體類型一致的媒體類型。--是否可以認為,一種字符集就是一種媒體類型?
同consumes類似,produces也可以使用“!”。同樣建議使用MediaType中的常量。
同consumes類似,方法級別的produces會覆蓋類級別的媒體類型!!!
請求參數和請求頭的值 Request Parameter 、Request Header values
可以通過請求參數條件來窄化請求的匹配,如:"myParam"
, "!myParam"
, or "myParam=myValue"
。前兩個用於測試請求參數中是否出現該參數,第三個則需要請求參數有一個特定的值。例子:
@Controller @RequestMapping("/owners/{ownerId}") public class RelativePathUriTemplateController { @GetMapping(path = "/pets/{petId}", params = "myParam=myValue") public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) { // implementation omitted } }
同樣的情況還適合請求頭:
@Controller @RequestMapping("/owners/{ownerId}") public class RelativePathUriTemplateController { @GetMapping(path = "/pets", headers = "myHeader=myValue") public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) { // implementation omitted } }
雖然你可以使用通配符來匹配Content-Type和Accept header values(如headers="content-type=text/*",可以匹配"text/plain" 和"text/html"),但建議使用consumes和produces。這也是它們的設計目的。
HTTP HEAD 和 HTTP OPTIONS
@RequestMapping方法映射到“GET”,同時也會隱式的映射到“HEAD”!
@RequestMapping方法內建支持HTTP OPTIONS。略。
3.3、定義@RequestMapping handler methods
@RequestMapping handler methods可以有非常靈活的簽名。除了BindingResult參數之外的參數可以按任意順序排放。
Spring 3.1 為 @RequestMapping methods引入了一組新的支持類,分別是:
RequestMappingHandlerMapping
andRequestMappingHandlerAdapter
。建議使用它們,而且,應該使用Spring 3.1及以后的新特性。這些新的支持類默認由MVC命名空間啟用,如果是Java config,必須顯式的配置--否則無法使用。
支持的方法參數類型
- 請求/響應對象(Servlet API)。如ServletRequest 或 HttpServletRequest。
- 會話對象(Servlet API),HttpSession類型的。該類型的參數會強制相應的session出現。就是說,其實參永遠非null。
session的訪問可能不是線程安全的,特別是在一個Servlet環境中。如果需要多個請求並發訪問一個session時,可以考慮將RequestMappingHandlerAdapter的synchronizeOnSession設置為true。
org.springframework.web.context.request.WebRequest
或者org.springframework.web.context.request.NativeWebRequest
。允許泛型的請求參數訪問,以及請求/會話屬性訪問,不需要綁定到native的Servlet/Portlet API。- java.util.Locale,用於獲取當前請求的locale,由已啟用的最符合的locale resolver來決定--實際上是在MVC環境中配置的LocaleResolver/LocacleContextResolver。
- java.util.TimeZone (Java 6+) / java.time.ZoneId (Java 8),用於獲取當前請求的時區,由LocaleContextResolver決定。
- java.io.InputStream / java.io.Reader,用於獲取請求的內容。其值是由Servlet API暴露的原生InputStream/Reader。
- java.io.OutputStream / java.io.Writer,用於生成響應的內容。其值是由Servlet API暴露的原生的OutputStream/Writer。
- org.springframework.http.HttpMethod,用於獲取HTTP request method。
- java.security.Principle,包含了當前已認證的用戶。
- @PathVariable注解的參數,用於獲取URI模板變量。
- @MatrixVariable注解的參數,用於獲取URI path segments中的name-value對。
- @RequestParam注解的參數,用於獲取特定的Servlet 請求參數。參數值會被轉換成方法參數類型 -- 類型轉換。
- @RequestHeader注解的參數,用於獲取特定的Servlet請求HTTP headers。類型轉換。
- @RequestBody注解的參數,用於獲取HTTP請求體。參數值會使用HttpMessageConverters來進行類型轉換。
- @RequestPart注解的參數,用於獲取一個multipart/form-data請求的內容。
- @SessionAttribute注解的參數,用於獲取已有的、永久的session attributes (例如用戶認證對象)。與其對比,通過@SessionAttributes會獲取臨時存儲在session的model attributes。
- @RequestAttribute注解的參數,用於獲取request attributes。
- HttpEntity<?> 參數,用於獲取Servlet request HTTP headers和contents。request stream會被轉成entity body--使用HttpMessageConverters。
java.util.Map
/org.springframework.ui.Model
/org.springframework.ui.ModelMap
,略。org.springframework.web.servlet.mvc.support.RedirectAttributes
,用於指定在重定向中使用的一組具體的attributes,也可以用於添加flash attributes(attributes暫存在服務器側,以在重定向后可用。)。- command或者form objects,用於綁定請求參數到bean properties,或者直接綁定到字段,通過定制的類型轉換--依賴於@InitBinder方法 和/或 HandlerAdapter配置。見RequestMappingHandlerAdapter的webBindingInitializer property。這些command objects和它們的校驗結果默認會被暴露成model attributes,使用command class name -- 例如,用model attribute “orderAddress”代表一個“some.package.OrderAddress”類型的command object。@ModelAttribute注解,可以用在方法參數上,以定制model attribute name。
org.springframework.validation.Errors
/org.springframework.validation.BindingResult
校驗結果,用於其前面的第一個command 或者 form object。org.springframework.web.bind.support.SessionStatus
,狀態處理,用於標記form處理已完成,該標記會導致session attributes被清理 -- 那些由@SessionAttributes注解在type上指定的session attributes!org.springframework.web.util.UriComponentsBuilder
,一個builder,用於預處理一個URL,關聯到當前請求的host、port、scheme、context path、以及servlet mapping的字面值部分。
Errors和BindingResult參數,必須跟在model object后面,因為可能有多個model object,Spring會為每個model object創建一個單獨的BindingResult,所以,下面的例子不會執行:
@PostMapping // Invalid ordering of BindingResult and @ModelAttribute. public String processSubmit(@ModelAttribute("pet") Pet pet, Model model, BindingResult result) { ... }
注意,在Pet和BindingResult之間有一個Model參數。必須如下排序才能工作:
@PostMapping public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result, Model model) { ... }
支持JDK 1.8 的 java.util.Optional 作為方法參數類型,與帶有required attribute的注解一起使用(如@RequestParam、@RequestHeader等等)。這些情況下,java.util.Optional 相當於 required = false 。
支持的方法返回類型
- ModelAndView對象。with the model implicitly enriched with command objects and the results of
@ModelAttribute
annotated reference data accessor methods. - Model對象。with the view name implicitly determined through a
RequestToViewNameTranslator
and the model implicitly enriched with command objects and the results of@ModelAttribute
annotated reference data accessor methods. - Map對象。 for exposing a model, with the view name implicitly determined through a
RequestToViewNameTranslator
and the model implicitly enriched with command objects and the results of@ModelAttribute
annotated reference data accessor methods. - View對象。 with the model implicitly determined through command objects and
@ModelAttribute
annotated reference data accessor methods. The handler method may also programmatically enrich the model by declaring aModel
argument (see above). - 一個String值,能被解釋為logical view name。with the model implicitly determined through command objects and
@ModelAttribute
annotated reference data accessor methods. The handler method may also programmatically enrich the model by declaring aModel
argument (see above). - void,如果方法自己處理響應 -- 方法形參里聲明ServletResponse / HttpServletResponse。or if the view name is supposed to be implicitly determined through a
RequestToViewNameTranslator
(not declaring a response argument in the handler method signature). - 如果方法帶有@ResponseBody,返回類型會被寫成response HTTP body。返回值會被轉成聲明的方法參數類型--使用HttpMessageConverters。
- HttpEntity<?>或ResponseEntity<?>對象,用於訪問Servlet response HTTP headers 和 contents。該entity body會被轉成response stream -- 通過HttpMessageConverters。
- HttpHeaders對象,用於返回一個無body的response。
- 當應用想在一個由Spring MVC管理的線程中異步地produce返回值時,可以返回Callable<?>。
- 當應用想從它自己選擇的線程中produce返回值時,可以返回DeferredResult<?>。
- 當應用想從它自己選擇的線程中produce返回值時,可以返回ListenableFuture<?>。
- 返回一個ResponseBodyEmittter,以異步的將多個對象寫入到response。也可以作為ResponseEntity內的body。
- 返回一個SseEmitter,以異步的將Server-Sent Events寫入到response。也可以作為ResponseEntity內的body。
- 返回一個StreamingResponseBody,以異步的寫入到response OutputStream。也可以作為ResponseEntity內的body。
- 任意其他返回類型,都被認為是一個單獨的model attribute,暴露給view,--使用通過方法級別的@ModelAttribute指定的attribute name(或者,默認的attribute name,基於返回類型的類名字)。The model is implicitly enriched with command objects and the results of
@ModelAttribute
annotated reference data accessor methods.
使用@RequestParameter將請求參數綁定到方法參數
@Controller @RequestMapping("/pets") @SessionAttributes("pet") public class EditPetForm { // ... @GetMapping public String setupForm(@RequestParam("petId") int petId, ModelMap model) { Pet pet = this.clinic.loadPet(petId); model.addAttribute("pet", pet); return "petForm"; } // ... }
默認,@RequestParam的required attribute是true,可以設置為false。@RequestParam(name="id", required=false)
如果目標方法參數的類型不是String,會自動使用類型轉換。
當在Map<String, String> 或 MultiValueMap<String, String>實參上使用@RequestParam注解時,會被填入所有的request parameters。
使用RequestBody注解來映射request body
方法參數的@RequestBody注解,標識了方法參數應該綁成HTTP request body的值。例如:
@PutMapping("/something") public void handle(@RequestBody String body, Writer writer) throws IOException { writer.write(body); }
可以通過使用一個HttpMessageConverter將request body轉成method argument。HttpMessageConverter負責將HTTP request msg轉成一個對象以及將對象轉成HTTP response body。RequestMappingHandlerAdapter支持@RequestBody的默認HttpMessageConverters:
ByteArrayHttpMessageConverter
converts byte arrays.StringHttpMessageConverter
converts strings.FormHttpMessageConverter
converts form data to/from a MultiValueMap<String, String>.SourceHttpMessageConverter
converts to/from a javax.xml.transform.Source.
注意,如果使用MVC 命名空間或者使用MVC Java config,默認會注冊更多的message converters。
如果你打算讀寫XML,你會需要配置一個MarshallingHttpMessageConverter -- 使用org.springframework.oxm包中的特定的Marshaller和Unmarshaller實現。下面的例子演示了如何直接讀寫XML -- 但是,如果你的應用是通過MVC命名空間或MVC Java config來配置的,見 Section 22.16.1, “Enabling the MVC Java Config or the MVC XML Namespace” 。
<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注解,Spring會使用配置好的Validator實例來校驗該參數。當使用MVC命名空間或MVC Java config時,一個JSR-303 validator會被自定的配置 -- 假如classpath中有一個JSR-303實現。
就像使用@ModelAttribute注解的參數已有,Errors參數可以用來檢查errors。如果沒有聲明這樣一個參數,會拋出一個MethodArgumentNotValidException
。該異常由DefaultHandlerExceptionResolver
來處理,會返回400 error。
使用@ResponseBody注解來映射response body
@ResponseBody注解類似於@RequestBody。該注解放在方法上指示返回類型會被寫入HTTP response body (沒有被放入Model,或被解釋成view name)。 例如:
@GetMapping("/something") @ResponseBody public String helloWorld() { return "Hello World"; }
上面的例子,會將字符串寫入HTTP response stream。
如同@RequestBody,Spring會將返回的對象轉換成一個response body -- 使用一個HttpMessageConverter。
使用@RestController注解創建一個REST Controller
使用@RestController代替@ResponseBody與@Controller。它雖然是由后兩者組合而成,但在將來會被賦予更多語義。
@RestController也可以配合@ControllerAdvice或@RestControllerAdvice beans。詳見 the the section called “Advising controllers with @ControllerAdvice and @RestControllerAdvice” section。
使用HttpEntity
HttpEntity 類似於 @RequestBody和@ResponseBody。除了能獲取request和response body之外,HttpEntity(以及其response子類:ResponseEntity)還允許獲取request和response headers,如下:
@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); }
The above example gets the value of the MyRequestHeader
request header, and reads the body as a byte array. It adds the MyResponseHeader
to the response, writes Hello World
to the response stream, and sets the response status code to 201 (Created).
As with @RequestBody
and @ResponseBody
, Spring uses HttpMessageConverter
to convert from and to the request and response streams. For more information on these converters, see the previous section and Message Converters.
在方法上使用@ModelAttribute
該注解可以用在方法或方法參數上。本部分講解用在方法上的作用,下一部分會講解用在方法參數上的作用。
在方法上使用該注解,意味着該方法的一個目的是增加一個或多個model attribute。該方法支持的參數類型與@RequestMapping methods一樣,但不能直接映射到請求。相反,同一個Controller中的@ModelAttribute methods會在@RequestMapping methods之前被調用!!!例子:
// 添加一個 attribute // 該方法的返回值會被添加到model account中 // 你可以定義該model的名字,例如 @ModelAttribute("myAccount") @ModelAttribute public Account addAccount(@RequestParam String number) { return accountManager.findAccount(number); } // 添加多個 attributes @ModelAttribute public void populateModel(@RequestParam String number, Model model) { model.addAttribute(accountManager.findAccount(number)); // add more ... }
@ModelAttribute methods被用於將常用的attributes填入model。
注意兩種形式的@ModelAttribute methods。第一個,是隱式的將返回值添加為attribute。第二個,接收一個Model,然后在其中增加任意數量的model attributes。
一個Controller可以擁有任意數量的@ModelAttribute methods。所有這些方法都會在同一個Controller中的@RequestMapping methods之前被調用!
@ModelAttribute methods 也可以被定義在@ControllerAdvice class內,這樣的methods會被用於所有Controllers。--就是在所有Controller的所有@RequestMapping methods之前被調用!
如果沒有顯式指定一個model attribute name,會發生什么?這種情況下,會基於其類型賦予一個默認的名字。例如,如果方法返回了Account類型,那默認的name就是account。
@ModelAttribute注解也可以用在@RequestMapping methods上。這種情況下,方法的返回值被解釋成一個model attribute,而非view name。view name會基於name慣例而獲取到,更像是返回了void。 see Section 22.13.3, “The View - RequestToViewNameTranslator”。
在方法參數上使用@ModelAttribute
當@ModelAttribute用於方法參數上時,代表該參數應該從該model中獲取。如果model中沒有,該參數會先被實例化,再被添加到model。一旦出現在model中,該參數的字段會被匹配名字的request parameters填充。這就是Spring MVC中的數據綁定(data binding),一個非常有用的機制,節省了你手動解析每個form字段的時間。
@PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@ModelAttribute Pet pet) { }
上面的例子,Pet實例從哪里來?有幾個選項:
- 可能已經存在於@SessionAttributes的model中。
- 可能已經存在於同一個Controller的@ModelAttribute method的model中。
- 可能基於URI模板變量和類型轉換器而獲取(稍后詳解)。
- 可能使用其默認構造器實例化。
@ModelAttribute method是從數據庫中獲取attribute的一種常用方式,可能可選的存儲於requests之間--通過使用@SessionAttributes。某些情況下,使用URI模板變量和類型轉換器更為方便。例子:
@PutMapping("/accounts/{account}") public String save(@ModelAttribute("account") Account account) { // ... }
上面的例子,model attribute的name與URI模板變量的名字一致。如果你注冊了一個Converter<String, Account>,那么上面的例子就可以不必使用一個@ModelAttribute method。
下一步就是數據綁定。WebDataBinder類會匹配request parameter names -- 包含query string parameters 和 form fields -- 到model attribute fields,根據名字。匹配的字段會被填充--當必要的類型轉換被應用了之后。Data binding and validation are covered in Chapter 9, Validation, Data Binding, and Type Conversion. Customizing the data binding process for a controller level is covered in the section called “Customizing WebDataBinder initialization”.
數據綁定的一個結果是,可能存在errors,例如缺失必須的字段或者類型轉換錯誤。為了檢查該類錯誤,需要在@ModelAttribute argument之后緊跟着添加一個BindingResult argument。
@PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { if (result.hasErrors()) { return "petForm"; } // ... }
使用BindingResult,你可以檢查是否有errors,可以使用Spring的<errors> form tag來在同一個form中顯示錯誤。
注意,某些情況下,不使用數據綁定而獲取model中的一個attribute很有用。這些情況下,你可以在Controller中注入Model,或者在注解上使用binding flag,如下:
@ModelAttribute public AccountForm setUpForm() { return new AccountForm(); } @ModelAttribute public Account findAccount(@PathVariable String accountId) { return accountRepository.findOne(accountId); } @PostMapping("update") public String update(@Valid AccountUpdateForm form, BindingResult result, @ModelAttribute(binding=false) Account account) { // ... }
In addition to data binding you can also invoke validation using your own custom validator passing the same BindingResult
that was used to record data binding errors. That allows for data binding and validation errors to be accumulated in one place and subsequently reported back to the user:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { new PetValidator().validate(pet, result); if (result.hasErrors()) { return "petForm"; } // ... }
-- 就是根據BindingResult的結果進行自己的操作。
或者,可以使用JSR-303 @Valid注解來自動調用校驗:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { if (result.hasErrors()) { return "petForm"; } // ... }
使用@SessionAttributes在requests之間的HTTP session中存儲model attributes
type-level @SessionAttributes注解,聲明了用於特定handler的session attributes。這會列出model attributes的names或types -- 應該透明的存儲於session或某conversational storage,在后續的requests中作為form-backing beans。
@Controller @RequestMapping("/editPet.do") @SessionAttributes("pet") public class EditPetForm { // ... }
使用@SessionAttribute訪問預存的全局session attributes
如果需要訪問pre-existing global session attributes,就是在controller外部(例如在filter中)管理的 ,且可能或可能不會出現在method parameter上使用@SessionAttribute注解(--什么鬼)(If you need access to pre-existing session attributes that are managed globally, i.e. outside the controller (e.g. by a filter), and may or may not be present use the @SessionAttribute
annotation on a method parameter)。
@RequestMapping("/") public String handle(@SessionAttribute User user) { // ... }
當需要增加或移除session attributes時,可以考慮在controller method上注入 org.springframework.web.context.request.WebRequest 或 javax.servlet.http.HttpSession。
為了在session中臨時存儲model attributes以作為controller workflow的一部分,可以考慮使用SessionAttributes as described in the section called “Using @SessionAttributes to store model attributes in the HTTP session between requests”.
使用@RequestAttribute來獲取request attributes
類似於@SessionAttribute,@RequestAttribute也可以用於獲取pre-existing request attributes -- 由filter或interceptor創建的。
@RequestMapping("/") public String handle(@RequestAttribute Client client) { // ... }
處理application/x-www-form-urlencoded data
前面的部分描述了使用@ModelAttribute來支持來自瀏覽器客戶端的form submission requests。@ModelAttribute注解還被推薦用於處理來自非瀏覽器客戶端的請求。然而,當處理HTTP PUT requests時,有一個顯著的不同。瀏覽器會通過HTTP GET或HTTP POST提交表單數據。非瀏覽器客戶端還可以通過HTTP PUT來提交。這就有一個問題,因為Servlet specification要求 ServletRequest.getParameter*()
方法僅支持HTTP POST的字段獲取,而非HTTP PUT。
為了支持HTTP PUT和PATCH 請求,spring-web模塊提供了過濾器:HttpPutFormContentFilter。
<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>
上面的filter,會攔截content type為 application/x-www-form-urlencoded 的 HTTP PUT和PATCH請求,從其請求體中讀取表單數據,封裝ServletRequest以讓ServletRequest.getParameter*() 能夠使用表單數據。
由於HttpPutFormContentFilter會consume請求體,所以,不應為那些依賴針對 application/x-www-form-urlencoded 的轉換器的PUT或PATCH URLs配置該filter。這包括@RequestBody MultiValueMap<String, String> 和 HttpEntity<MultiValueMap<String, String>>。
使用@CookieValue注解來映射cookie values
該注解允許一個方法參數綁定一個HTTP cookie的值。
假定從http request接收了如下cookie:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
下面的代碼演示了如何獲取JSESSIONID cookie:
@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@CookieValue("JSESSIONID") String cookie) {
//...
}
如果目標方法參數的類型不是String,會自動應用類型轉換。
該注解,也被在Servlet和Portlet環境下的annotated handler methods支持。
使用@RequestHeader注解來映射request header attributes
這是一個樣例request header:
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 headers的值:
@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 argument時,該map會被填入所有的header values。
內建的支持允許轉換一個逗號間隔的字符串,將其轉成一個字符串或其他類型的數組/集合。例如,@RequestHeader(“Accept”)注解的方法參數,可能是String類型,也可能是String[]或List<String>類型。
該注解,也被在Servlet和Portlet環境下的annotated handler methods支持。
method parameters 和 type conversion
從request中提取出來的基於字符串的值,包括request parameters、path variables、request headers、還有cookie values,可能需要被轉成它們要綁定的method parameter或field的類型。如果目標類型不是String,Spring會自動轉成合適的類型。支持所有簡單類型,如int、long、Date等等。甚至,你可以使用一個WebDataBinder來定制轉換過程,或者通過在FormattingConversionService中注冊Formatters。
定制WebDataBinder 初始化
通過Spring的WebDataBinder使用PropertyEditors來定制request parameter 的綁定,你可以在你的controller中使用@InitBinder methods,或者在@ControllerAdvice class中使用@InitBinder methods,或者提供一個定制的WebBindingInitializer。 See the the section called “Advising controllers with @ControllerAdvice and @RestControllerAdvice” section for more details.
使用@InitBinder 定制數據綁定
在controller方法上使用@InitBinder,允許你在controller內部配置web數據綁定。@InitBinder代表方法初始化了WebDataBinder--會被用於填充被注解的handler方法的command 和 form object arguments。
這些init-binder methods,支持@RequestMapping所支持的所有參數,除了command/form objects和相應的校驗結果對象。init-binder methods必須沒有返回值。所以,大多被聲明為void。 典型的arguments包括WebDataBinder 結合 WebRequest或 java.util.Locale,允許代碼注冊特定context的editors。
下面的案例演示了使用@InitBinder來配置針對所有java.util.Date form properties的一個CustomDateEditor。
@Controller public class MyFormController { @InitBinder protected 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實例。
如果你在一個shared FormattingConversionService中有基於Formatter的設置,這會非常有用,只需要同樣的做法來復用特定controller針對binding rules的調節。
@Controller public class MyFormController { @InitBinder protected void initBinder(WebDataBinder binder) { binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); } // ... }
配置一個定制的WebBindingInitializer
為了將數據綁定初始化外部化,你可以提供一個定制的WebBindingInitializer實現,然后通過提供一個定制的AnnotationMethodHandlerAdapter的bean configuration來啟用它。
下面的例子示意了使用了org.springframework.samples.petclinic.web.ClinicBindingInitializer
的配置,該配置配置了幾個PetClinic controllers需要的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 methods也可以定義在@ControllerAdvice class內,會用於所有的controllers。效果同WebBindingInitializer。See the the section called “Advising controllers with @ControllerAdvice and @RestControllerAdvice” section for more details.
advising controllers with @ControllerAdvice and @RestControllerAdvice
@ControllerAdvice注解,是一個component annotation,允許實現類能夠在classpath掃描中被自動探測到。當使用MVC namespace或MVC Java config時,自動啟用。
@ControllerAdvice class,可以含有@ExceptionHandler、@InitBinder以及@ModelAttribute methods,這些methods,會被應用到所有controller中的@RequestMapping methods,而非僅僅其所聲明的controller中。
@RestControllerAdvice,等同於@ExceptionHandler + @ResponseBody methods。
@ControllerAdvice和@RestControllerAdvice 都可以指定作用的controllers:
// Target all Controllers annotated with @RestController @ControllerAdvice(annotations = RestController.class) public class AnnotationAdvice {} // Target all Controllers within specific packages @ControllerAdvice("org.example.controllers") public class BasePackageAdvice {} // Target all Controllers assignable to specific classes @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) public class AssignableTypesAdvice {}
詳見@ControllerAdvice文檔。
Jackson Serialization View Support
It can sometimes be useful to filter contextually the object that will be serialized to the HTTP response body. 為了提供這種能力,Spring MVC提供了內建的支持,可以rendering with Jackson’s Serialization Views.
在一個@Response controller method上,或者在那些返回ResponseEntity的controller methods上,簡單的添加@JsonView注解,並指定需要使用的view class或Interface即可。如下:
@RestController public class UserController { @GetMapping("/user") @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; } }
注意,盡管@JsonView允許指定多個class,但在controller method上使用時只能指定一個class!可以考慮使用復合接口,如果你需要啟用多個views。
對於那些依賴view resolution的controllers,簡單的將序列化view class添加到model即可:
@Controller public class UserController extends AbstractController { @GetMapping("/user") public String getUser(Model model) { model.addAttribute("user", new User("eric", "7!jd#h23")); model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class); return "userView"; } }
Jackson JSONP 支持
為了啟用JSONP對@ResponseBody和@ResponseEntity methods的支持,聲明一個@ControllerAdvice bean -- 需要繼承AbstractJsonpResponseBodyAdvice,並在其構造中指出JSONP的query parameter name(s)。如下:
@ControllerAdvice public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice { public JsonpAdvice() { super("callback"); } }
對於依賴view resolution的controllers,JSONP自動被啟用,默認的query parameter name是 jsonp 或 callback。 可以通過其jsonpParameterNames property來定制。
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-async
Spring 3.2 引入了基於 Servlet 3 的異步請求處理。不是一直以來的讓controller method返回一個值,而是,讓controller method返回一個java.util.concurrent.Callable,然后從Spring MVC管理的線程中produce 返回值。同時,main Servlet container thread 會被退出和釋放,以處理其他請求。Spring MVC在一個獨立的線程調用Callable -- 通過TaskExecutor,當Callable返回時,請求會被分派回Servlet container,從而恢復處理。例子:
@PostMapping public Callable<String> processUpload(final MultipartFile file) { return new Callable<String>() { public String call() throws Exception { // ... return "someView"; } }; }
另一個選擇是,controller method返回DeferredResult。這種情況下,也可能是任意線程produce的返回值,就是說,非Spring MVC管理的線程!例如,結果可能是響應某個外部事件,如一個JMS message、一個scheduled task等等,而produce的結果。下面是一個例子:
@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 異步請求處理特性的相關知識,會很難理解這點。這里是關於底層機制的一些基本的事實:
- 一個ServletRequest可以被放入asynchronous mode,使用request.startAsync()即可。這樣做的主要效果就是,該Servlet以及所有Filters,能夠退出,但response仍然保持open-- 允許在后面完成處理。
- request.startAsync() 會返回 AsyncContext,可以被用於對async處理的更進一步的控制。例如,它提供了dispatch方法,類似於Servlet API的forward -- 除了它允許應用恢復在一個Servlet container thread中的請求處理。
- ServletRequest提供了對當前DispatcherType的訪問,該DispatcherType can be used to distinguish between processing the initial request, an async dispatch, a forward, and other dispatcher types.
With the above in mind, the following is the sequence of events for async request processing with a Callable
:
- Controller returns a
Callable
. - Spring MVC starts asynchronous processing and submits the
Callable
to aTaskExecutor
for processing in a separate thread. - The
DispatcherServlet
and all Filter’s exit the Servlet container thread but the response remains open. - The
Callable
produces a result and Spring MVC dispatches the request back to the Servlet container to resume processing. - The
DispatcherServlet
is invoked again and processing resumes with the asynchronously produced result from theCallable
.
The sequence for DeferredResult
is very similar except it’s up to the application to produce the asynchronous result from any thread:
- Controller returns a
DeferredResult
and saves it in some in-memory queue or list where it can be accessed. - Spring MVC starts async processing.
- The
DispatcherServlet
and all configured Filter’s exit the request processing thread but the response remains open. - The application sets the
DeferredResult
from some thread and Spring MVC dispatches the request back to the Servlet container. - The
DispatcherServlet
is invoked again and processing resumes with the asynchronously produced result.
For further background on the motivation for async request processing and when or why to use it please read this blog post series.
async requests 的 Exception處理
如果,一個由controller method返回的Callable在執行時 拋出了一個Exception,會發生什么?簡短的答案是與一個controller method拋出一個異常時相同。會經歷常規的異常處理機制。 長的解釋是,當Callable拋出一個Exception時,Spring MVC會將該Exception分派到Servlet container,將其作為結果以及恢復request processing的引導,此時request processing會處理Exception,而非controller method return value。當使用DeferredResult時,你還可以選擇是否調用setResult或者setErrorResult -- 傳入Exception實例。
攔截async requests
一個HandlerInterceptor也可以實現AsyncHandlerInterceptor,以實現afterConcurrentHandlingStarted callback,當asynchronous processing開始時,會調用afterConcurrentHandlingStarted ,而非postHandle和afterComplete。
一個HandlerInterceptor也可以注冊一個CallableProcessingInterceptor 或 一個 DeferredResultProcessingInterceptor,以更深度地集成asynchronous request的lifecycle,例如,handle 一個 timeout event。詳見 AsyncHandlerInterceptor javadoc。
DeferredResult 類型,也提供了諸如 onTimeout(Runnable)、onCompletion(Runnable)之類的方法。詳見javadoc。
當使用一個Callable時,你可以將其wrap進一個WebAsyncTask的實例,該實例也可以提供timeout和completion的方法注冊。
HTTP Streaming
一個controller method可以使用DeferredResult和Callable來異步的produce其返回值,可被用於實現諸如long polling之類的技術 -- 這樣,服務器可以將一個事件盡快的push到客戶端。
如果你想要在一個HTTP response上push多個事件會怎樣?這就是與”Long Polling” 有關的技術,也就是HTTP Streaming。 Spring MVC通過ResponseBodyEmitter 返回值類型使其成為可能,該返回值類型可悲用於發送多個對象(而非使用@ResponseBody只發送一個 -- 這種更常見),每個被發送的對象都通過一個HttpMessageConverter被寫入到response 。
例子:
@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的body,以便定制response的status 和 headers。
HTTP Streaming With Server-Sent Events
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-async-sse
HTTP Streaming Directly To The OutputStream
@RequestMapping("/download") public StreamingResponseBody handle() { return new StreamingResponseBody() { @Override public void writeTo(OutputStream outputStream) throws IOException { // write... } }; }
Configuring Asynchronous Request Processing
spring-test模塊提供了第一等級的針對注解controller的測試支持。See Section 15.6, “Spring MVC Test Framework”.
在之前的Spring版本中,用戶必須要在web應用上下文中定義一個或者多個HandlerMapping beans 以將incoming web requests映射到合適的handlers。 隨着annotated controllers的引入,現在一般可以不必那樣做了,因為RequestMappingHandlerMapping 會自動在所有@Controller beans中查找@RequestMapping注解。然而,務必記住,所有的繼承自AbstractHandlerMapping的HandlerMapping類,都有以下properties -- 你可以用來定制它們的行為:
interceptors,使用的攔截器列表。
defaultHandler,默認使用的handler -- 當handler mapping 沒有一個匹配的handler時。
order,基於該property的值(Ordered接口),Spring將所有可用的handler mappings進行排序,並應用第一匹配的handler。
alwaysUseFullPath,如果設為true,Spring會在當前Servlet context中使用全路徑來查找合適的handler。如果false(默認就是),會使用當前Servlet mapping內的路徑。例如,如果一個Servlet被映射到/testing/*,當設為true時,使用/testing/viewPage.html,否則,/viewPage.html。
urlDecode,默認true,自Spring 2.5起。如果你傾向於對比encoded paths,需要設為false。然而,HttpServletRequest總是以decoded形式暴露Servlet path。注意,當與encoded path對比時,Servlet path不會匹配。
配置攔截器的例子:
<beans> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"> <property name="interceptors"> <bean class="example.MyInterceptor"/> </property> </bean> <beans>
4.1 使用HandlerInterceptor攔截requests
spring的handler mapping機制包括handler interceptors,當你想要針對特定的requests應用特定的功能時,非常有用。
位於handler mapping內的interceptors,必須實現org.springframework.web.servlet.HandlerInterceptor (或者其實現/子類)。
該接口定義有三個方法preHandle(..) postHandle(..) afterHandle(..)。 見這里。(為知筆記的連接,不知道行不行,以后再說)
preHandle(..)方法會返回一個boolean值,如果false,會破壞執行鏈的處理過程(不再往下執行)。如果false,DispatcherServlet會認定該攔截器自身來處理請求(例如,渲染視圖等),所以不會繼續執行其他的攔截器和實際的handler。
攔截器可以在所有繼承自AbstractHandlerMapping的類中設置,使用其interceptors屬性! 如下:
<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; } }
由該映射處理的所有請求,都會被 TimeBaseAccessInterceptor 攔截。 該攔截器的就是讓你只能在辦公時間訪問。
注意:當使用RequestMappingHandlerMapping時,實際的handler是HandlerMethod的一個實例,該HandlerMethod會識別要被調用的特定的controller method。
我的補充:handler mapping這個過程,是將request與handler之間映射起來的過程。Spring提供的實現類,能用的也就這幾個:
--- 還有一個RequestMappingHandlerAdapter,不要混淆了。
如你所見,Spring的適配器類 HandlerInterceptorAdapter,讓繼承HandlerInterceptor更加簡單。
在上面的例子中,被配置過的攔截器會被應用到所有由注解過的controller method處理的請求上。如果你想窄化一個攔截器應用的URL路徑,你可以使用MVC 命名空間或者MVC Java config,或者聲明MappedInterceptor類型的bean實例來完成。See Section 22.16.1, “Enabling the MVC Java Config or the MVC XML Namespace”.
注意:postHandle方法,通常不能理想地配合@ResponseBody和ResponseEntity方法。 在這種情況下,一個HttpMessageConverter會寫入並提交響應 -- 先於postHandle!從而導致無法修改響應,例如,添加一個header。這種情況下,可以實現ResponseBodyAdvice,然后要么將其聲明為@ControllerAdvice bean,要么直接在RequestMappingHandlerAdapter中配置。