映射请求

本节讨论带注解控制器的请求映射。

@RequestMapping

您可以使用 @RequestMapping 注解将请求映射到控制器方法。它具有各种属性,可以按 URL、HTTP 方法、请求参数、标头和媒体类型进行匹配。您可以在类级别使用它来表达共享映射,或者在方法级别使用它来缩小到特定的端点映射。

还有 @RequestMapping 的 HTTP 方法特定快捷方式变体

  • @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 模式进行映射。有两种选择

  • PathPattern — 一个预解析的模式,与作为 PathContainer 预解析的 URL 路径匹配。此解决方案专为 Web 使用而设计,可以有效地处理编码和路径参数,并高效地匹配。

  • AntPathMatcher — 将字符串模式与字符串路径匹配。这是原始解决方案,也用于 Spring 配置中选择类路径、文件系统和其他位置的资源。它效率较低,字符串路径输入对于有效处理编码和其他 URL 问题是一个挑战。

PathPattern 是 Web 应用程序的推荐解决方案,也是 Spring WebFlux 中的唯一选择。它从版本 5.3 开始启用用于 Spring MVC,并且从版本 6.0 开始默认启用。有关路径匹配选项的自定义,请参阅 MVC 配置

PathPattern 支持与 AntPathMatcher 相同的模式语法。此外,它还支持捕获模式,例如 {*spring},用于匹配路径末尾的 0 个或多个路径段。PathPattern 还限制了 ** 用于匹配多个路径段的使用,使其仅允许在模式末尾使用。这消除了在为给定请求选择最佳匹配模式时出现歧义的许多情况。有关完整的模式语法,请参阅 PathPatternAntPathMatcher

一些示例模式

  • "/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。默认情况下支持简单类型(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/**)被认为比没有双通配符的其他模式更不具体。

有关完整详细信息,请按照上述链接访问模式比较器。

后缀匹配

从 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 攻击,在渲染响应主体之前,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_VALUEAPPLICATION_XML_VALUE

可生成媒体类型

您可以根据 Accept 请求头和控制器方法生成的 content type 列表缩小请求映射范围,如下例所示

  • 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 方法,而不是使用 @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 接口是针对客户端与服务器使用情况的中立契约。除了简化客户端代码之外,在某些情况下,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的列表。