對映請求
本節討論註解控制器的請求對映。
@RequestMapping
你可以使用 @RequestMapping 註解將請求對映到控制器方法。它具有各種屬性,可以根據 URL、HTTP 方法、請求引數、請求頭和媒體型別進行匹配。你可以在類級別使用它來表達共享對映,也可以在方法級別使用它來縮小到特定的端點對映。
還有 HTTP 方法特定的 @RequestMapping 快捷變體
-
@GetMapping -
@PostMapping -
@PutMapping -
@DeleteMapping -
@PatchMapping
提供這些快捷方式是自定義註解,因為,可以說,大多數控制器方法應該對映到特定的 HTTP 方法,而不是使用 @RequestMapping(預設情況下,它匹配所有 HTTP 方法)。在類級別仍然需要 @RequestMapping 來表達共享對映。
@RequestMapping 不能與同一元素(類、介面或方法)上宣告的其他 @RequestMapping 註解一起使用。如果在同一元素上檢測到多個 @RequestMapping 註解,將記錄警告,並且只使用第一個對映。這也適用於組合的 @RequestMapping 註解,例如 @GetMapping、@PostMapping 等。 |
以下示例具有型別和方法級別的對映
-
Java
-
Kotlin
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
fun getPerson(@PathVariable id: Long): Person {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun add(@RequestBody person: Person) {
// ...
}
}
URI 模式
@RequestMapping 方法可以使用 URL 模式進行對映。Spring MVC 使用 PathPattern——一個預解析的模式,與同樣預解析為 PathContainer 的 URL 路徑進行匹配。該解決方案專為 Web 使用而設計,有效處理編碼和路徑引數,並高效匹配。有關路徑匹配選項的自定義,請參閱 MVC 配置。
AntPathMatcher 變體現在已棄用,因為它效率較低,並且字串路徑輸入對於有效處理編碼和 URL 的其他問題來說是一個挑戰。 |
你可以使用 glob 模式和萬用字元對映請求
| 模式 | 描述 | 示例 |
|---|---|---|
|
字面模式 |
|
|
匹配一個字元 |
|
|
匹配路徑段中的零個或多個字元 |
|
|
匹配零個或多個路徑段 |
不允許使用 |
|
類似於 |
|
|
將正則表示式 |
|
|
類似於 |
不允許使用 |
捕獲的 URI 變數可以透過 @PathVariable 訪問。例如
-
Java
-
Kotlin
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}
你可以在類和方法級別宣告 URI 變數,如以下示例所示
-
Java
-
Kotlin
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {
@GetMapping("/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
@Controller
@RequestMapping("/owners/{ownerId}")
class OwnerController {
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}
}
URI 變數會自動轉換為適當的型別,否則會引發 TypeMismatchException。預設支援簡單型別(int、long、Date 等),你可以為任何其他資料型別註冊支援。請參閱 型別轉換 和 DataBinder。
你可以顯式命名 URI 變數(例如,@PathVariable("customId")),但如果名稱相同且你的程式碼使用 -parameters 編譯器標誌編譯,則可以省略該細節。
語法 {varName:regex} 宣告一個帶有正則表示式的 URI 變數,其語法為 {varName:regex}。例如,給定 URL "/spring-web-3.0.5.jar",以下方法提取名稱、版本和副檔名
-
Java
-
Kotlin
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
// ...
}
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable name: String, @PathVariable version: String, @PathVariable ext: String) {
// ...
}
URI 路徑模式也可以有
-
嵌入式
${…}佔位符,透過PropertySourcesPlaceholderConfigurer在啟動時針對本地、系統、環境和其他屬性源進行解析。這對於例如根據外部配置引數化基本 URL 非常有用。 -
SpEL 表示式
#{…}。
模式比較
當多個模式匹配一個 URL 時,必須選擇最佳匹配。這透過以下方法之一完成,具體取決於是否啟用解析的 PathPattern:
兩者都有助於對模式進行排序,更具體的模式排在前面。如果模式的 URI 變數(計為 1)、單個萬用字元(計為 1)和雙萬用字元(計為 2)的數量較少,則該模式更具體。如果分數相同,則選擇較長的模式。如果分數和長度相同,則選擇 URI 變數多於萬用字元的模式。
預設對映模式 (/**) 不參與評分,並且始終排在最後。此外,字首模式(例如 /public/**)被認為不如其他沒有雙萬用字元的模式具體。
有關完整詳細資訊,請點選以上鍊接到模式比較器。
字尾匹配和 RFD
反射檔案下載 (RFD) 攻擊類似於 XSS,因為它依賴於請求輸入(例如,查詢引數和 URI 變數)在響應中被反射。但是,RFD 攻擊不是將 JavaScript 插入 HTML,而是依賴於瀏覽器切換到執行下載並將響應視為可執行指令碼(在稍後雙擊時)。
在 Spring MVC 中,@ResponseBody 和 ResponseEntity 方法存在風險,因為它們可以渲染不同的內容型別,客戶端可以透過 URL 路徑擴充套件請求這些內容型別。停用字尾模式匹配並使用路徑擴充套件進行內容協商可以降低風險,但不足以防止 RFD 攻擊。
為了防止 RFD 攻擊,在渲染響應正文之前,Spring MVC 會新增 Content-Disposition:inline;filename=f.txt 標頭,以建議一個固定且安全的下載檔案。僅當 URL 路徑包含的副檔名既不允許作為安全檔案,也沒有明確註冊用於內容協商時,才會執行此操作。但是,當 URL 直接在瀏覽器中輸入時,它可能會產生副作用。
預設情況下,許多常見的路徑副檔名都被允許為安全。具有自定義 HttpMessageConverter 實現的應用程式可以明確註冊用於內容協商的副檔名,以避免為這些副檔名新增 Content-Disposition 標頭。請參閱 內容型別。
有關 RFD 的更多建議,請參閱 CVE-2015-5211。
可消費媒體型別
你可以根據請求的 Content-Type 縮小請求對映範圍,如以下示例所示
-
Java
-
Kotlin
@PostMapping(path = "/pets", consumes = "application/json") (1)
public void addPet(@RequestBody Pet pet) {
// ...
}
| 1 | 使用 consumes 屬性根據內容型別縮小對映範圍。 |
@PostMapping("/pets", consumes = ["application/json"]) (1)
fun addPet(@RequestBody pet: Pet) {
// ...
}
| 1 | 使用 consumes 屬性根據內容型別縮小對映範圍。 |
consumes 屬性還支援否定表示式——例如,!text/plain 表示除 text/plain 以外的任何內容型別。
你可以在類級別宣告一個共享的 consumes 屬性。然而,與大多數其他請求對映屬性不同,當在類級別使用時,方法級別的 consumes 屬性會覆蓋而不是擴充套件類級別的宣告。
MediaType 提供常用媒體型別的常量,例如 APPLICATION_JSON_VALUE 和 APPLICATION_XML_VALUE。 |
可生產媒體型別
你可以根據 Accept 請求頭和控制器方法生成的內容型別列表縮小請求對映範圍,如以下示例所示
-
Java
-
Kotlin
@GetMapping(path = "/pets/{petId}", produces = "application/json") (1)
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
| 1 | 使用 produces 屬性根據內容型別縮小對映範圍。 |
@GetMapping("/pets/{petId}", produces = ["application/json"]) (1)
@ResponseBody
fun getPet(@PathVariable petId: String): Pet {
// ...
}
| 1 | 使用 produces 屬性根據內容型別縮小對映範圍。 |
媒體型別可以指定字元集。支援否定表示式——例如,!text/plain 表示除 "text/plain" 以外的任何內容型別。
你可以在類級別宣告一個共享的 produces 屬性。然而,與大多數其他請求對映屬性不同,當在類級別使用時,方法級別的 produces 屬性會覆蓋而不是擴充套件類級別的宣告。
MediaType 提供常用媒體型別的常量,例如 APPLICATION_JSON_VALUE 和 APPLICATION_XML_VALUE。 |
引數、請求頭
你可以根據請求引數條件縮小請求對映範圍。你可以測試請求引數是否存在 (myParam)、是否存在 (!myParam) 或特定值 (myParam=myValue)。以下示例展示瞭如何測試特定值
-
Java
-
Kotlin
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
| 1 | 測試 myParam 是否等於 myValue。 |
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
| 1 | 測試 myParam 是否等於 myValue。 |
你也可以將相同的與請求頭條件一起使用,如以下示例所示
-
Java
-
Kotlin
@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
| 1 | 測試 myHeader 是否等於 myValue。 |
@GetMapping("/pets/{petId}", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
| 1 | 測試 myHeader 是否等於 myValue。 |
API 版本
沒有標準方法來指定 API 版本,因此當你在 MVC 配置 中啟用 API 版本控制時,你需要指定如何解析版本。MVC 配置會建立一個 ApiVersionStrategy,該策略又用於對映請求。
一旦啟用 API 版本控制,你就可以開始對映帶有版本的請求。@RequestMapping 的 version 屬性支援以下內容
-
無值——匹配任何版本
-
固定版本("1.2")——僅匹配給定版本
-
基線版本 ("1.2+")——匹配給定版本及以上版本
如果多個控制器方法的版本小於或等於請求版本,則選擇其中最高且最接近請求版本的那個,實際上取代了其餘的。
為了說明這一點,請考慮以下對映
-
Java
@RestController
@RequestMapping("/account/{id}")
public class AccountController {
@GetMapping (1)
public Account getAccount() {
}
@GetMapping(version = "1.1") (2)
public Account getAccount1_1() {
}
@GetMapping(version = "1.2+") (3)
public Account getAccount1_2() {
}
@GetMapping(version = "1.5") (4)
public Account getAccount1_5() {
}
}
| 1 | 匹配任何版本 |
| 2 | 匹配版本 1.1 |
| 3 | 匹配版本 1.2 及以上 |
| 4 | 匹配版本 1.5 |
對於版本為 "1.3" 的請求
-
(1) 匹配,因為它匹配任何版本
-
(2) 不匹配
-
(3) 匹配,因為它匹配 1.2 及以上版本,並且被選中為最高匹配
-
(4) 更高且不匹配
對於版本為 "1.5" 的請求
-
(1) 匹配,因為它匹配任何版本
-
(2) 不匹配
-
(3) 匹配,因為它匹配 1.2 及以上版本
-
(4) 匹配,並且被選中為最高匹配
版本為 "1.6" 的請求沒有匹配項。(1) 和 (3) 確實匹配,但被 (4) 取代,(4) 只允許嚴格匹配,因此不匹配。在這種情況下,NotAcceptableApiVersionException 會導致 400 響應。
| 以上假設請求版本是 "支援" 版本,否則會失敗。 |
有關底層基礎設施和 API 版本控制支援的更多詳細資訊,請參閱 API 版本控制。
HTTP HEAD, OPTIONS
@GetMapping(和 @RequestMapping(method=HttpMethod.GET))透明地支援 HTTP HEAD 用於請求對映。控制器方法不需要更改。在 jakarta.servlet.http.HttpServlet 中應用的響應包裝器確保 Content-Length 標頭設定為寫入的位元組數(而無需實際寫入響應)。
預設情況下,HTTP OPTIONS 透過將 Allow 響應頭設定為所有具有匹配 URL 模式的 @RequestMapping 方法中列出的 HTTP 方法列表來處理。
對於沒有 HTTP 方法宣告的 @RequestMapping,Allow 標頭設定為 GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS。控制器方法應始終宣告支援的 HTTP 方法(例如,透過使用 HTTP 方法特定的變體:@GetMapping、@PostMapping 等)。
你可以顯式地將 @RequestMapping 方法對映到 HTTP HEAD 和 HTTP OPTIONS,但在常見情況下沒有必要。
自定義註解
Spring MVC 支援使用組合註解進行請求對映。這些註解本身是用 @RequestMapping 元註解的註解,並組合起來重新宣告 @RequestMapping 屬性的子集(或全部),具有更窄、更具體的用途。
@GetMapping、@PostMapping、@PutMapping、@DeleteMapping 和 @PatchMapping 是組合註解的示例。之所以提供它們,是因為,可以說,大多數控制器方法應該對映到特定的 HTTP 方法,而不是使用 @RequestMapping,後者預設匹配所有 HTTP 方法。如果你需要一個如何實現組合註解的示例,請檢視這些註解是如何宣告的。
@RequestMapping 不能與同一元素(類、介面或方法)上宣告的其他 @RequestMapping 註解一起使用。如果在同一元素上檢測到多個 @RequestMapping 註解,將記錄警告,並且只使用第一個對映。這也適用於組合的 @RequestMapping 註解,例如 @GetMapping、@PostMapping 等。 |
Spring MVC 還支援帶有自定義請求匹配邏輯的自定義請求對映屬性。這是一個更高階的選項,需要繼承 RequestMappingHandlerMapping 並重寫 getCustomMethodCondition 方法,你可以在其中檢查自定義屬性並返回你自己的 RequestCondition。
顯式註冊
你可以透過程式設計方式註冊處理程式方法,這可用於動態註冊或高階情況,例如在不同的 URL 下使用同一處理程式的不同的例項。以下示例註冊了一個處理程式方法
-
Java
-
Kotlin
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build(); (2)
Method method = UserHandler.class.getMethod("getUser", Long.class); (3)
mapping.registerMapping(info, handler, method); (4)
}
}
| 1 | 注入目標處理程式和控制器處理程式對映。 |
| 2 | 準備請求對映元資料。 |
| 3 | 獲取處理程式方法。 |
| 4 | 添加註冊。 |
@Configuration
class MyConfig {
@Autowired
fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)
val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)
val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)
mapping.registerMapping(info, handler, method) (4)
}
}
| 1 | 注入目標處理程式和控制器處理程式對映。 |
| 2 | 準備請求對映元資料。 |
| 3 | 獲取處理程式方法。 |
| 4 | 添加註冊。 |
@HttpExchange
雖然 @HttpExchange 的主要目的是使用生成的代理抽象 HTTP 客戶端程式碼,但放置此類註解的介面是客戶端與伺服器使用無關的契約。除了簡化客戶端程式碼之外,在某些情況下,HTTP 服務客戶端 可能是伺服器公開其 API 以供客戶端訪問的便捷方式。這會導致客戶端和伺服器之間耦合增加,並且通常不是一個好的選擇,特別是對於公共 API,但對於內部 API 可能正是目標。它是在 Spring Cloud 中常用的一種方法,這也是為什麼 @HttpExchange 作為控制器類中伺服器端處理的 @RequestMapping 的替代方案受到支援。
例如:
-
Java
-
Kotlin
@HttpExchange("/persons")
interface PersonService {
@GetExchange("/{id}")
Person getPerson(@PathVariable Long id);
@PostExchange
void add(@RequestBody Person person);
}
@RestController
class PersonController implements PersonService {
public Person getPerson(@PathVariable Long id) {
// ...
}
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
@HttpExchange("/persons")
interface PersonService {
@GetExchange("/{id}")
fun getPerson(@PathVariable id: Long): Person
@PostExchange
fun add(@RequestBody person: Person)
}
@RestController
class PersonController : PersonService {
override fun getPerson(@PathVariable id: Long): Person {
// ...
}
@ResponseStatus(HttpStatus.CREATED)
override fun add(@RequestBody person: Person) {
// ...
}
}
@HttpExchange 和 @RequestMapping 有區別。@RequestMapping 可以透過路徑模式、HTTP 方法等對映到任意數量的請求,而 @HttpExchange 聲明瞭一個具有具體 HTTP 方法、路徑和內容型別的單個端點。
對於方法引數和返回值,通常,@HttpExchange 支援 @RequestMapping 所支援的方法引數的子集。值得注意的是,它排除了任何伺服器端特定的引數型別。有關詳細資訊,請參閱 @HttpExchange 和 @RequestMapping 的列表。
@HttpExchange 還支援一個 headers() 引數,該引數接受像 "name=value" 這樣的鍵值對,就像客戶端的 @RequestMapping(headers={}) 中一樣。在伺服器端,這擴充套件到 @RequestMapping 支援的完整語法。