請求對應

本節討論註解控制器的請求對應。

@RequestMapping

您可以使用 @RequestMapping 註解將請求對應到控制器方法。它有各種屬性可依 URL、HTTP 方法、請求參數、標頭與媒體類型進行比對。您可以在類別層級使用它來表達共用對應,或在方法層級使用它來縮小到特定端點對應。

也有 @RequestMapping 的 HTTP 方法特定捷徑變體

  • @GetMapping

  • @PostMapping

  • @PutMapping

  • @DeleteMapping

  • @PatchMapping

這些捷徑是自訂註解,其提供的原因是,可以說,大多數控制器方法應該對應到特定的 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 模式

可以使用 URL 模式對應 @RequestMapping 方法。有兩種替代方案

  • PathPattern — 一個預先解析的模式,針對 URL 路徑進行比對,URL 路徑也會預先解析為 PathContainer。此解決方案專為 Web 使用而設計,可有效地處理編碼與路徑參數,並有效率地進行比對。

  • AntPathMatcher — 比對字串模式與字串路徑。這是原始的解決方案,也用於 Spring 組態中,以選取類別路徑、檔案系統與其他位置上的資源。效率較低,且字串路徑輸入在有效地處理編碼與 URL 的其他問題方面是一項挑戰。

PathPattern 是 Web 應用程式的建議解決方案,也是 Spring WebFlux 中唯一的選擇。它已在 Spring MVC 5.3 版中啟用使用,並從 6.0 版開始預設啟用。請參閱MVC 組態,以了解路徑比對選項的客製化。

PathPattern 支援與 AntPathMatcher 相同的模式語法。此外,它也支援擷取模式,例如 {*spring},用於比對路徑結尾的 0 個或多個路徑區段。PathPattern 也限制 ** 的使用,以比對複數個路徑區段,使其僅允許在模式結尾使用。這消除了在為給定的請求選擇最佳比對模式時的許多歧義情況。如需完整的模式語法,請參閱PathPatternAntPathMatcher

一些範例模式

  • "/resources/ima?e.png" - 比對路徑區段中的一個字元

  • "/resources/*.png" - 比對路徑區段中的零個或多個字元

  • "/resources/**" - 比對複數個路徑區段

  • "/projects/{project}/versions" - 比對路徑區段並將其擷取為變數

  • "/projects/{project:[a-z]+}/versions" - 比對並擷取具有正規表示式的變數

可以使用 @PathVariable 存取擷取的 URI 變數。例如

  • 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。預設支援簡單型別 (intlongDate 等),您可以註冊對任何其他資料型別的支援。請參閱型別轉換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/**)被認為不如其他沒有雙萬用字元的模式具體。

如需完整詳細資訊,請追蹤以上連結至模式 Comparator。

後綴比對

從 5.3 開始,預設情況下,Spring MVC 不再執行 .* 後綴模式比對,其中對應到 /person 的控制器也會隱含地對應到 /person.*。因此,路徑副檔名不再用於解譯回應的請求內容類型 — 例如,/person.pdf/person.xml 等等。

當瀏覽器過去傳送難以一致解譯的 Accept 標頭時,以這種方式使用檔案副檔名是必要的。目前,這已不再是必要,並且應該優先選擇使用 Accept 標頭。

隨著時間的推移,已證明檔案名稱副檔名的使用在各種方面都存在問題。當與 URI 變數、路徑參數與 URI 編碼的使用重疊時,可能會造成歧義。關於基於 URL 的授權與安全性的推理(請參閱下一節以取得更多詳細資訊)也變得更加困難。

若要在 5.3 之前的版本中完全停用路徑副檔名的使用,請設定以下內容

除了透過 "Accept" 標頭之外,擁有一種請求內容類型的方法仍然可能很有用,例如,在瀏覽器中輸入 URL 時。路徑副檔名的安全替代方案是使用查詢參數策略。如果您必須使用檔案副檔名,請考慮透過ContentNegotiationConfigurermediaTypes 屬性將它們限制為明確註冊的副檔名列表。

後綴比對與 RFD

反射檔案下載 (RFD) 攻擊類似於 XSS,因為它依賴於在回應中反射的請求輸入(例如,查詢參數與 URI 變數)。然而,RFD 攻擊並非將 JavaScript 插入 HTML,而是依賴於瀏覽器切換以執行下載,並在稍後按兩下時將回應視為可執行腳本。

在 Spring MVC 中,@ResponseBodyResponseEntity 方法有風險,因為它們可以呈現不同的內容類型,客户端可以透過 URL 路徑副檔名請求這些內容類型。停用後綴模式比對與使用路徑副檔名進行內容協商可以降低風險,但不足以預防 RFD 攻擊。

為了預防 RFD 攻擊,在呈現回應 Body 之前,Spring MVC 會新增 Content-Disposition:inline;filename=f.txt 標頭,以建議固定的安全下載檔案。只有當 URL 路徑包含既不允許作為安全也不為內容協商明確註冊的檔案副檔名時,才會執行此操作。然而,當 URL 直接輸入到瀏覽器中時,可能會產生副作用。

許多常見的路徑副檔名預設允許作為安全。具有自訂 HttpMessageConverter 實作的應用程式可以明確註冊用於內容協商的檔案副檔名,以避免為這些副檔名新增 Content-Disposition 標頭。請參閱內容類型

請參閱CVE-2015-5211,以取得與 RFD 相關的其他建議。

可取用的媒體類型

您可以根據請求的 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_VALUEAPPLICATION_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_VALUEAPPLICATION_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
您可以使用標頭條件來匹配 Content-TypeAccept,但最好改用 consumesproduces

HTTP HEAD, OPTIONS

@GetMapping(以及 @RequestMapping(method=HttpMethod.GET))透明地支援 HTTP HEAD 以進行請求映射。控制器方法不需要變更。應用於 jakarta.servlet.http.HttpServlet 的回應包裝器確保 Content-Length 標頭設定為寫入的位元組數(實際上不寫入回應)。

預設情況下,HTTP OPTIONS 是透過將 Allow 回應標頭設定為所有具有相符 URL 模式的 @RequestMapping 方法中列出的 HTTP 方法列表來處理。

對於沒有 HTTP 方法宣告的 @RequestMappingAllow 標頭會設定為 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 方法,而不是使用預設情況下匹配所有 HTTP 方法的 @RequestMapping。如果您需要如何實作組合註解的範例,請查看它們的宣告方式。

@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 介面 是一個對用戶端與伺服器使用保持中立的契約。除了簡化用戶端程式碼之外,在某些情況下,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() 參數,該參數接受類似於用戶端 @RequestMapping(headers={}) 中的 "name=value" 形式的配對。在伺服器端,這擴展到 @RequestMapping 支援的完整語法。