SpringMVC(4) - 控制器(2) - @RequestMapping(1) - 對映
1. 簡單使用
可以使用@RequestMapping註解將 /appointments 等URL對映到整個類或特定的處理器方法上。 通常,類級註解將特定請求路徑(或路徑模式)對映到表單控制器上,其他方法級註解通過HTTP請求方法(如GET、POST請求)或請求引數條件縮小主對映範圍。
以下示例顯示了使用註解的Spring MVC應用程式中的控制器:
@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 的GET請求。
getForDay()方法顯示了@RequestMapping:URI模板的另一種用法。
不需要類級別的@RequestMapping。 沒有它,所有路徑都是絕對的,而不是相對的。 以下示例顯示了使用@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()); } }
上面的示例未指定GET與PUT,POST等,因為@RequestMapping預設對映所有HTTP方法。 使用@RequestMapping(method = GET)或@GetMapping來縮小對映範圍。
2. 組合@RequestMapping
Spring Framework 4.3引入了以下@RequestMapping註解的方法級組合變體,這些變體有助於簡化常見HTTP方法的對映,並更好地表達帶註解的處理器方法的語義。 例如,@GetMapping可以讀作GET @RequestMapping。
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
- @PatchMapping
以下示例顯示了上一節中AppointmentsController的修改版本,該版本已使用組合的@RequestMapping註釋進行了簡化。
@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";
}
}
3. @Controller和AOP代理
在某些情況下,控制器可能需要在執行時使用AOP代理進行修飾。 例如,如果選擇在控制器上直接使用@Transactional註解。 在這種情況下,對於控制器而言,Spring建議使用基於類的代理。 這通常是控制器的預設選擇。 但是,如果控制器必須實現不是Spring Context回撥的介面(例如InitializingBean,* Aware等),則可能需要顯式配置基於類的代理。 例如,使用<tx:annotation-driven />,更改為<tx:annotation-driven proxy-target-class =“true”/>。
4. Spring MVC 3.1中@RequestMapping方法的新支援類
Spring 3.1為@RequestMapping方法引入了一組新的支援類,分別稱為RequestMappingHandlerMapping和RequestMappingHandlerAdapter。建議使用它們,甚至需要利用Spring MVC 3.1中的新功能並進一步使用。預設情況下,MVC名稱空間和MVC Java配置啟用新的支援類,但如果兩者都不使用,則必須明確配置。本節介紹舊支援類和新支援類之間的一些重要差異。
到Spring 3.1之前,型別和方法級請求對映檢查在兩個分開的階段:
- 第一,由DefaultAnnotationHandlerMapping選擇控制器。
- 第二,由AnnotationMethodHandlerAdapter呼叫的實際方法縮小範圍。
在Spring 3.1中新增新支援類後,RequestMappingHandlerMapping是唯一決定應該處理請求的方法的地方。將控制器方法視為一組唯一端點,其中每個方法的對映都是從型別和方法級別的@RequestMapping資訊派生的。
這確保了一些新的可能性。HandlerInterceptor或HandlerExceptionResolver現在可以期望基於物件的處理器是HandlerMethod,那麼,它允許它們檢查確切的方法、引數和相關的註解。不再需要跨不同控制器分割URL的處理。
之前的一些匹配機制將不存在:
- 首先使用SimpleUrlHandlerMapping或BeanNameUrlHandlerMapping選擇控制器,然後根據@RequestMapping註解縮小方法。
- 依靠方法名稱作為後退機制來消除兩個沒有顯式路徑對映URL路徑但是否相同匹配的@RequestMapping方法之間的歧義,例如, 通過HTTP方法。 在新的支援類中,必須唯一地對映@RequestMapping方法。
- 如果沒有其他控制器方法更具體地匹配,則使用單個預設方法(沒有顯式路徑對映)處理請求。 在新的支援類中,如果找不到匹配的方法,則會引發404錯誤。
現有支援類仍支援上述功能。 但是,要利用新的Spring MVC 3.1功能,需要使用新的支援類。
5. URI模板模式
URI模板可用於在@RequestMapping方法中方便地訪問URL的選定部分。
URI模板是類似URI的字串,包含一個或多個變數名稱。 當為這些變數替換值時,模板將成為URI。 例如,URI模板http://www.example.com/users/{userId}包含變數userId。 將值 1 分配給變數會產生http://www.example.com/users/1。
在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模板“/owners/{ownerId}`”指定變數名稱`ownerId。 當控制器處理此請求時,ownerId的值將設定為URI的相應部分中找到的值。 例如,當/owner/1 的請求進入時,ownerId的值為 1。
注:
要處理@PathVariable註解,Spring MVC需要按名稱查詢匹配的URI模板變數。 可以在註解中指定它:
@GetMapping("/owners/{ownerId}")
public String findOwner(@PathVariable("ownerId") String theOwner, Model model) {
// implementation omitted
}
或者,如果URI模板變數名稱與方法引數名稱匹配,則可以省略該詳細資訊。 只要程式碼使用除錯資訊或Java 8上的-parameters編譯器標誌進行編譯,Spring MVC就會將方法引數名稱與URI模板變數名稱進行匹配:
@GetMapping("/owners/{ownerId}")
public String findOwner(@PathVariable String ownerId, 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註解時,將使用所有URI模板變數填充對映。
可以從型別和方法級別@RequestMapping註釋組裝URI模板。 因此,可以使用諸如/owners/42/pets/21之類的URL呼叫findPet()方法。
@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {
@RequestMapping("/pets/{petId}")
public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
// implementation omitted
}
}
@PathVariable引數可以是任何簡單型別,例如int,long,Date等。如果未能執行此操作,Spring會自動轉換為適當的型別或丟擲TypeMismatchException。 還可以註冊解析其他資料型別的支援。
6. 具有正則表示式的URI模板模式
有時需要更精確地定義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) {
// ...
}
7. 路徑模式
除URI模板外,@RequestMapping註解和所有組合的@RequestMapping變體還支援Ant樣式的路徑模式(例如,/myPath/*.do)。 還支援URI模板變數和Ant樣式globs的組合(例如/owners/*/pets/{petId})。
8. 路徑模式比較
當URL匹配多個模式時,使用排序來查詢最具體的匹配。
具有較低URI變數和萬用字元計數的模式被認為更具體。 例如/hotels/{hotel}/*有1個URI變數和1個萬用字元,被認為比/hotels/{hotel}/**更具體,它們是1個URI變數和2個萬用字元。
如果兩個模式具有相同的計數,則更長的模式被認為更具體。 例如/foo/bar*比/foo/*更長並且被認為更具體。
當兩個模式具有相同的計數和長度時,具有較少萬用字元的模式被認為更具體。 例如/hotels/{hotel}比/hotels/*更具體。
還有一些額外的特殊規則:
- 預設對映模式/**不如任何其他模式具體。 例如/api/{a}/{b}/{c}更具體。
- 諸如/public/**之類的字首模式不如任何其他不包含雙萬用字元的模式特定。 例如/public/path3/{a}/{b}/{c}更具體。
有關完整詳細資訊,請參閱AntPathMatcher中的AntPatternComparator。 可以自定義PathMatcher(請參見有關配置Spring MVC的部分中的第22.16.11節“路徑匹配”)。
9. 帶佔位符的路徑模式
@RequestMapping註解中的模式支援針對本地屬性或系統屬性和環境變數的$ {...}佔位符。 在控制器對映到的路徑可能需要通過配置進行自定義的情況下,這可能很有用。 有關佔位符的更多資訊,請參閱PropertyPlaceholderConfigurer類的javadoc。
10. 字尾模式匹配
預設情況下,Spring MVC執行“.*”字尾模式匹配,以便對映到/person的控制器也隱式對映到/person.*。這使得通過URL路徑(例如/person.pdf,/person.xml)請求資源的不同表示變得容易。
可以關閉字尾模式匹配或將其限制為為內容協商目的明確註冊的一組路徑擴充套件。通常建議使用常見請求對映來最小化歧義,例如/ person/{id},其中點可能不代表副檔名,例如/person/[email protected] vs /person/[email protected]。此外,如下面的註解中所解釋的,字尾模式匹配以及內容協商可能在某些情況下用於嘗試惡意攻擊,並且有充分的理由對其進行有意義的限制。
有關字尾模式匹配配置,請參見第22.16.11節“路徑匹配”,對於內容協商配置,請參見第22.16.6節“內容協商”。
11. 字尾模式匹配和RFD
2014年,Trustwave在一篇論文中首次描述了反射檔案下載(RFD:Reflected File Download)攻擊。該攻擊類似於XSS,因為它依賴於響應中反映的輸入(例如查詢引數,URI變數)。但是,如果基於副檔名(例如.bat,.cmd)雙擊,則RFD攻擊不依賴於將JavaScript插入HTML,而是依賴瀏覽器切換來執行下載並將響應視為可執行指令碼。
在Spring MVC @ResponseBody和ResponseEntity方法存在風險,因為它們可以呈現客戶端可以請求的不同內容型別,包括通過URL路徑擴充套件。但請注意,既不禁用字尾模式匹配也不禁用路徑擴充套件僅用於內容協商目的,這對於防止RFD攻擊都是有效的。
為了全面防範RFD,在展示響應體之前,Spring MVC添加了一個 Content-Disposition:inline;filename=f.txt 頭,以建議一個固定且安全的下載檔案檔名。僅當URL路徑包含既未列入白名單也未明確註冊以用於內容協商目的的副檔名時,才會執行此操作。但是,當直接在瀏覽器中輸入URL時,它可能會產生副作用。
預設情況下,許多常見路徑副檔名都列入白名單。此外,REST API呼叫通常不能直接在瀏覽器中用作URL。但是,使用自定義HttpMessageConverter實現的應用程式可以顯式註冊副檔名以進行內容協商,並且不會為此類擴充套件新增Content-Disposition頭。
注:
這最初是作為CVE-2015-5211工作的一部分而引入的。 以下是報告中的其他建議:
- 編碼而不是轉義JSON響應。 這也是OWASP XSS的推薦。 有關如何使用Spring執行此操作的示例請參閱spring-jackson-owasp。
- 將字尾模式匹配配置為僅關閉或限制為僅顯式註冊的字尾。
- 配置內容協商,將屬性“useJaf”和“ignoreUnknownPathExtensions”設定為false,這將導致對未知副檔名的URL進行406響應。 但請注意,如果URL自然希望在結尾處有一個點,則可能不會選擇此選項。
- 新增X-Content-Type-Options:nosniff頭到響應。 Spring Security 4預設執行此操作。
12. 矩陣變數
URI規範RFC 3986定義了在路徑段中包含名稱 - 值對的可能性。規範中沒有使用特定術語。可以應用一般的“URI路徑引數”,儘管源自Tim Berners-Lee的舊帖子的更獨特的“矩陣URI”也經常被使用並且是眾所周知的。在Spring MVC中,這些被稱為矩陣變數。
矩陣變數可以出現在任何路徑段中,每個矩陣變數用“;”分隔(分號)。例如:“/cars;color=red;year=2012”。多個值可以是“,”(逗號)分隔的“顏色=紅色,綠色,藍色”或變數名稱可以重複“顏色=紅色;顏色=綠色;顏色=藍色”。
如果URL預計包含矩陣變數,則請求對映模式必須使用URI模板表示它們。這確保了請求可以正確匹配,無論是否存在矩陣變數以及它們的提供順序。
下面是提取矩陣變數“q”的示例:
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
由於所有路徑段都可能包含矩陣變數,因此在某些情況下,需要更具體地確定變數的預期位置:
// 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
}
矩陣變數可以定義為可選,並指定預設值:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
所有矩陣變數都可以在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]
}
請注意,要啟用矩陣變數,必須將RequestMappingHandlerMapping的removeSemicolonContent屬性設定為false。 預設情況下,它設定為true。
注:
MVC Java配置和MVC名稱空間都提供了啟用矩陣變數的選項。
如果使用的是Java配置,則“使用MVC Java配置進行高階自定義”部分介紹瞭如何自定義RequestMappingHandlerMapping。
在MVC名稱空間中,<mvc:annotation-driven>元素具有應該設定為true的enable-matrix-variables屬性。 預設情況下,它設定為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>
13. 媒體型別
可以通過指定可使用的媒體型別列表來縮小主對映。 僅當Content-Type請求頭與指定的媒體型別匹配時,才會匹配請求。 例如:
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet, Model model) {
// implementation omitted
}
可使用的媒體型別表示式也可以使用否定:!text/plain,以匹配除了Content-Type為text/plain之外的所有請求。 還要考慮使用MediaType中提供的常量,例如APPLICATION_JSON_VALUE和APPLICATION_JSON_UTF8_VALUE。
提示:型別和方法級別支援可使用媒體型別條件。 與大多數其他條件不同,在型別級別使用時,方法級可使用媒體型別會覆蓋而不是擴充套件型別級可使用媒體型別。
14. 可生成的媒體型別
可以通過指定可生成的媒體型別列表來縮小主對映。 僅當Accept請求頭與其中一個值匹配時,才會匹配請求。 此外,使用生成條件可確保用於生成響應的實際內容型別遵循生成條件中指定的媒體型別。 例如:
@GetMapping(path = "/pets/{petId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Pet getPet(@PathVariable String petId, Model model) {
// implementation omitted
}
注:請注意,生成條件中指定的媒體型別也可以選擇指定字符集。 例如,在上面的程式碼片段中,我們指定的媒體型別與MappingJackson2HttpMessageConverter中配置的預設媒體型別相同,包括UTF-8字符集。
就像使用消費一樣,可生成的媒體型別表示式可以使用否定:!text/plain,以匹配除了具有text/plain的Accept頭的請求之外的所有請求。 還要考慮使用MediaType中提供的常量,例如APPLICATION_JSON_VALUE和APPLICATION_JSON_UTF8_VALUE。
注:型別和方法級別支援生成條件。 與大多數其他條件不同,在型別級別使用時,方法級可生成型別會覆蓋而不是擴充套件型別級可生成型別。
15. 請求引數和請求頭值
可以通過請求引數條件縮小請求匹配,例如“myParam”,“!myParam”或“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頭值(例如“content-type=text/*”將匹配“text/plain”和“text/html”),但建議使用 分別可以使用和產生的條件。 它們專門用於此目的。
16. HTTP HEAD和HTTP OPTIONS
對映到“GET”的@RequestMapping方法也隱式對映到“HEAD”,即不需要顯式宣告“HEAD”。處理HTTP HEAD請求就好像它是HTTP GET一樣,除了不寫入主體,只計算位元組數並設定“Content-Length”標頭。
@RequestMapping方法內建了對HTTP OPTIONS的支援。預設情況下,通過將“允許”響應頭設定為在具有匹配URL模式的所有@RequestMapping方法上顯式宣告的HTTP方法來處理HTTP OPTIONS請求。當沒有顯式宣告HTTP方法時,“Allow”標頭設定為“GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS”。理想情況下,始終宣告@RequestMapping方法要處理的HTTP方法,或者使用專用的@RequestMapping變體之一。
雖然沒有必要,但@RequestMapping方法可以對映到並處理HTTP HEAD或HTTP OPTIONS,或兩者兼而有之。