Spring Web MVC:注解控制器(Annotated Controllers)

https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller.html

Spring MVC 提供了一个基于注解的编程模型,其中 @Controller@RestController 组件使用注解来表达请求映射、请求输入、异常处理等。注解控制器具有灵活的方法签名,并且无需扩展基类或实现特定的接口。以下示例展示了一个通过注解定义的控制器:

@Controller
public class HelloController {

	@GetMapping("/hello")
	public String handle(Model model) {
		model.addAttribute("message", "Hello World!");
		return "index";
	}
}

在前面的例子中,该方法接受一个 Model 并返回一个作为 String 的视图名称,但还有许多其他选项,它们将在本章后面进行解释。

声明

可以使用 Servlet 的 WebApplicationContext 中的标准 Spring bean 定义来定义控制器 beans。@Controller 注解允许自动检测,这与 Spring 在类路径中检测 @Component 类并为它们自动注册 bean 定义的通用支持相一致。它还作为注解类的标记,表明其作为 Web 组件的角色。

为了启用对这类 @Controller beans 的自动检测,可以在 Java 配置中添加组件扫描,如下所示:

@Configuration
@ComponentScan("org.example.web")
public class WebConfig {

	// ...
}

以下示例展示了与前面示例等价的 XML 配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context
		https://www.springframework.org/schema/context/spring-context.xsd">

	<context:component-scan base-package="org.example.web"/>

	<!-- ... -->

</beans>

@RestController 是一个组合注解,它本身带有 @Controller@ResponseBody 的元注解,用来表示一个控制器,该控制器的每个方法都继承类型级别的 @ResponseBody 注解,因此它直接将内容写入响应体,而不是进行视图解析和使用 HTML 模板进行渲染。

AOP 代理

在某些情况下,可能需要在运行时使用 AOP 代理来装饰控制器。一个例子是,如果选择在控制器上直接使用 @Transactional 注解。在这种情况下,对于控制器来说,建议使用基于类的代理。当直接在控制器上使用这样的注解时,这会自动发生。

如果控制器实现了接口,并且需要 AOP 代理,可能需要显式配置基于类的代理。例如,在使用 @EnableTransactionManagement 时,可以更改为 @EnableTransactionManagement(proxyTargetClass = true),而在使用 <tx:annotation-driven/> 时,可以更改为 <tx:annotation-driven proxy-target-class="true"/>

请注意,从 6.0 版本开始,使用接口代理时,Spring MVC 不再仅基于接口上的类型级别 @RequestMapping 注解来检测控制器。请启用基于类的代理,否则接口上也必须有一个 @Controller 注解。

请求映射

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

@RequestMapping

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

@RequestMapping 还有针对特定 HTTP 方法的快捷方式变体,包括:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

这些快捷方式之所以被提供,是因为可以说大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用 @RequestMapping,后者默认情况下匹配所有 HTTP 方法。在类级别,仍然需要 @RequestMapping 来表示共享映射。

@RequestMapping 不能与在同一元素(类、接口或方法)上声明的其他 @RequestMapping 注解一起使用。如果在同一元素上检测到多个 @RequestMapping 注解,将会记录警告,并且仅使用第一个映射。这也适用于组合的 @RequestMapping 注解,如 @GetMapping@PostMapping 等。

以下示例包含了类型和方法级别的映射:

@RestController
@RequestMapping("/persons")
class PersonController {

	@GetMapping("/{id}")
	public Person getPerson(@PathVariable Long id) {
		// ...
	}

	@PostMapping
	@ResponseStatus(HttpStatus.CREATED)
	public void add(@RequestBody Person person) {
		// ...
	}
}

URI 模式

@RequestMapping 方法可以使用 URL 模式进行映射。有两种替代方法:

  • PathPattern — 这是一个预解析的模式,用于与同样被预解析为 PathContainer 的 URL 路径进行匹配。专为 Web使用而设计,这种解决方案可以有效地处理编码和路径参数,并实现高效的匹配。
  • AntPathMatcher — 将字符串模式与字符串路径进行匹配。这是原始的解决方案,也用于 Spring配置中,以在类路径、文件系统和其他位置选择资源。它的效率较低,且字符串路径输入对于有效处理 URL 的编码和其他问题是一个挑战。

PathPattern 是 Web 应用程序的推荐解决方案,并且是 Spring WebFlux 中的唯一选择。从 Spring MVC 的 5.3 版本开始,就启用了 PathPattern 的使用,而从 6.0 版本开始,它默认启用。

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

一些示例模式:

  • "/resources/ima?e.png" - 在路径段中匹配一个字符
  • "/resources/*.png" - 在路径段中匹配零个或多个字符
  • "/resources/**" - 匹配多个路径段
  • "/projects/{project}/versions" - 匹配一个路径段并将其捕获为变量
  • "/projects/{project:[a-z]+}/versions" - 使用正则表达式匹配并捕获变量

获的 URI 变量可以通过 @PathVariable 访问。例如:

@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
	// ...
}

可以在类和方法级别声明 URI 变量,如下例所示:

@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

	@GetMapping("/pets/{petId}")
	public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
		// ...
	}
}

URI 变量会自动转换为适当的类型,如果转换失败则会抛出 TypeMismatchException 异常。默认情况下,支持简单的类型(如 intlongDate 等),并且可以注册对其他任何数据类型的支持。

可以显式地为 URI 变量命名(例如,使用 @PathVariable("customId")),但如果变量名相同且代码使用 -parameters 编译器标志编译,则可以省略这一细节。

语法 {varName:regex} 用于声明带有正则表达式的 URI 变量,其语法格式为 {varName:regex}。例如,对于 URL "/spring-web-3.0.5.jar",以下方法将提取出名称、版本和文件扩展名:

@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
	// ...
}

URI 路径模式也可以包含内嵌的 ${…} 占位符,这些占位符在启动时通过 PropertySourcesPlaceholderConfigurer 针对本地、系统、环境和其他属性源进行解析。例如,可以使用此功能来根据某些外部配置参数化基础 URL。

模式比较

当多个模式匹配一个 URL 时,必须选择最佳匹配。这是否根据是否启用了解析的 PathPattern 的使用来决定,具体做法如下:

  • PathPattern.SPECIFICITY_COMPARATOR
  • AntPathMatcher.getPatternComparator(String path)

两者都有助于对模式进行排序,将更具体的模式排在前面。如果一个模式包含的 URI 变量数量(每个计为 1)、单通配符数量(每个计为 1)和双通配符数量(每个计为 2)更少,则该模式更具体。如果得分相同,则选择更长的模式。如果得分和长度都相同,则选择 URI 变量数量多于通配符数量的模式。

默认的映射模式(/**)不计入评分,并且总是排在最后。此外,前缀模式(如 /public/**)被认为比其他没有双通配符的模式更不具体。

后缀匹配

从 5.3 版本开始,Spring MVC 默认不再执行以 .* 结尾的模式匹配,这意味着映射到 /person 的控制器将不再隐式地映射到 /person.*。因此,路径扩展名不再用于解释请求的响应内容类型——例如,/person.pdf/person.xml 等。

过去,当浏览器发送难以一致解释的 Accept 请求头时,使用文件扩展名的方式是必要的。然而现在,这不再是必须的,使用 Accept 请求头应该是首选方案。

随着时间的推移,文件扩展名的使用已被证明在多个方面存在问题。当它与 URI 变量、路径参数和 URI 编码结合使用时,可能会导致歧义。基于 URL 的授权和安全性的推理也变得更加困难。

要在 5.3 版本之前的版本中完全禁用路径扩展名的使用,请设置以下属性:

  • useSuffixPatternMatching(false)
  • favorPathExtension(false)

除了通过“Accept”请求头来请求内容类型之外,还有其他方式仍然是有用的,比如在浏览器中直接输入 URL 时。一种安全的替代路径扩展名的方法是使用查询参数策略。如果你必须使用文件扩展名,那么请考虑将它们限制为通过 ContentNegotiationConfigurermediaTypes 属性明确注册的一系列扩展名。

后缀匹配和反射型文件下载(RFD)攻击

反射型文件下载(RFD)攻击与跨站脚本攻击(XSS)类似,都依赖于请求输入(例如查询参数和 URI 变量)在响应中被反射。然而,与将 JavaScript 插入到 HTML 中的 XSS 攻击不同,RFD 攻击依赖于浏览器切换到执行下载操作,并在稍后双击时将其响应当作可执行脚本来处理。

在 Spring MVC 中,@ResponseBodyResponseEntity 方法存在风险,因为它们可以渲染不同的内容类型,而客户端可以通过 URL 路径扩展来请求这些内容类型。禁用后缀模式匹配以及使用路径扩展进行内容协商可以降低风险,但不足以防止 RFD 攻击。

为了防止 RFD 攻击,Spring MVC 在渲染响应体之前,会添加一个 Content-Disposition:inline;filename=f.txt 的头部信息,以建议一个固定且安全的下载文件。这仅在 URL 路径包含既不被视为安全又未明确注册用于内容协商的文件扩展名时才会执行。然而,当直接在浏览器中键入 URL 时,这可能会产生潜在的副作用。

许多常见的路径扩展名默认被视为安全的。具有自定义 HttpMessageConverter 实现的应用程序可以显式地为内容协商注册文件扩展名,以避免为这些扩展名添加 Content-Disposition 头部信息。

可消费媒体类型

你可以根据请求的 Content-Type 来缩小请求映射的范围,如下例所示:

@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
	// ...
}

consumes 属性还支持否定表达式——例如,!text/plain 表示除 text/plain 以外的任何内容类型。

你可以在类级别声明一个共享的 consumes 属性。然而,与大多数其他请求映射属性不同,当在类级别使用时,方法级别的 consumes 属性会覆盖而不是扩展类级别的声明。

MediaType 提供了常用媒体类型的常量,例如 APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE

可生成媒体类型

你可以根据 Accept 请求头部和控制器方法生成的内容类型列表来缩小请求映射的范围,如下例所示:

@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
	// ...
}

媒体类型可以指定字符集。同时,也支持否定表达式——例如,!text/plain 表示除了 "text/plain" 以外的任何内容类型。

你可以在类级别声明一个共享的 produces 属性。然而,与大多数其他请求映射属性不同,当在类级别使用时,方法级别的 produces 属性会覆盖而不是扩展类级别的声明。

MediaType 类为常用的媒体类型提供了常量,例如 APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE

参数、请求头

你可以根据请求参数条件来缩小请求映射的范围。你可以检查请求参数(myParam)是否存在,检查某个参数是否不存在(!myParam),或者检查参数是否具有特定值(myParam=myValue)。以下示例展示了如何检查特定值:

@GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
public void findPet(@PathVariable String petId) {
	// ...
}

你也可以使用相同的方式来处理请求头条件,如下例所示:

@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue")
public void findPet(@PathVariable String petId) {
	// ...
}

你可以使用 headers 条件来匹配 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下注册同一个处理器的不同实例。以下示例展示了如何注册一个处理器方法:

@Configuration
public class MyConfig {

	@Autowired
	public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler)
			throws NoSuchMethodException {

		RequestMappingInfo info = RequestMappingInfo
				.paths("/user/{id}").methods(RequestMethod.GET).build();

		Method method = UserHandler.class.getMethod("getUser", Long.class);

		mapping.registerMapping(info, handler, method);
	}
}

@HttpExchange

虽然 @HttpExchange 的主要目的是通过生成的代理来抽象 HTTP 客户端代码,但放置这些注解的 HTTP 接口是一个与客户端和服务器使用无关的约定。除了简化客户端代码外,在某些情况下,HTTP 接口也可能是服务器方便暴露其 API 以供客户端访问的一种方式。这会导致客户端和服务器之间的耦合度增加,通常对于公共 API 来说不是一个好的选择,但对于内部 API 来说可能正是目标所在。这是 Spring Cloud 中常用的方法,这也是为什么 @HttpExchange 支持作为控制器类中服务器端处理的 @RequestMapping 的替代方案。

例如:

@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@RequestMapping 存在差异。@RequestMapping 能够通过路径模式、HTTP 方法等映射到任意数量的请求,而 @HttpExchange 则声明一个具有具体 HTTP 方法、路径和内容类型的单一端点。

对于方法参数和返回值,通常来说,@HttpExchange支持@RequestMapping所支持的方法参数的一个子集。值得注意的是,它排除了任何特定于服务器端的参数类型。

处理器方法(Handler Methods)

@RequestMapping 处理器方法具有灵活的签名,并可以从一系列受支持的控制器方法参数和返回值中进行选择。

方法参数(Method Arguments)

下表描述了支持的控制器方法参数。对于任何参数,不支持响应式类型。

JDK 8的java.util.Optional被支持作为方法参数,与具有required属性的注解(例如@RequestParam@RequestHeader等)结合使用,等同于required=false

  • WebRequest, NativeWebRequest:对请求参数、请求和会话属性的通用访问,无需直接使用Servlet API。
  • jakarta.servlet.ServletRequest, jakarta.servlet.ServletResponse:选择任何特定的请求或响应类型——例如,ServletRequestHttpServletRequest或Spring的MultipartRequestMultipartHttpServletRequest
  • jakarta.servlet.http.HttpSession:强制要求存在一个会话。因此,这样的参数永远不会为null。请注意,会话访问不是线程安全的。如果允许多个请求并发访问一个会话,请考虑将会话同步标志设置为true
  • jakarta.servlet.http.PushBuilder:Servlet 4.0 push builder API用于编程式HTTP/2资源推送。请注意,根据Servlet规范,如果客户端不支持该HTTP/2特性,注入的PushBuilder实例可以为null
  • java.security.Principal:当前已认证的用户——如果已知,可能是特定的Principal实现类。这个参数不会立即解析,如果它被注解了,这是为了允许自定义解析器在回退到通过HttpServletRequest#getUserPrincipal进行默认解析之前先进行解析。例如,Spring Security的Authentication实现了Principal,并且会通过HttpServletRequest#getUserPrincipal这样注入,除非它还被@AuthenticationPrincipal注解,在这种情况下,它会通过Authentication#getPrincipal被一个自定义的Spring Security解析器解析。
  • HttpMethod:请求的HTTP方法。
  • java.util.Locale:当前的请求区域设置,由最具体的LocaleResolver(实际上是配置的LocaleResolverLocaleContextResolver)确定。
  • java.util.TimeZone + java.time.ZoneId:与当前请求相关联的时区,由LocaleContextResolver确定。
  • java.io.InputStream, java.io.Reader:用于访问Servlet API公开的原始请求体。
  • java.io.OutputStream, java.io.Writer:用于访问Servlet API公开的原始响应体。
  • @PathVariable:用于访问URI模板变量。
  • @MatrixVariable:用于访问URI路径段中的名值对。
  • @RequestParam:用于访问Servlet请求参数,包括多部分文件。参数值转换为声明的方法参数类型。对于简单的参数值,使用@RequestParam是可选的。
  • @RequestHeader:用于访问请求头。头部值转换为声明的方法参数类型。
  • @CookieValue:用于访问cookies。Cookies值转换为声明的方法参数类型。
  • @RequestBody:用于访问HTTP请求体。通过使用HttpMessageConverter实现,将主体内容转换为声明的方法参数类型。
  • HttpEntity<B>:用于访问请求头和请求体。请求体通过HttpMessageConverter进行转换。
  • @RequestPart:于访问multipart/form-data请求中的一个部分,并使用HttpMessageConverter转换该部分的主体。
  • java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap:用于访问在HTML控制器中使用并作为视图渲染的一部分暴露给模板的模型。
  • RedirectAttributes:指定在重定向时使用的属性(即,要附加到查询字符串中)以及在重定向后的请求之前临时存储的flash属性。
  • @ModelAttribute:用于访问模型中现有的属性(如果不存在则实例化),并应用数据绑定和验证。
  • Errors, BindingResult:用于访问来自命令对象(即@ModelAttribute参数)的验证和数据绑定的错误,或者来自@RequestBody@RequestPart参数验证的错误。必须在被验证的方法参数之后立即声明一个ErrorsBindingResult参数。
  • SessionStatus + 类级别的 @SessionAttributes:用于标记表单处理完成,这将触发通过类级别的@SessionAttributes注解声明的会话属性的清理。
  • UriComponentsBuilder:用于准备相对于当前请求的主机、端口、协议、上下文路径和servlet映射的字面部分的URL。
  • @SessionAttribute:用于访问任何会话属性,与因类级别@SessionAttributes声明而存储在会话中的模型属性形成对比。
  • @RequestAttribute:用于访问请求属性。
  • 任何其他参数:如果方法参数与本表中之前的任何值都不匹配,并且它是一个简单类型(由BeanUtils#isSimpleProperty确定),则它被解析为@RequestParam。否则,它将被解析为@ModelAttribute

返回值

下表描述了支持的控制器方法返回值。所有返回值都支持响应式类型。

  • @ResponseBody:返回值通过HttpMessageConverter实现转换,并写入响应。
  • HttpEntity<B>, ResponseEntity<B>:回值指定了完整的响应(包括HTTP头和主体),将通过HttpMessageConverter实现进行转换,并写入响应。
  • HttpHeaders:用于返回带有头部信息且无主体的响应。
  • ErrorResponse:于渲染带有主体详细信息的RFC 7807错误响应。
  • ProblemDetail:用于渲染带有主体详细信息的RFC 7807错误响应。
  • String:一个视图名称,将被ViewResolver实现解析,并与隐式模型一起使用——通过命令对象和@ModelAttribute方法确定。处理程序方法也可以通过声明一个Model参数来以编程方式丰富模型。
  • View:一个View实例,用于与隐式模型一起渲染——通过命令对象和@ModelAttribute方法确定。处理程序方法也可以通过声明一个Model参数来以编程方式丰富模型。
  • java.util.Map, org.springframework.ui.Model:要添加到隐式模型的属性,视图名称通过RequestToViewNameTranslator隐式确定。
  • @ModelAttribute:要添加到模型的属性,视图名称通过RequestToViewNameTranslator隐式确定。@ModelAttribute是可选的。
  • ModelAndView 对象:要使用的视图和模型属性,以及可选的响应状态。
  • void:如果一个方法具有void返回类型(或null返回值),并且它还具有ServletResponse参数、OutputStream参数,或者有@ResponseStatus注解,那么该方法被认为已经完全处理了响应。如果控制器进行了ETaglastModified时间戳的正向检查,情况也是如此。如果上述都不成立,对于REST控制器,void返回类型也可以表示“无响应体”,对于HTML控制器,则表示选择默认视图名称。
  • DeferredResult<V>:从任何线程异步地产生上述任何返回值——例如,作为某个事件或回调的结果。
  • Callable<V>:Spring MVC管理的线程中异步地产生上述任何返回值。
  • ListenableFuture<V>, java.util.concurrent.CompletionStage<V>, java.util.concurrent.CompletableFuture<V>:作为DeferredResult的替代方案,作为一种便利(例如,当底层服务返回其中之一时)。
  • ResponseBodyEmitter, SseEmitter:异步发出对象流,以便使用HttpMessageConverter实现写入响应。也支持作为ResponseEntity的主体。
  • StreamingResponseBody:异步写入响应的OutputStream。也支持作为ResponseEntity的主体。
  • 通过ReactiveAdapterRegistry注册的Reactor和其他响应类型:单一值类型,例如Mono,可以与返回DeferredResult相比较。多值类型,例如Flux,可以根据请求的媒体类型被视为流,例如"text/event-stream"、“application/json+stream”,或者被收集到List中并渲染为单一值。
  • 其它返回值:如果以任何其他方式返回的值仍未解决,则将其视为模型属性,除非它是BeanUtils#isSimpleProperty确定的简单类型,在这种情况下它仍然未解决。

类型转换

一些表示基于String的请求输入的注解控制器方法参数(如@RequestParam@RequestHeader@PathVariable@MatrixVariable@CookieValue)如果参数被声明为String以外的类型,则可能需要进行类型转换。

对于这些情况,基于配置的转换器自动应用类型转换。默认情况下,支持简单类型(intlongDate等)。可以通过WebDataBinder或通过向FormattingConversionService注册Formatters来自定义类型转换。

类型转换中的一个实际问题是空String源值的处理。如果作为类型转换结果的值为null,则这样的值被视为缺失。这可能适用于LongUUID和其他目标类型。如果想允许注入null,可以在参数注解上使用required标志,或者声明参数为@Nullable

从5.3版本开始,即使在类型转换之后,也将强制执行非空参数。如果你的处理程序方法打算接受null值,那么要么将参数声明为@Nullable,要么在相应的@RequestParam等注解中标记为required=false。这是最佳实践,也是解决5.3升级中遇到的回归问题的推荐解决方案。

或者,可以特别处理例如在必需的@PathVariable情况下产生的MissingPathVariableException。转换后的null值将被视为原始值为空,因此将抛出相应的Missing...​Exception变体。

矩阵变量(Matrix Variables)

RFC 3986讨论了路径段中的名称-值对。在Spring MVC中,根据Tim Berners-Lee的“旧帖子”将这些称为“矩阵变量”,但它们也可以被称为URI路径参数。

矩阵变量可以出现在任何路径段中,每个变量由分号分隔,多个值由逗号分隔(例如,/cars;color=red,green;year=2012)。通过重复变量名也可以指定多个值(例如,color=red;color=green;color=blue)。

如果URL预期包含矩阵变量,控制器方法的请求映射必须使用URI变量来掩盖该变量内容,并确保无论矩阵变量的顺序和存在如何,请求都能成功匹配。以下示例使用了矩阵变量:

// 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
}

要获取所有矩阵变量,可以使用MultiValueMap,如下例所示:

// 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" : 22, "s" : 23]
}

请注意,你需要启用矩阵变量的使用。在MVC Java配置中,需要通过路径匹配设置一个UrlPathHelper,其中removeSemicolonContent=false。在MVC XML命名空间中,可以设置<mvc:annotation-driven enable-matrix-variables="true"/>

@RequestParam

可以使用@RequestParam注解将Servlet请求参数(即查询参数或表单数据)绑定到控制器中的方法参数。

以下示例展示了如何做到这一点:

@Controller
@RequestMapping("/pets")
public class EditPetForm {

	// ...

	@GetMapping
	public String setupForm(@RequestParam("petId") int petId, Model model) {
		Pet pet = this.clinic.loadPet(petId);
		model.addAttribute("pet", pet);
		return "petForm";
	}

	// ...

}

默认情况下,使用此注解的方法参数是必需的,但可以通过将@RequestParam注解的required标志设置为false或通过使用java.util.Optional包装器声明参数来指定方法参数是可选的。

如果目标方法参数类型不是String,将自动应用类型转换。

将参数类型声明为数组或列表可以解析同一参数名称的多个参数值。

@RequestParam注解被声明为Map<String, String>MultiValueMap<String, String>,并且在注解中没有指定参数名称时,该映射将使用每个给定参数名称的请求参数值进行填充。

使用@RequestParam是可选的(例如,设置其属性)。默认情况下,任何简单值类型(由BeanUtils#isSimpleProperty确定)的参数,如果没有被任何其他参数解析器解析,都会被视为使用了@RequestParam注解。

@RequestHeader

可以使用@RequestHeader注解将请求头绑定到控制器中的方法参数。

考虑以下请求,带有头:

Host                    localhost:8080
Accept                  text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language         fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding         gzip,deflate
Accept-Charset          ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive              300

以下示例获取Accept-EncodingKeep-Alive头的值:

@GetMapping("/demo")
public void handle(
		@RequestHeader("Accept-Encoding") String encoding,
		@RequestHeader("Keep-Alive") long keepAlive) {
	//...
}

如果目标方法参数类型不是String,将自动应用类型转换。

@RequestHeader注解用于Map<String, String>MultiValueMap<String, String>HttpHeaders参数时,该映射将使用所有头值进行填充。

内置支持可用于将逗号分隔的字符串转换为字符串数组或集合,或者转换为类型转换系统已知的其他类型。例如,用@RequestHeader("Accept")注解的方法参数可以是String类型,也可以是String[]List类型。

@CookieValue

可以使用@CookieValue注解将HTTP cookie的值绑定到控制器中的方法参数。

考虑一个带有以下cookie的请求:

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

以下示例展示了如何获取cookie值:

@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) {
	//...
}

如果目标方法参数类型不是String,将自动应用类型转换。

@ModelAttribute

@ModelAttribute方法参数注解将请求参数绑定到模型对象。例如:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) {
	// method logic...
}

Pet实例可能是:

  • 从模型中访问的,它可能已经被@ModelAttribute方法添加。
  • 如果模型属性在类级别的@SessionAttributes注解中列出,则可以从HTTP会话中访问。
  • 如果模型属性名称与请求值的名称匹配,例如路径变量或请求参数,则可以通过转换器获得。
  • 通过默认构造函数实例化。
  • 通过带有参数的“主构造函数”实例化,这些参数与Servlet请求参数匹配。参数名称通过字节码中运行时保留的参数名称确定。

如上所述,如果模型属性名称与请求值的名称(例如路径变量或请求参数)匹配,并且存在兼容的Converter<String, T>,则可以使用Converter<String, T>来获取模型对象。在下面的例子中,模型属性名称account匹配URI路径变量account,并且有一个已注册的Converter<String, Account>,它可能会从持久化存储中检索它:

@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) {
	// ...
}

默认情况下,构造函数和属性数据绑定都会被应用。然而,模型对象设计需要仔细考虑,出于安全原因,建议使用专门为Web绑定定制的对象,或者仅应用构造函数绑定。如果仍然必须使用属性绑定,则应设置allowedFields模式以限制可以设置的属性。

使用构造函数绑定时,可以通过@BindParam注解自定义请求参数名称。例如:

class Account {

    private final String firstName;

	public Account(@BindParam("first-name") String firstName) {
		this.firstName = firstName;
	}
}

BindParam也可以放在与构造函数参数对应的字段上。虽然@BindParam是默认支持的,但也可以通过在DataBinder上设置DataBinder.NameResolver来使用不同的注解。

在某些情况下,可能需要在没有数据绑定的情况下访问模型属性。对于这种情况,可以将Model注入控制器并直接访问它,或者设置@ModelAttribute(binding=false),如下例所示:

@ModelAttribute
public AccountForm setUpForm() {
	return new AccountForm();
}

@ModelAttribute
public Account findAccount(@PathVariable String accountId) {
	return accountRepository.findOne(accountId);
}

@PostMapping("update")
public String update(AccountForm form, BindingResult result,
		@ModelAttribute(binding=false) Account account) {
	// ...
}

如果数据绑定导致错误,默认情况下会引发MethodArgumentNotValidException,但还可以在@ModelAttribute旁边添加一个BindingResult参数,以便在控制器方法中处理此类错误。例如:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {
	if (result.hasErrors()) {
		return "petForm";
	}
	// ...
}

可以通过添加jakarta.validation.Valid注解或Spring的@Validated注解,在数据绑定后自动应用验证。例如:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) {
	if (result.hasErrors()) {
		return "petForm";
	}
	// ...
}

如果@ModelAttribute之后没有BindingResult参数,那么将使用验证错误引发MethodArgumentNotValueException。然而,如果由于其他参数具有@jakarta.validation.Constraint注解而应用了方法验证,那么将引发HandlerMethodValidationException

使用@ModelAttribute是可选的。默认情况下,任何不是简单值类型(由BeanUtils#isSimpleProperty确定)且未通过其他参数解析器解析的参数都被视为隐式的@ModelAttribute

当使用GraalVM编译到本地镜像时,上述描述的隐式@ModelAttribute支持不允许正确的提前运行时相关的数据绑定反射提示的推断。因此,建议在GraalVM本地镜像中使用的方法参数上显式地用@ModelAttribute注解。

@SessionAttributes

@SessionAttributes用于在HTTP Servlet会话中存储请求之间的模型属性。它是一种类型级别的注解,声明了特定控制器使用的会话属性。这通常列出了应该在会话中透明存储的模型属性的名称或模型属性的类型,以便后续请求访问。

以下示例使用了@SessionAttributes注解:

@Controller
@SessionAttributes("pet")
public class EditPetForm {
	// ...
}

在第一次请求时,当向模型中添加一个名为“pet”的模型属性时,它会自动升级到HTTP Servlet会话中并保存。它会一直保留在那里,直到另一个控制器方法使用SessionStatus方法参数来清除存储,如下例所示:
@Controller

@SessionAttributes("pet")
public class EditPetForm {

	// ...

	@PostMapping("/pets/{id}")
	public String handle(Pet pet, BindingResult errors, SessionStatus status) {
		if (errors.hasErrors) {
			// ...
		}
		status.setComplete();
		// ...
	}
}

@SessionAttribute

如果需要访问全局管理(即,在控制器外部——例如,通过过滤器)的预先存在的会话属性,并且这些属性可能存在也可能不存在,可以在方法参数上使用@SessionAttribute注解,如下例所示:

@RequestMapping("/")
public String handle(@SessionAttribute User user) {
	// ...
}

对于需要添加或移除会话属性的用例,请考虑将org.springframework.web.context.request.WebRequestjakarta.servlet.http.HttpSession注入到控制器方法中。

对于在控制器工作流中将会话属性临时存储在会话中,请考虑使用@SessionAttributes

@RequestAttribute

类似于@SessionAttribute,你可以使用@RequestAttribute注解来访问之前创建的预存在请求中的属性(例如,由Servlet过滤器或HandlerInterceptor创建):

@GetMapping("/")
public String handle(@RequestAttribute Client client) {
	// ...
}

重定向属性(Redirect Attributes)

默认情况下,所有模型属性都被视为在重定向URL中作为URI模板变量进行暴露。对于剩余的属性,那些是基本类型或基本类型集合或数组的属性会自动作为查询参数附加到URL中。

将基本类型属性作为查询参数附加可能是期望的结果,如果模型实例是专门为重定向准备的。然而,在带有注解的控制器中,模型可能包含为了渲染目的而添加的额外属性(例如,下拉字段的值)。为了避免这些属性可能出现在URL中的情况,一个@RequestMapping方法可以声明一个类型为RedirectAttributes的参数,并使用它来指定要提供给RedirectView的确切属性。如果该方法进行重定向,则会使用RedirectAttributes的内容。否则,将使用模型的内容。

RequestMappingHandlerAdapter提供了一个名为ignoreDefaultModelOnRedirect的标志,你可以使用它来指示,如果控制器方法进行重定向,则不应使用默认Model的内容。相反,控制器方法应该声明一个类型为RedirectAttributes的属性,或者如果没有这样做,则不应将任何属性传递给RedirectView。为了保持向后兼容性,MVC命名空间和MVC Java配置都将此标志设置为false。然而,对于新应用,建议将其设置为true

请注意,在扩展重定向URL时,当前请求中的URI模板变量会自动可用,你不需要通过ModelRedirectAttributes显式地添加它们。以下示例展示了如何定义重定向:

@PostMapping("/files/{path}")
public String upload(...) {
	// ...
	return "redirect:files/{path}";
}

将数据传递给重定向目标的另一种方法是使用flash 属性。与其他重定向属性不同,flash 属性保存在HTTP会话中(因此不会出现在URL中)。

Flash属性

Flash属性提供了一种方式,使得一个请求可以存储属性以供另一个请求使用。这在重定向时最为常见,例如Post-Redirect-Get模式。Flash属性在重定向之前会被暂时保存(通常保存在会话中),以便在重定向后的请求中可用,并会立即被移除。

Spring MVC支持Flash属性主要有两个主要的抽象概念。FlashMap用于保存Flash属性,而FlashMapManager则用于存储、检索和管理FlashMap实例。

Flash属性的支持总是“开启”的,不需要显式启用。但是,如果没有使用,它不会导致HTTP会话的创建。在每个请求中,都有一个“输入”FlashMap,它包含从上一个请求传递的属性(如果有的话),以及一个“输出”FlashMap,它包含要保存以供后续请求使用的属性。通过RequestContextUtils中的静态方法,可以在Spring MVC中的任何地方访问这两个FlashMap实例。

带有注解的控制器通常不需要直接与FlashMap交互。相反,一个@RequestMapping方法可以接收一个类型为RedirectAttributes的参数,并使用它来为一个重定向场景添加Flash属性。通过RedirectAttributes添加的Flash属性会自动传播到“输出”FlashMap。类似地,在重定向之后,来自“输入”FlashMap的属性会自动添加到服务目标URL的控制器的Model中。

将请求与Flash属性进行匹配

Flash属性的概念存在于许多其他Web框架中,但已经证明有时会暴露于并发问题。这是因为根据定义,Flash属性应该被存储直到下一个请求。然而,“下一个”请求可能并不是预期的接收者,而是另一个异步请求(例如轮询或资源请求),在这种情况下,Flash属性会被过早地移除。

为了减少这类问题的可能性,RedirectView会自动将FlashMap实例“标记”为目标重定向URL的路径和查询参数。反过来,默认的FlashMapManager在查找“输入”FlashMap时会将这些信息与传入的请求进行匹配。

这并不能完全消除并发问题的可能性,但通过重定向URL中已有的信息可以大大减少这种可能性。因此,建议主要在重定向场景中使用Flash属性。

Multipart

启用MultipartResolver之后,具有multipart/form-data内容的POST请求将被解析,并可作为常规请求参数进行访问。以下示例访问一个常规表单字段和一个上传的文件:

@Controller
public class FileUploadController {

	@PostMapping("/form")
	public String handleFormUpload(@RequestParam("name") String name,
			@RequestParam("file") MultipartFile file) {

		if (!file.isEmpty()) {
			byte[] bytes = file.getBytes();
			// store the bytes somewhere
			return "redirect:uploadSuccess";
		}
		return "redirect:uploadFailure";
	}
}

将参数类型声明为List<MultipartFile>允许对相同的参数名称解析多个文件。

@RequestParam注解被声明为Map<String, MultipartFile>MultiValueMap<String, MultipartFile>类型,并且没有在注解中指定参数名称时,该映射将被填充为每个给定参数名称的Multipart文件。

在使用Servlet进行multipart解析时,还可以将方法参数或集合值类型声明为jakarta.servlet.http.Part,而不是Spring的MultipartFile

还可以将multipart内容作为数据绑定到命令对象的一部分。例如,前面示例中的表单字段和文件可以是表单对象上的字段,如下例所示:

class MyForm {

	private String name;

	private MultipartFile file;

	// ...
}

@Controller
public class FileUploadController {

	@PostMapping("/form")
	public String handleFormUpload(MyForm form, BindingResult errors) {
		if (!form.getFile().isEmpty()) {
			byte[] bytes = form.getFile().getBytes();
			// store the bytes somewhere
			return "redirect:uploadSuccess";
		}
		return "redirect:uploadFailure";
	}
}

在RESTful服务场景中,Multipart请求也可以由非浏览器客户端提交。以下示例显示了包含JSON的文件:

POST /someUrl
Content-Type: multipart/mixed

--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit

{
	"name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...

可以使用@RequestParam注解作为String访问“元数据”部分,但可能希望从JSON反序列化它(类似于@RequestBody)。在使用HttpMessageConverter进行转换后,使用@RequestPart注解来访问multipart:

@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata,
		@RequestPart("file-data") MultipartFile file) {
	// ...
}

可以将@RequestPartjakarta.validation.Valid结合使用,或者使用Spring的@Validated注解,两者都会应用标准的Bean验证。默认情况下,验证错误会导致MethodArgumentNotValidException异常,该异常会被转换为400(BAD_REQUEST)响应。或者,可以通过ErrorsBindingResult参数在控制器内部本地处理验证错误,如下例所示:

@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata, Errors errors) {
	// ...
}

如果方法验证因为其他参数有@Constraint注解而适用,那么会抛出HandlerMethodValidationException异常。

@RequestBody

可以使用@RequestBody注解来读取请求体,并通过HttpMessageConverter将其反序列化为一个对象。以下示例使用了@RequestBody参数:

@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
	// ...
}

可以使用MVC配置的Message Converters选项来配置或自定义消息转换。

可以将@RequestBodyjakarta.validation.Valid或Spring的@Validated注解结合使用,两者都会触发标准Bean验证。默认情况下,验证错误会导致MethodArgumentNotValidException异常,该异常会被转换为400(BAD_REQUEST)响应。或者,可以通过ErrorsBindingResult参数在控制器内部本地处理验证错误,如下例所示:

@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, Errors errors) {
	// ...
}

如果方法验证因为其他参数带有@Constraint注解而适用,那么将会抛出HandlerMethodValidationException异常。

HttpEntity

HttpEntity 与使用 @RequestBody 在很大程度上是相同的,但它基于一个容器对象,该对象公开了请求头和请求体。以下列表显示了一个示例:

@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
	// ...
}

@ResponseBody

可以在方法上使用@ResponseBody注解,以便通过HttpMessageConverter将返回值序列化为响应体。以下列表显示了一个示例:

@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
	// ...
}

@ResponseBody 注解也可以在类级别上使用,在这种情况下,它会被所有的控制器方法继承。这就是 @RestController 的作用,它其实只是一个元注解,标记有 @Controller@ResponseBody

可以将 @ResponseBody 注解与响应式类型(reactive types)一起使用。

可以使用MVC配置中的Message Converters选项来配置或自定义消息转换。

可以将@ResponseBody方法与JSON序列化视图结合起来使用。

ResponseEntity

ResponseEntity@ResponseBody类似,但它还包括状态和头部信息。例如:

@GetMapping("/something")
public ResponseEntity<String> handle() {
	String body = ... ;
	String etag = ... ;
	return ResponseEntity.ok().eTag(etag).body(body);
}

Spring MVC支持使用单个响应式类型来异步生成ResponseEntity,以及用于响应体的单个和多值响应式类型。这允许以下类型的异步响应:

  • ResponseEntity<Mono<T>>ResponseEntity<Flux<T>> 可以立即知道响应的状态和头部信息,而响应体则在稍后的某个时间点异步提供。如果响应体包含0到1个值,则使用Mono;如果它可以产生多个值,则使用Flux
  • Mono<ResponseEntity<T>> 可以在稍后的某个时间点异步地提供响应状态、头部和响应体,这三者一应俱全。这允许响应状态和头部根据异步请求处理的结果而变化。

Jackson JSON

Spring为Jackson JSON库提供了支持。

JSON视图

Spring MVC为Jackson的序列化视图提供了内置支持,这允许仅渲染对象中的所有字段的子集。要与@ResponseBodyResponseEntity控制器方法一起使用它,可以使用Jackson的@JsonView注解来激活一个序列化视图类,如下例所示:

@RestController
public class UserController {

	@GetMapping("/user")
	@JsonView(User.WithoutPasswordView.class)
	public User getUser() {
		return new User("eric", "7!jd#h23");
	}
}

public class User {

	public interface WithoutPasswordView {};
	public interface WithPasswordView extends WithoutPasswordView {};

	private String username;
	private String password;

	public User() {
	}

	public User(String username, String password) {
		this.username = username;
		this.password = password;
	}

	@JsonView(WithoutPasswordView.class)
	public String getUsername() {
		return this.username;
	}

	@JsonView(WithPasswordView.class)
	public String getPassword() {
		return this.password;
	}
}

@JsonView允许一个视图类的数组,但每个控制器方法只能指定一个。如果需要激活多个视图,可以使用一个复合接口。

如果希望以编程方式执行上述操作,而不是声明@JsonView注解,可以将返回值包装在MappingJacksonValue中,并使用它来提供序列化视图:

@RestController
public class UserController {

	@GetMapping("/user")
	public MappingJacksonValue getUser() {
		User user = new User("eric", "7!jd#h23");
		MappingJacksonValue value = new MappingJacksonValue(user);
		value.setSerializationView(User.WithoutPasswordView.class);
		return value;
	}
}

对于依赖于视图解析的控制器,可以将序列化视图类添加到模型中,如下例所示:

@Controller
public class UserController extends AbstractController {

	@GetMapping("/user")
	public String getUser(Model model) {
		model.addAttribute("user", new User("eric", "7!jd#h23"));
		model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class);
		return "userView";
	}
}

模型(Model)

可以使用@ModelAttribute注解:

  • @RequestMapping方法的参数上使用@ModelAttribute注解,可以从模型中创建或访问一个对象,并通过WebDataBinder将其绑定到请求上。
  • 作为@Controller@ControllerAdvice类中的方法级别注解,@ModelAttribute注解有助于在任何@RequestMapping方法调用之前初始化模型。
  • @RequestMapping方法上使用@ModelAttribute注解,表示其返回值是一个模型属性。

本部分将讨论@ModelAttribute方法,也就是上面列表中的第二项。一个控制器可以有任意数量的@ModelAttribute方法。在同一个控制器中,所有这样的方法都会在@RequestMapping方法之前被调用。@ModelAttribute方法还可以通过@ControllerAdvice跨控制器共享。

@ModelAttribute方法具有灵活的方法签名。它们支持许多与@RequestMapping方法相同的参数,除了@ModelAttribute本身或与请求体相关的任何内容。

以下示例展示了一个@ModelAttribute方法:

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
	model.addAttribute(accountRepository.findAccount(number));
	// add more ...
}

以下示例仅添加了一个属性:

@ModelAttribute
public Account addAccount(@RequestParam String number) {
	return accountRepository.findAccount(number);
}

当没有显式指定名称时,会根据对象类型选择一个默认名称。始终可以使用重载的addAttribute方法或通过@ModelAttribute上的name属性(对于返回值)来分配显式名称。

还可以在@RequestMapping方法上使用@ModelAttribute作为方法级别的注解,在这种情况下,@RequestMapping方法的返回值将被解释为模型属性。这通常不是必需的,因为在HTML控制器中这是默认行为,除非返回值是一个字符串,否则它将被解释为视图名称。@ModelAttribute还可以自定义模型属性名称,如下例所示:

@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
	// ...
	return account;
}

@InitBinder

@Controller或@ControllerAdvice类可以拥有@InitBinder方法来初始化WebDataBinder实例,这些实例可以:

  • 将请求参数绑定到模型对象。
  • 将请求中的值从字符串转换为对象属性类型。
  • 在渲染HTML表单时,将模型对象的属性格式化为字符串。

在@Controller中,DataBinder的自定义设置仅适用于控制器内部,或者甚至可以通过注解名称引用特定模型属性。在@ControllerAdvice中,自定义设置可以应用于所有控制器或控制器的一个子集。

可以在DataBinder中注册PropertyEditor、Converter和Formatter组件以进行类型转换。另外,还可以使用MVC配置在全局共享的FormattingConversionService中注册Converter和Formatter组件。

@InitBinder方法可以有与@RequestMapping方法相同的许多参数,但显著的区别在于它不包括@ModelAttribute。通常,这样的方法有一个WebDataBinder参数(用于注册)和一个void返回值,例如:

@Controller
public class FormController {

	@InitBinder
	public void initBinder(WebDataBinder binder) {
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
		dateFormat.setLenient(false);
		binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
	}

	// ...
}

另外,当通过共享的FormattingConversionService使用基于Formatter的设置时,可以重用相同的方法并注册特定于控制器的Formatter实现,如下例所示:

@Controller
public class FormController {

	@InitBinder
	protected void initBinder(WebDataBinder binder) {
		binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
	}

	// ...
}

模型设计(Model Design)

Web请求的数据绑定涉及将请求参数绑定到模型对象。默认情况下,请求参数可以绑定到模型对象的任何公共属性,这意味着恶意客户端可以为模型对象图中存在的属性提供额外的值,即使这些属性并不预期被设置。这就是为什么模型对象的设计需要仔细考虑的原因。

模型对象及其嵌套的对象图有时也被称为命令对象、表单支持对象或POJO(Plain Old Java Object,简单的老式Java对象)。

一个好的做法是使用专用的模型对象,而不是将JPA或Hibernate等域模型实体暴露给Web数据绑定。例如,在更改电子邮件地址的表单上,创建一个ChangeEmailForm模型对象,只声明输入所需的属性:

public class ChangeEmailForm {

	private String oldEmailAddress;
	private String newEmailAddress;

	public void setOldEmailAddress(String oldEmailAddress) {
		this.oldEmailAddress = oldEmailAddress;
	}

	public String getOldEmailAddress() {
		return this.oldEmailAddress;
	}

	public void setNewEmailAddress(String newEmailAddress) {
		this.newEmailAddress = newEmailAddress;
	}

	public String getNewEmailAddress() {
		return this.newEmailAddress;
	}

}

另一个好的做法是使用构造函数绑定,它只使用构造函数参数所需的请求参数,而忽略任何其他输入。这与属性绑定相反,属性绑定默认会绑定所有存在匹配属性的请求参数。

如果既没有专门的模型对象,也没有构造器绑定足够使用,而你必须使用属性绑定,建议在WebDataBinder上注册allowedFields模式(区分大小写),以防止设置意外的属性。例如:

@Controller
public class ChangeEmailController {

	@InitBinder
	void initBinder(WebDataBinder binder) {
		binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
	}

	// @RequestMapping methods, etc.

}

你也可以注册disallowedFields模式(不区分大小写)。但是,“allowed”配置优于“disallowed”,因为它更明确,也更不容易出错。

默认情况下,构造器绑定和属性绑定都会被使用。如果你只想使用构造器绑定,你可以通过@InitBinder方法在控制器内部局部地或者在全局范围内通过@ControllerAdvice在WebDataBinder上设置declarativeBinding标志。开启这个标志可以确保只使用构造器绑定,并且除非配置了allowedFields模式,否则不会使用属性绑定。例如:

@Controller
public class MyController {

	@InitBinder
	void initBinder(WebDataBinder binder) {
		binder.setDeclarativeBinding(true);
	}

	// @RequestMapping methods, etc.

}

验证(Validation)

Spring MVC为@RequestMapping方法提供了内置的验证支持,包括使用Java Bean Validation的选项。验证支持在两个层面上起作用。

首先,@ModelAttribute、@RequestBody和@RequestPart方法参数的解析器会在参数带有Jakarta的@Valid或Spring的@Validated注解时执行验证,并在必要时抛出MethodArgumentNotValidException异常。或者,你可以在控制器方法中通过在已验证的参数之后立即添加一个Errors或BindingResult方法参数来处理错误。

第二,如果存在Java Bean Validation,并且任何方法参数有@Constraint注解,那么将应用方法验证,并在必要时抛出HandlerMethodValidationException异常。在这种情况下,你仍然可以添加一个Errors或BindingResult方法参数来在控制器方法中处理验证错误,但如果其他方法参数有验证错误,则会抛出HandlerMethodValidationException异常。如果方法被@Valid或@Constraint注解标记,那么方法验证也可以应用于返回值。

你可以通过WebMvc配置全局地配置一个Validator,或者通过@Controller或@ControllerAdvice中的@InitBinder方法局部地配置。你还可以使用多个验证器。

如果一个控制器有类级别的@Validated注解,那么方法验证会通过AOP代理来应用。为了利用Spring Framework 6.1中添加的Spring MVC对方法验证的内置支持,你需要从控制器中移除类级别的@Validated注解。

为了对方法验证错误进行进一步的自定义处理,你可以扩展ResponseEntityExceptionHandler或使用控制器或@ControllerAdvice中的@ExceptionHandler方法,并直接处理HandlerMethodValidationException异常。该异常包含一个ParameterValidationResults列表,该列表按方法参数对验证错误进行分组。你可以遍历这些错误,或者通过控制器方法参数类型提供一个带有回调方法的访问者:

HandlerMethodValidationException ex = ... ;

ex.visitResults(new HandlerMethodValidationException.Visitor() {

	@Override
	public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
			// ...
	}

	@Override
	public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) {
			// ...
	}

	@Override
	public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) {

	// ...

	@Override
	public void other(ParameterValidationResult result) {
			// ...
	}
});

异常

@Controller和@ControllerAdvice类可以拥有@ExceptionHandler方法来处理来自控制器方法的异常,如下例所示:

@Controller
public class SimpleController {

	// ...

	@ExceptionHandler
	public ResponseEntity<String> handle(IOException ex) {
		// ...
	}
}

异常可能与正在传播的顶级异常(例如直接抛出的IOException)或包装异常中的嵌套原因(例如,被IllegalStateException包装的IOException)匹配。从5.3版本开始,这可以在任意的原因级别上进行匹配,而之前只考虑直接原因。

对于匹配异常类型,最好将目标异常声明为方法参数,如前面的示例所示。当多个异常方法匹配时,通常优先匹配根异常而不是原因异常。更具体地说,ExceptionDepthComparator用于根据异常从抛出的异常类型的深度对异常进行排序。

或者,注解声明可以缩小要匹配的异常类型,如以下示例所示:

@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
	// ...
}

你甚至可以使用特定异常类型的列表和非常通用的参数签名,如以下示例所示:

@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
	// ...
}

根异常和原因异常匹配之间的区别可能会令人惊讶。

在之前展示的IOException变体中,由于FileSystemException和RemoteException都继承自IOException,因此通常使用实际的FileSystemException或RemoteException实例作为参数来调用该方法。然而,如果任何此类匹配的异常在自身也是IOException的包装异常中被传播,那么传入的异常实例就是该包装异常。

在handle(Exception)变体中,行为甚至更为简单。在包装场景中,该方法总是用包装异常来调用,而实际匹配的异常则需要通过ex.getCause()来查找。只有当FileSystemException或RemoteException作为顶级异常抛出时,传入的异常才是实际的FileSystemException或RemoteException实例。

一般建议你在参数签名中尽可能具体,以减少根异常和原因异常类型之间不匹配的可能性。考虑将多匹配方法拆分成单独的@ExceptionHandler方法,每个方法通过其签名匹配一个特定的异常类型。

在多个@ControllerAdvice配置中,建议在具有相应优先级的@ControllerAdvice中声明主要的根异常映射。尽管根异常匹配比原因异常匹配更优先,但这是在给定控制器或@ControllerAdvice类的方法之间定义的。这意味着在优先级较高的@ControllerAdvice bean中进行原因匹配比在低优先级@ControllerAdvice bean中的任何匹配(例如根异常)更优先。

@ExceptionHandler方法实现可以选择通过以原始形式重新抛出异常来退出处理特定的异常实例。这在某些场景下很有用,例如你只对根级别的匹配感兴趣,或者在无法静态确定的特定上下文中进行匹配。重新抛出的异常会通过剩余的解析链传播,就像给定的@ExceptionHandler方法一开始就没有匹配到异常一样。

Spring MVC 中对 @ExceptionHandler 方法的支持建立在 DispatcherServlet 级别的 HandlerExceptionResolver 机制上。

方法参数

ExceptionHandler 方法支持以下参数:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

返回值

ExceptionHandler 方法支持以下返回值:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Controller Advice

@ExceptionHandler、@InitBinder 和 @ModelAttribute 方法仅适用于它们被声明的 @Controller 类或其类层次结构中。如果它们被声明在 @ControllerAdvice 或 @RestControllerAdvice 类中,则它们将应用于任何控制器。此外,从5.3版本开始,@ControllerAdvice 中的 @ExceptionHandler 方法可用于处理来自任何 @Controller 或其他处理器的异常。

@ControllerAdvice 注解被元注解 @Component 标记,因此可以通过组件扫描注册为 Spring bean。@RestControllerAdvice 注解被元注解 @ControllerAdvice 和 @ResponseBody 标记,这意味着 @ExceptionHandler 方法的返回值将通过响应体消息转换进行渲染,而不是通过 HTML 视图。

在启动时,RequestMappingHandlerMapping 和 ExceptionHandlerExceptionResolver 会检测控制器通知bean(controller advice beans)并在运行时应用它们。来自 @ControllerAdvice 的全局 @ExceptionHandler 方法会在来自 @Controller 的本地方法之后应用。相反,全局的 @ModelAttribute 和 @InitBinder 方法会在本地方法之前应用。

@ControllerAdvice 注解具有一些属性,允许你缩小它们所应用的控制器和处理器的范围。例如:

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

前例中的选择器在运行时进行评估,如果大量使用,可能会对性能产生负面影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值