請求對映
本節討論帶註解的 Controller 的請求對映。
@RequestMapping
你可以使用 @RequestMapping
註解將請求對映到 Controller 方法。它有多種屬性,可以透過 URL、HTTP 方法、請求引數、請求頭和媒體型別進行匹配。你可以在類級別使用它來表達共享對映,或者在方法級別使用它來縮小到特定的端點對映。
@RequestMapping
還有針對特定 HTTP 方法的快捷變體:
-
@GetMapping
-
@PostMapping
-
@PutMapping
-
@DeleteMapping
-
@PatchMapping
這些快捷方式是 自定義註解,提供它們是因為,可以說,大多數 Controller 方法應該對映到特定的 HTTP 方法,而不是使用預設匹配所有 HTTP 方法的 @RequestMapping
。在類級別仍然需要 @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 模式進行對映。有兩種替代方案:
-
PathPattern
— 一種預解析的模式,用於匹配也預解析為PathContainer
的 URL 路徑。該方案專為 Web 用途設計,能有效處理編碼和路徑引數,並且匹配效率高。 -
AntPathMatcher
— 將 String 模式與 String 路徑進行匹配。這是原始的解決方案,也用於 Spring 配置中選擇類路徑、檔案系統和其他位置的資源。它的效率較低,並且 String 路徑輸入在有效處理 URL 編碼及其他問題方面具有挑戰性。
PathPattern
是 Web 應用的推薦解決方案,並且是 Spring WebFlux 中唯一的選擇。從 5.3 版本開始,它在 Spring MVC 中被啟用,並從 6.0 版本開始預設啟用。有關路徑匹配選項的自定義,請參閱 MVC 配置。
PathPattern
支援與 AntPathMatcher
相同的模式語法。此外,它還支援捕獲模式,例如 {*spring}
,用於匹配路徑末尾的 0 個或多個路徑段。PathPattern
還限制使用 **
來匹配多個路徑段,只允許它出現在模式的末尾。這消除了在為給定請求選擇最佳匹配模式時出現的許多歧義情況。有關完整的模式語法,請參閱 PathPattern 和 AntPathMatcher。
一些模式示例:
-
"/resources/ima?e.png"
- 匹配路徑段中的一個字元 -
"/resources/*.png"
- 匹配路徑段中的零個或多個字元 -
"/resources/**"
- 匹配多個路徑段 -
"/projects/{project}/versions"
- 匹配路徑段並將其捕獲為變數 -
"/projects/{project:[a-z]+}/versions"
- 使用正則表示式匹配並捕獲變數
捕獲的 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 進行引數化。
模式比較
當多個模式匹配一個 URL 時,必須選擇最佳匹配。根據是否啟用解析的 PathPattern
,使用以下方法之一進行選擇:
兩者都有助於將更具體的模式排在前面。一個模式如果 URI 變數(計為 1)、單個萬用字元(計為 1)和雙萬用字元(計為 2)的數量較低,則更具體。在得分相等的情況下,選擇更長的模式。在得分和長度相同的情況下,選擇 URI 變數多於萬用字元的模式。
預設對映模式 (/**
) 不參與評分,總是排在最後。此外,字首模式(如 /public/**
)被認為不如沒有雙萬用字元的其他模式具體。
有關完整詳細資訊,請點選上面的連結檢視模式比較器。
字尾匹配
從 5.3 版本開始,Spring MVC 預設不再執行 .*
字尾模式匹配,即對映到 /person
的 Controller 不再隱式對映到 /person.*
。因此,路徑副檔名不再用於解釋響應請求的內容型別——例如,/person.pdf
、/person.xml
等。
以前,當瀏覽器傳送難以一致解釋的 Accept
請求頭時,以這種方式使用副檔名是必要的。目前,這已不再是必需的,應該首選使用 Accept
請求頭。
隨著時間的推移,檔名副檔名的使用在多種方面已被證明存在問題。當與 URI 變數、路徑引數和 URI 編碼重疊使用時,它可能導致歧義。基於 URL 的授權和安全性推理(更多詳細資訊見下一節)也變得更加困難。
要在 5.3 之前的版本中完全停用路徑副檔名,請設定以下內容:
-
useSuffixPatternMatching(false)
,請參閱 PathMatchConfigurer -
favorPathExtension(false)
,請參閱 ContentNegotiationConfigurer
擁有除透過 "Accept"
請求頭之外的方式來請求內容型別仍然有用,例如在瀏覽器中輸入 URL 時。路徑副檔名的一種安全替代方案是使用查詢引數策略。如果必須使用副檔名,請考慮透過 ContentNegotiationConfigurer 的 mediaTypes
屬性將其限制為顯式註冊的副檔名列表。
字尾匹配和 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
請求頭和 Controller 方法生成的內容型別列表來縮小請求對映的範圍,如下例所示:
-
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 。 |
HTTP HEAD, OPTIONS
@GetMapping
(以及 @RequestMapping(method=HttpMethod.GET)
)為請求對映透明地支援 HTTP HEAD。Controller 方法無需更改。在 jakarta.servlet.http.HttpServlet
中應用的響應包裝器確保 Content-Length
請求頭被設定為寫入的位元組數(而實際上並未寫入響應)。
預設情況下,HTTP OPTIONS 透過將 Allow
響應頭設定為所有具有匹配 URL 模式的 @RequestMapping
方法中列出的 HTTP 方法列表來處理。
對於沒有宣告 HTTP 方法的 @RequestMapping
,Allow
請求頭被設定為 GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
。Controller 方法應始終宣告支援的 HTTP 方法(例如,透過使用特定於 HTTP 方法的變體:@GetMapping
、@PostMapping
等)。
你可以顯式地將 @RequestMapping
方法對映到 HTTP HEAD 和 HTTP OPTIONS,但在一般情況下沒有必要。
自定義註解
Spring MVC 支援使用 組合註解 進行請求對映。這些註解本身被 @RequestMapping
元註解,並組合以更窄、更具體的方式重新宣告 @RequestMapping
的部分(或全部)屬性。
@GetMapping
、@PostMapping
、@PutMapping
、@DeleteMapping
和 @PatchMapping
是組合註解的示例。提供它們是因為,可以說,大多數 Controller 方法應該對映到特定的 HTTP 方法,而不是使用預設匹配所有 HTTP 方法的 @RequestMapping
。如果你需要如何實現組合註解的示例,請檢視它們的宣告方式。
@RequestMapping 不能與宣告在同一元素(類、介面或方法)上的其他 @RequestMapping 註解一起使用。如果在同一元素上檢測到多個 @RequestMapping 註解,將記錄警告,並且只使用第一個對映。這同樣適用於組合的 @RequestMapping 註解,例如 @GetMapping 、@PostMapping 等。 |
Spring MVC 還支援帶有自定義請求匹配邏輯的自定義請求對映屬性。這是一個更高階的選項,需要繼承 RequestMappingHandlerMapping
並重寫 getCustomMethodCondition
方法,在該方法中,你可以檢查自定義屬性並返回自己的 RequestCondition
。
顯式註冊
你可以透過程式設計方式註冊 handler 方法,這可用於動態註冊或高階場景,例如同一 handler 的不同例項在不同 URL 下。以下示例註冊了一個 handler 方法:
-
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 | 注入目標 handler 和用於 Controller 的 handler 對映。 |
2 | 準備請求對映元資料。 |
3 | 獲取 handler 方法。 |
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 | 注入目標 handler 和用於 Controller 的 handler 對映。 |
2 | 準備請求對映元資料。 |
3 | 獲取 handler 方法。 |
4 | 添加註冊。 |
@HttpExchange
雖然 @HttpExchange
的主要目的是透過生成的代理抽象化 HTTP 客戶端程式碼,但放置此類註解的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
支援的完整語法。