Appearance
Spring Web Servlet:MVC - Part2
Tip:基于 Spring Core 5.3.30 版本。
1. 注解控制器
Spring MVC 提供了一个基于注解的编程模型,其中 @Controller 和 @RestController 组件使用注解来表达请求映射、请求输入、异常处理等。注解控制器具有灵活的方法签名,不必扩展基类或实现特定接口。以下示例显示了一个由注解定义的控制器:
Java
@Controller
public class HelloController {
@GetMapping("/hello")
public String handle(Model model) {
model.addAttribute("message", "Hello World!");
return "index";
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
在前面的示例中,该方法接受一个 Model 并返回一个 String 的视图名称,但是还有许多其他选项,这些将在本章后面进行解释。
Note:spring.io 上的指南和教程使用了本节描述的基于注解的编程模型。
1.1. 声明
你可以在 Servlet 的 WebApplicationContext 中使用标准的 Spring Bean 定义来定义控制器 Bean。@Controller 注解允许自动检测,这与 Spring 对于在类路径中检测 @Component 类并为它们自动注册 Bean 定义的通用支持是一致的。它也作为被注解类的原型,表明其作为 Web 组件的角色。
要启用对这样的 @Controller Bean 的自动检测,你可以在你的 Java 配置中添加组件扫描。如下例所示:
Java
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {
// ...
}1
2
3
4
5
2
3
4
5
下面的示例展示了与前述示例等效的 XML 配置:
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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController 是一个复合注解,它本身是用 @Controller 和 @ResponseBody 元注解的,用来指示一个控制器,其每个方法都继承了类型级别的 @ResponseBody 注解,因此,它直接写入响应体,而不是通过 HTML 模板进行视图解析和渲染。
1.1.1. AOP 代理
在某些情况下,你可能需要在运行时用 AOP 代理来装饰一个控制器。一个例子是如果你选择直接在控制器上使用 @Transactional 注解。在这种情况下,特别是对于控制器,我们推荐使用基于类的代理。这通常是控制器的默认选择。然而,如果一个控制器必须实现一个不是 Spring Context 回调的接口(如 InitializingBean,*Aware 等等),你可能需要显式地配置基于类的代理。例如,对于 <tx:annotation-driven/>,你可以改为 <tx:annotation-driven proxy-target-class="true"/>,对于 @EnableTransactionManagement,你可以改为 @EnableTransactionManagement(proxyTargetClass = true)。
1.2. 请求映射
你可以使用 @RequestMapping 注解将请求映射到控制器的方法。它有各种属性可以通过 URL、HTTP 方法、请求参数、头部信息和媒体类型进行匹配。你可以在类级别使用它来表示共享的映射,或者在方法级别使用它来缩小到特定的端点映射。
还有一些 HTTP 方法特定的 @RequestMapping 的快捷方式:
@GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping
这些快捷方式是自定义注解,因为可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用 @RequestMapping,后者默认匹配所有 HTTP 方法。在类级别仍然需要 @RequestMapping 来表示共享的映射。
以下示例具有类型和方法级别的映射:
Java
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
1.2.1. URI Pattern
@RequestMapping 方法可以使用 URL 模式进行映射。有两种选择:
PathPattern:预解析的模式与同样预解析为PathContainer的 URL 路径进行匹配。这种解决方案专为网络使用设计,可以有效处理编码和路径参数,并进行高效匹配。AntPathMatcher:将字符串模式与字符串路径进行匹配。这是最初也用于 Spring 配置以在类路径、文件系统和其他位置选择资源的解决方案。它的效率较低,而且字符串路径输入在有效处理编码和 URL 的其他问题上面临挑战。
PathPattern 是网络应用的推荐解决方案,它是 Spring WebFlux 的唯一选择。在 5.3 版本之前,AntPathMatcher 是 Spring MVC 的唯一选择,并继续作为默认选项。然而,PathPattern 可以在 MVC 配置中启用。
PathPattern 支持与 AntPathMatcher 相同的模式语法。此外,它还支持捕获模式,例如 {*spring},用于匹配路径末尾的 0 个或多个路径段。PathPattern 还限制了使用 ** 匹配多个路径段的使用,使其只能在模式的末尾使用。这消除了在为给定请求选择最佳匹配模式时的许多歧义情况。有关完整的模式语法,请参考 PathPattern 和 AntPathMatcher。
一些示例模式:
"/resources/ima?e.png":匹配路径段中的一个字符"/resources/*.png":匹配路径段中的零个或多个字符"/resources/**":匹配多个路径段"/projects/{project}/versions":匹配路径段并将其捕获为变量"/projects/{project:[a-z]+}/versions":使用正则表达式匹配并捕获变量
可以使用 @PathVariable 访问捕获的 URI 变量。例如:
Java
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}1
2
3
4
2
3
4
你可以在类和方法级别声明 URI 变量,如下例所示:
Java
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {
@GetMapping("/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
URI 变量会自动转换为适当的类型,否则会引发 TypeMismatchException。默认情况下,支持简单类型(如 int、long、Date 等),你可以注册支持任何其他数据类型。请参见类型转换和 DataBinder。
你可以显式命名 URI 变量(例如,@PathVariable("customId")),但如果名称相同且你的代码是在带有调试信息的情况下编译的,或者在 Java 8 上带有 -parameters 编译器标志,那么你可以省略这个细节。
语法 {varName:regex} 声明了一个带有正则表达式的 URI 变量,其语法为 {varName:regex}。例如,给定 URL "/spring-web-3.0.5.jar",以下方法提取了名称、版本和文件扩展名:
Java
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
// ...
}1
2
3
4
2
3
4
URI 路径模式也可以嵌入 ${...} 占位符,这些占位符在启动时通过使用 PropertySourcesPlaceholderConfigurer 针对本地、系统、环境和其他属性源进行解析。例如,你可以使用此功能基于某些外部配置对基础 URL 进行参数化。
1.2.2. Pattern 比较
当多个模式匹配一个 URL 时,必须选择最佳匹配。这是通过以下方式之一完成的,具体取决于是否启用解析的 PathPattern:
这两者都有助于将更具体的模式排序在前面。如果一个模式的 URI 变量(计数为 1)、单个通配符(计数为 1)和双通配符(计数为 2)的数量较少,那么这个模式就不够具体。给定相等的分数,选择较长的模式。给定相同的分数和长度,选择 URI 变量比通配符多的模式。
默认的映射模式(/**)被排除在评分之外,并始终排序在最后。此外,前缀模式(如 /public/**)被认为比没有双通配符的其他模式更不具体。
有关完整的细节,请按照上述链接查看模式比较器。
1.2.3. 后缀匹配
从 5.3 版本开始,默认情况下 Spring MVC 不再执行 .* 后缀模式匹配,即映射到 /person 的控制器也隐式地映射到 /person.*。因此,路径扩展名不再用于解释响应的请求内容类型 —— 例如,/person.pdf、/person.xml 等。
当浏览器发送难以一致解释的 Accept 头时,以这种方式使用文件扩展名是必要的。目前,这已经不再是必要的,使用 Accept 头应该是首选。
随着时间的推移,使用文件名扩展名在各种方式上都被证明是有问题的。当与 URI 变量、路径参数和 URI 编码的使用重叠时,它可能会引起歧义。关于基于 URL 的授权和安全性的推理(请参阅下一节以获取更多详细信息)也变得更加困难。
要在 5.3 版本之前完全禁用路径扩展的使用,请设置以下内容:
useSuffixPatternMatching(false),参见PathMatchConfigurerfavorPathExtension(false),参见ContentNegotiationConfigurer
除了通过 Accept 头请求内容类型之外,还有其他方式可能会很有用,例如在浏览器中输入 URL。路径扩展的一种安全替代方案是使用查询参数策略。如果你必须使用文件扩展名,考虑将它们限制为通过 ContentNegotiationConfigurer 的 mediaTypes 属性显式注册的扩展名列表。
1.2.4. 后缀匹配与 RFD
反射文件下载(RFD,Reflected File Download)攻击与 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 头。参见内容类型。
参见 CVE-2015-5211 以获取与 RFD 相关的其他建议。
1.2.5. 可消费的媒体类型
你可以根据请求的 Content-Type 来缩小请求映射,如下面的示例所示:
Java
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
// ...
}1
2
3
4
2
3
4
consumes 属性也支持否定表达式 —— 例如,!text/plain 表示除 text/plain 之外的任何内容类型。
你可以在类级别声明一个共享的 consumes 属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别的 consumes 属性会覆盖而不是扩展类级别的声明。
Note:
MediaType提供了常用媒体类型的常量,如APPLICATION_JSON_VALUE和APPLICATION_XML_VALUE。
1.2.6. 可生成的媒体类型
你可以根据 Accept 请求头和控制器方法生成的内容类型列表来缩小请求映射,如下面的示例所示:
Java
@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}1
2
3
4
5
2
3
4
5
媒体类型可以指定字符集。支持否定表达式 —— 例如,!text/plain 表示除 text/plain 之外的任何内容类型。
你可以在类级别声明一个共享的 produces 属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别的 produces 属性会覆盖而不是扩展类级别的声明。
Note:
MediaType提供了常用媒体类型的常量,如APPLICATION_JSON_VALUE和APPLICATION_XML_VALUE。
1.2.7. 参数、头部
你可以根据请求参数条件来缩小请求映射。你可以测试一个请求参数的存在(myParam),一个请求参数的不存在(!myParam),或者一个特定的值(myParam=myValue)。下面的示例展示了如何测试一个特定的值:
Java
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
public void findPet(@PathVariable String petId) {
// ...
}1
2
3
4
2
3
4
你也可以用同样的方法处理请求头条件,如下面的示例所示:
Java
@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue")
public void findPet(@PathVariable String petId) {
// ...
}1
2
3
4
2
3
4
Note:你可以使用
headers条件来匹配Content-Type和Accept,但使用consumes和produces会更好。
1.2.8. HTTP HEAD、OPTIONS
@GetMapping(和 @RequestMapping(method=HttpMethod.GET))透明地支持 HTTP HEAD 的请求映射。控制器方法不需要改变。一个在 javax.servlet.http.HttpServlet 中应用的响应包装器确保设置了 Content-Length 头,该头的值为写入的字节数(实际上并没有写入到响应中)。
@GetMapping(和 @RequestMapping(method=HttpMethod.GET))被隐式地映射到并支持 HTTP HEAD。HTTP HEAD 请求被处理得就像它是 HTTP GET,除了不写入主体,而是计算字节数并设置 Content-Length 头。
默认情况下,HTTP OPTIONS 是通过将 Allow 响应头设置为所有 @RequestMapping 方法中列出的具有匹配 URL 模式的 HTTP 方法列表来处理的。
对于没有 HTTP 方法声明的 @RequestMapping,Allow 头被设置为 GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS。控制器方法应始终声明支持的 HTTP 方法(例如,使用 HTTP 方法特定的变体:@GetMapping,@PostMapping 等)。
你可以显式地将 @RequestMapping 方法映射到 HTTP HEAD 和 HTTP OPTIONS,但在常见情况下这是不必要的。
1.2.9. 自定义注解
Spring MVC 支持使用组合注解进行请求映射。这些注解本身是用 @RequestMapping 元注解的,并且被组合以重新声明 @RequestMapping 属性的一个子集(或全部),其目的更狭窄,更具体。
@GetMapping、@PostMapping、@PutMapping、@DeleteMapping 和 @PatchMapping 是组合注解的例子。它们被提供出来是因为,可以说,大多数控制器方法应该映射到一个特定的 HTTP 方法,而不是使用 @RequestMapping,后者默认匹配所有的 HTTP 方法。如果你需要一个组合注解的例子,看看这些是如何声明的。
Spring MVC 也支持自定义请求映射属性和自定义请求匹配逻辑。这是一个更高级的选项,需要子类化 RequestMappingHandlerMapping 并重写 getCustomMethodCondition 方法,在那里你可以检查自定义属性并返回你自己的 RequestCondition。
1.2.10. 显式注册
你可以以编程方式注册处理器方法,这可以用于动态注册或者高级情况,例如在不同的 URL 下的同一处理器的不同实例。下面的示例注册了一个处理器方法:
Java
@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);
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
1.3. 处理器方法(Handler Methods)
@RequestMapping 处理器方法具有灵活的签名,可以从一系列支持的控制器方法参数和返回值中进行选择。
1.3.1. 方法参数
下方表格描述了支持的控制器方法参数。任何参数都不支持响应式类型。
JDK 8 的 java.util.Optional 作为方法参数是支持的,它可以与具有 required 属性的注解(例如,@RequestParam,@RequestHeader 等)结合使用,等同于 required=false。
| 控制器方法参数 | 描述 |
|---|---|
WebRequest,NativeWebRequest | 对请求参数以及请求和会话属性的通用访问,无需直接使用 Servlet API。 |
javax.servlet.ServletRequest,javax.servlet.ServletResponse | 选择任何特定的请求或响应类型 —— 例如,ServletRequest,HttpServletRequest,或者 Spring 的 MultipartRequest,MultipartHttpServletRequest。 |
javax.servlet.http.HttpSession | 强制存在一个会话。因此,这样的参数永远不会为 null。注意,会话访问不是线程安全的。如果允许多个请求并发访问一个会话,那么考虑将 RequestMappingHandlerAdapter 实例的 synchronizeOnSession 标志设置为 true。 |
javax.servlet.http.PushBuilder | Servlet 4.0 推送构建器 API,用于编程式 HTTP/2 资源推送。注意,根据 Servlet 规范,如果客户端不支持该 HTTP/2 功能,那么注入的 PushBuilder 实例可能为 null。 |
java.security.Principal | 当前认证的用户 —— 可能是一个特定的 Principal 实现类,如果已知的话。注意,如果这个参数被注解,那么它不会被急切地解析,以便允许一个自定义解析器在回退到通过 HttpServletRequest#getUserPrincipal 进行默认解析之前解析它。例如,Spring Security Authentication 实现了 Principal,并且会通过 HttpServletRequest#getUserPrincipal 被注入,除非它也被 @AuthenticationPrincipal 注解,在这种情况下,它会被一个自定义的 Spring Security 解析器通过 Authentication#getPrincipal 解析。 |
HttpMethod | 请求的 HTTP 方法。 |
java.util.Locale | 当前请求的地区设置,由最具体的可用 LocaleResolver 确定(实际上,是配置的 LocaleResolver 或 LocaleContextResolver)。 |
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 模板变量。参见 URI Pattern。 |
@MatrixVariable | 用于访问 URI 路径段中的 name-value 对。参见矩阵变量(Matrix Variables)。 |
@RequestParam | 用于访问 Servlet 请求参数,包括 Multipart 文件。参数值被转换为声明的方法参数类型。参见 @RequestParam 以及 Multipart。注意,对于简单的参数值,使用 @RequestParam 是可选的。参见 “任何其他参数”,在此表的末尾。 |
@RequestHeader | 用于访问请求头。头值被转换为声明的方法参数类型。参见 @RequestHeader。 |
@CookieValue | 用于访问 Cookies。Cookies 值被转换为声明的方法参数类型。参见 @CookieValue。 |
@RequestBody | 用于访问 HTTP 请求体。通过使用 HttpMessageConverter 实现,将主体内容转换为声明的方法参数类型。参见 @RequestBody。 |
HttpEntity<B> | 用于访问请求头和主体。主体通过 HttpMessageConverter 转换。参见 HttpEntity。 |
@RequestPart | 用于访问 multipart/form-data 请求中的一部分,通过 HttpMessageConverter 转换部分的主体。参见 Multipart。 |
java.util.Map,org.springframework.ui.Model,org.springframework.ui.ModelMap | 用于访问在 HTML 控制器中使用并作为视图渲染的一部分暴露给模板的 Model。 |
RedirectAttributes | 指定在重定向的情况下使用的属性(即,要附加到查询字符串的属性)和 Flash 属性,这些属性将被暂时存储,直到重定向后的请求。参见重定向属性和 Flash 属性。 |
@ModelAttribute | 用于访问 Model 中的现有属性(如果不存在则实例化),并应用数据绑定和验证。参见 @ModelAttribute 以及 Model 和 DataBinder。注意,使用 @ModelAttribute 是可选的(例如,设置其属性)。参见 “任何其他参数”,在此表的末尾。 |
Errors,BindingResult | 用于访问来自命令对象(即,@ModelAttribute 参数)的验证和数据绑定的错误,或者来自 @RequestBody 或 @RequestPart 参数的验证错误。你必须在经过验证的方法参数之后立即声明一个 Errors 或 BindingResult 参数。 |
SessionStatus + 类级别的 @SessionAttributes | 用于标记表单处理完成,这将触发清理通过类级别的 @SessionAttributes 注解声明的会话属性。参见 @SessionAttributes 以获取更多详细信息。 |
UriComponentsBuilder | 用于准备相对于当前请求的 Host、Port、Scheme、上下文路径和 Servlet 映射的文字部分的 URL。参见 URI 链接。 |
@SessionAttribute | 用于访问任何会话属性,与由于类级别的 @SessionAttributes 声明而存储在会话中的 Model 属性形成对比。参见 @SessionAttribute 以获取更多详细信息。 |
@RequestAttribute | 用于访问请求属性。参见 @RequestAttribute 以获取更多详细信息。 |
| 任何其他参数 | 如果方法参数没有匹配到此表中的任何先前值,并且它是一个简单类型(由 BeanUtils#isSimpleProperty 确定),那么它将被解析为 @RequestParam。否则,它将被解析为 @ModelAttribute。 |
1.3.2. 返回值
下一个表格描述了支持的控制器方法返回值。所有返回值都支持响应式类型。
| 控制器方法返回值 | 描述 |
|---|---|
@ResponseBody | 通过 HttpMessageConverter 实现将返回值转换并写入响应。参见 @ResponseBody。 |
HttpEntity<B>,ResponseEntity<B> | 指定完整响应(包括 HTTP 头和主体)的返回值,将通过 HttpMessageConverter 实现转换并写入响应。参见 ResponseEntity。 |
HttpHeaders | 用于返回带有头部且无主体的响应。 |
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 注解,则被认为已完全处理了响应。如果控制器进行了积极的 ETag 或 lastModified 时间戳检查(参见控制器详细信息),也是如此。如果上述情况都不成立, void 返回类型也可以表示 REST 控制器的 “无响应主体” 或 HTML 控制器的默认视图名称选择。 |
DeferredResult<V> | 从任何线程异步生成前述任何返回值 —— 例如,作为某些事件或回调的结果。参见异步请求和 DeferredResult。 |
Callable<V> | 在 Spring MVC 管理的线程中异步生成上述任何返回值。参见异步请求和 Callable。 |
ListenableFuture<V>,java.util.concurrent.CompletionStage<V>,java.util.concurrent.CompletableFuture<V> | 作为 DeferredResult 的替代方案,作为一种便利(例如,当底层服务返回其中一个时)。 |
ResponseBodyEmitter,SseEmitter | 用 HttpMessageConverter 实现异步发射一个对象流(Stream),以写入响应中。也支持作为 ResponseEntity 的主体。参见异步请求和 HTTP 流。 |
StreamingResponseBody | 异步写入响应 OutputStream。也支持作为 ResponseEntity 的主体。参见异步请求和 HTTP 流。 |
通过 ReactiveAdapterRegistry 注册的 Reactor 和其他响应式类型 | 单值类型,例如 Mono,相当于返回 DeferredResult。多值类型,例如 Flux,可能会根据请求的媒体类型(例如 "text/event-stream","application/json+stream")被视为流,否则会被收集到 List 并作为单个值渲染。参见异步请求和响应式类型。 |
| 其他返回值 | 如果以任何其他方式的返回值仍未被解析,它将被视为模型属性,除非它是由 BeanUtils#isSimpleProperty 确定的简单类型,在这种情况下,它将保持未被解析。 |
1.3.3. 类型转换
一些注解的控制器方法参数代表基于字符串的请求输入(如 @RequestParam、@RequestHeader、@PathVariable、@MatrixVariable 和 @CookieValue),如果参数声明为除字符串以外的其他类型,可能需要进行类型转换。
对于这种情况,根据配置的转换器自动应用类型转换。默认情况下,支持简单类型(如 int、long、Date 等)。你可以通过 WebDataBinder(参见 DataBinder)或者向 FormattingConversionService 注册 Formatters 来自定义类型转换。参见 Spring 字段格式化。
类型转换中的一个实际问题是处理空字符串源值。如果类型转换的结果是 null,那么这样的值会被视为缺失。这可能适用于 Long、UUID 和其他目标类型。如果你想允许注入 null,可以在参数注解上使用 required 标志,或者声明参数为 @Nullable。
Note
从 5.3 版开始,即使在类型转换后,也会强制执行非
null参数。如果你的处理器方法打算接受null值,那么可以将你的参数声明为@Nullable,或者在相应的@RequestParam等注解中将其标记为required=false。这是最佳实践,也是解决在 5.3 升级中遇到的回归问题的推荐解决方案。或者,你可以特别处理例如在需要
@PathVariable的情况下产生的MissingPathVariableException。转换后的null值将被视为原始值为空,因此将抛出相应的Missing…Exception变体。
1.3.4. 矩阵变量(Matrix Variables)
RFC 3986 讨论了路径段中的 name-value 对。在 Spring MVC 中,我们根据 Tim Berners-Lee 的一篇 “旧帖子” 将其称为 “矩阵变量”,但它们也可以被称为 URI 路径参数。
矩阵变量可以出现在任何路径段中,每个变量由分号分隔,多个值由逗号分隔(例如,/cars;color=red,green;year=2012)。也可以通过重复变量名来指定多个值(例如,color=red;color=green;color=blue)。
如果预期 URL 包含矩阵变量,控制器方法的请求映射必须使用 URI 变量来掩盖该变量内容,并确保请求可以成功匹配,而不依赖于矩阵变量的顺序和存在。以下示例使用了一个矩阵变量:
Java
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}1
2
3
4
5
6
7
2
3
4
5
6
7
考虑到所有路径段可能都包含矩阵变量,你有时可能需要消除哪个路径变量中预期存在矩阵变量的歧义。以下示例展示了如何进行此操作:
Java
// 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
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
矩阵变量可以被定义为可选的,并指定一个默认值,如下面的示例所示:
Java
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}1
2
3
4
5
6
2
3
4
5
6
要获取所有矩阵变量,你可以使用 MultiValueMap,如下面的示例所示:
Java
// 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]
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
请注意,你需要启用矩阵变量的使用。在 MVC Java 配置中,你需要通过路径匹配设置一个 UrlPathHelper,并将 removeSemicolonContent 设置为 false。在 MVC XML 命名空间中,你可以设置 <mvc:annotation-driven enable-matrix-variables="true"/>。
1.3.5. @RequestParam
你可以使用 @RequestParam 注解将 Servlet 请求参数(即查询参数或表单数据)绑定到控制器中的方法参数。
以下示例展示了如何进行此操作:
Java
@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";
}
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
默认情况下,使用此注解的方法参数是必需的,但你可以通过将 @RequestParam 注解的 required 标志设置为 false 或者通过声明参数为 java.util.Optional 包装器来指定方法参数是可选的。
如果目标方法参数类型不是 String,则会自动应用类型转换。参见类型转换。
将参数类型声明为数组或列表可以解析同一参数名的多个参数值。
当一个 @RequestParam 注解被声明为 Map<String, String> 或 MultiValueMap<String, String>,并且在注解中没有指定参数名时,那么该映射将被填充每个给定参数名的请求参数值。
请注意,使用 @RequestParam 是可选的(例如,设置其属性)。默认情况下,任何简单值类型的参数(由 BeanUtils#isSimpleProperty 确定)并且没有被任何其他参数解析器解析的,都会被视为它如同被 @RequestParam 注解。
1.3.6. @RequestHeader
你可以使用 @RequestHeader 注解将请求头绑定到控制器中的方法参数。
考虑以下带有请求头的请求:
Text
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 3001
2
3
4
5
6
2
3
4
5
6
以下示例获取了 Accept-Encoding 和 Keep-Alive 请求头的值:
Java
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding,
@RequestHeader("Keep-Alive") long keepAlive) {
//...
}1
2
3
4
5
6
2
3
4
5
6
如果目标方法参数类型不是 String,则会自动应用类型转换。参见类型转换。
当 @RequestHeader 注解用于 Map<String, String>、MultiValueMap<String, String> 或 HttpHeaders 参数时,映射将填充所有头值。
Note:内置支持可将逗号分隔的字符串转换为字符串数组或集合,或者类型转换系统已知的其他类型。例如,用
@RequestHeader("Accept")注解的方法参数可以是String类型,也可以是String[]或List<String>类型。
1.3.7. @CookieValue
你可以使用 @CookieValue 注解将 HTTP Cookie 的值绑定到控制器中的方法参数。
考虑一个带有以下 Cookie 的请求:
Text
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD841
以下示例展示了如何获取 Cookie 的值:
Java
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) {
//...
}1
2
3
4
2
3
4
如果目标方法参数类型不是 String,则会自动应用类型转换。参见类型转换。
1.3.8. @ModelAttribute
你可以在方法参数上使用 @ModelAttribute 注解来访问模型中的属性,或者如果不存在,就实例化它。模型属性也会与 HTTP Servlet 请求参数的值重叠,这些参数的名称与字段名称匹配。这被称为数据绑定,它可以省去你处理解析和转换单个查询参数和表单字段的麻烦。以下示例展示了如何进行此操作:
Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) {
// method logic...
}1
2
3
4
2
3
4
上述的 Pet 实例的来源有以下几种方式:
从模型中检索,可能已经通过
@ModelAttribute方法添加到模型中;如果模型属性在类级别的
@SessionAttributes注解中列出,那么可以从 HTTP 会话中检索;通过
Converter获得,其中模型属性名称与请求值(如路径变量或请求参数)的名称匹配(参见下一个示例);使用其默认构造函数实例化;
通过与 Servlet 请求参数匹配的参数实例化 “主构造函数”。参数名称是通过 JavaBeans 的
@ConstructorProperties或通过字节码中保留的运行时参数名称确定的;
使用 @ModelAttribute 方法提供它或依赖框架创建模型属性的一种替代方法是,有一个 Converter<String, T> 来提供实例。当模型属性名称与请求值(如路径变量或请求参数)的名称匹配,并且有一个从 String 到模型属性类型的 Converter 时,就会应用这种方法。在下面的示例中,模型属性名称是 account,它与 URI 路径变量 account 匹配,并且有一个已注册的 Converter<String, Account>,它可以从数据存储中加载 Account:
Java
@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) {
// ...
}1
2
3
4
2
3
4
在获得模型属性实例后,将应用数据绑定。WebDataBinder 类将 Servlet 请求参数名称(查询参数和表单字段)匹配到目标对象的字段名称。在必要时应用类型转换后,将填充匹配的字段。有关数据绑定(和验证)的更多信息,请参见验证。有关自定义数据绑定的更多信息,请参见 DataBinder。
数据绑定可能会导致错误。默认情况下,会引发 BindException。然而,为了在控制器方法中检查这类错误,你可以在 @ModelAttribute 旁边立即添加一个 BindingResult 参数,如下面的示例所示:
Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {
if (result.hasErrors()) {
return "petForm";
}
// ...
}1
2
3
4
5
6
7
2
3
4
5
6
7
在某些情况下,你可能希望在不进行数据绑定的情况下访问模型属性。对于这种情况,你可以将 Model 注入到控制器中并直接访问,或者,设置 @ModelAttribute(binding=false),如下面的示例所示:
Java
@ModelAttribute
public AccountForm setUpForm() {
return new AccountForm();
}
@ModelAttribute
public Account findAccount(@PathVariable String accountId) {
return accountRepository.findOne(accountId);
}
@PostMapping("update")
public String update(@Valid AccountForm form, BindingResult result,
@ModelAttribute(binding=false) Account account) {
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
你可以通过添加 javax.validation.Valid 注解或 Spring 的 @Validated 注解(Bean 验证和 Spring 验证)在数据绑定后自动应用验证。以下示例展示了如何进行此操作:
Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) {
if (result.hasErrors()) {
return "petForm";
}
// ...
}1
2
3
4
5
6
7
2
3
4
5
6
7
请注意,使用 @ModelAttribute 是可选的(例如,设置其属性)。默认情况下,任何不是简单值类型的参数(由 BeanUtils#isSimpleProperty 确定)并且没有被任何其他参数解析器解析的,都会被视为它如同被 @ModelAttribute 注解。
1.3.9. @SessionAttributes
@SessionAttributes 用于在请求之间在 HTTP Servlet 会话中存储模型属性。它是一种类型级别的注解,声明了特定控制器使用的会话属性。这通常列出了应该在会话中透明存储的模型属性的名称或模型属性的类型,以便后续请求访问。
以下示例使用了 @SessionAttributes 注解:
Java
@Controller
@SessionAttributes("pet")
public class EditPetForm {
// ...
}1
2
3
4
5
2
3
4
5
在第一次请求时,当一个名为 pet 的模型属性被添加到模型中,它会自动被提升并保存在 HTTP Servlet 会话中。它会一直保留在那里,直到另一个控制器方法使用 SessionStatus 方法参数来清除存储,如下面的示例所示:
Java
@Controller
@SessionAttributes("pet")
public class EditPetForm {
// ...
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) {
if (errors.hasErrors) {
// ...
}
status.setComplete();
// ...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
1.3.10. @SessionAttribute
如果你需要访问全局管理(即,控制器之外,例如,通过过滤器)的预先存在的会话属性,这些属性可能存在也可能不存在,你可以在方法参数上使用 @SessionAttribute 注解,如下面的示例所示:
Java
@RequestMapping("/")
public String handle(@SessionAttribute User user) {
// ...
}1
2
3
4
2
3
4
对于需要添加或删除会话属性的用例,可以考虑将 org.springframework.web.context.request.WebRequest 或 javax.servlet.http.HttpSession 注入到控制器方法中。
对于作为控制器工作流一部分在会话中临时存储模型属性的情况,可以考虑使用 @SessionAttributes,如在 @SessionAttributes 中所述。
1.3.11. @RequestAttribute
与 @SessionAttribute 类似,你可以使用 @RequestAttribute 注解来访问之前创建的预存在请求属性(例如,由 Servlet Filter 或 HandlerInterceptor 所创建):
Java
@GetMapping("/")
public String handle(@RequestAttribute Client client) {
// ...
}1
2
3
4
2
3
4
1.3.12. 重定向属性
默认情况下,所有模型属性都被视为在重定向 URL 中暴露为 URI 模板变量。对于剩余的属性,如果它们是原始类型或原始类型的集合或数组,那么它们会自动被附加为查询参数。
如果一个模型实例是专门为重定向准备的,那么将原始类型属性作为查询参数附加可能是期望的结果。然而,在带注解的控制器中,模型可能包含额外的为渲染目的添加的属性(例如,下拉字段值)。为了避免这样的属性出现在 URL 中,一个 @RequestMapping 方法可以声明一个类型为 RedirectAttributes 的参数,并使用它来指定要提供给 RedirectView 的确切属性。如果方法确实进行了重定向,那么将使用 RedirectAttributes 的内容。否则,将使用模型的内容。
RequestMappingHandlerAdapter 提供了一个名为 ignoreDefaultModelOnRedirect 的标志,你可以使用它来指示控制器方法重定向时永远不应使用默认 Model 的内容。相反,控制器方法应声明一个类型为 RedirectAttributes 的属性,或者,如果它没有这样做,那么不应将任何属性传递给 RedirectView。MVC 命名空间和 MVC Java 配置都将此标志设置为 false,以保持向后兼容性。然而,对于新的应用程序,我们建议将其设置为 true。
请注意,来自当前请求的 URI 模板变量在扩展重定向 URL 时会自动提供,你不需要通过 Model 或 RedirectAttributes 明确添加它们。以下示例显示了如何定义重定向:
Java
@PostMapping("/files/{path}")
public String upload(...) {
// ...
return "redirect:files/{path}";
}1
2
3
4
5
2
3
4
5
将数据传递给重定向目标的另一种方式是使用 Flash 属性。与其他重定向属性不同,Flash 属性保存在 HTTP 会话中(因此,不会出现在 URL 中)。有关更多信息,请参阅 Flash 属性。
1.3.13. Flash 属性
Flash 属性提供了一种方式,让一个请求存储那些打算在另一个请求中使用的属性。这在重定向时最常需要,例如,Post-Redirect-Get 模式。Flash 属性在重定向之前被临时保存(通常在会话中),以便在重定向后的请求中可用,并立即被移除。
Spring MVC 有两个主要的抽象来支持 Flash 属性。FlashMap 用于保存 Flash 属性,而 FlashMapManager 用于存储、检索和管理 FlashMap 实例。
Flash 属性的支持始终是 “开启” 的,不需要显式启用。然而,如果没有使用,它永远不会导致 HTTP 会话创建。在每个请求中,都有一个 “input” FlashMap,其中包含从前一个请求(如果有的话)传递过来的属性,以及一个 “output” FlashMap,其中包含要保存给后续请求的属性。两个 FlashMap 实例都可以通过 Spring MVC 中 RequestContextUtils 的静态方法从任何地方访问。
带注解的控制器通常不需要直接操作 FlashMap。相反,一个 @RequestMapping 方法可以接受一个类型为 RedirectAttributes 的参数,并使用它来为重定向场景添加 Flash 属性。通过 RedirectAttributes 添加的 Flash 属性会自动传播到 “output” FlashMap。同样,重定向后,“input” FlashMap 中的属性会自动添加到服务目标 URL 的控制器的 Model 中。
Note:匹配请求到 Flash 属性
Flash 属性的概念在许多其他 Web 框架中都存在,并已被证明有时会暴露出并发问题。这是因为,根据定义,Flash 属性应该被存储到下一个请求。然而,真正的 “下一个” 请求可能不是预期的接收者,而是另一个异步请求(例如,轮询或资源请求),在这种情况下,Flash 属性可能会过早地被移除。
为了减少这种问题的可能性,
RedirectView自动将FlashMap实例与目标重定向 URL 的路径和查询参数 “标记(Stamps)”。反过来,当默认的FlashMapManager在查找 “input”FlashMap时,它会将该信息匹配到传入的请求。这并不能完全消除并发问题的可能性,但是通过已经在重定向 URL 中可用的信息,可以大大减少这种可能性。因此,我们建议你主要在重定向场景中使用 Flash 属性。
1.3.14. Multipart
在启用了 MultipartResolver 之后,带有 multipart/form-data 的 POST 请求的内容会被解析并作为常规请求参数可访问。以下示例访问了一个常规表单字段和一个上传的文件:
Java
@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";
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
将参数类型声明为 List<MultipartFile> 可以解析同一参数名的多个文件。
当 @RequestParam 注解被声明为 Map<String, MultipartFile> 或 MultiValueMap<String, MultipartFile>,并且在注解中没有指定参数名时,那么该映射将被填充每个给定参数名的 Multipart 文件。
Note:在 Servlet 3.0 的 Multipart 解析中,你也可以声明
javax.servlet.http.Part而不是 Spring 的MultipartFile,作为方法参数或集合值类型。
你还可以将 Multipart 内容作为数据绑定到命令对象的一部分。例如,前面示例中的表单字段和文件可以是表单对象上的字段,如下面的示例所示:
Java
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";
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Multipart 请求也可以在 RESTful 服务场景中从非浏览器客户端提交。以下示例显示了一个带有 JSON 的文件:
Text
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 ...1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
你可以使用 @RequestParam 以字符串的形式访问 "meta-data" 部分,但你可能希望将其从 JSON 反序列化(类似于 @RequestBody)。使用 @RequestPart 注解在用 HttpMessageConverter 转换后访问 Multipart 内容:
Java
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata,
@RequestPart("file-data") MultipartFile file) {
// ...
}1
2
3
4
5
2
3
4
5
你可以将 @RequestPart 与 javax.validation.Valid 或 Spring 的 @Validated 注解结合使用,这两者都会导致应用标准的 Bean 验证。默认情况下,验证错误会导致 MethodArgumentNotValidException,这会被转换为 400(BAD_REQUEST)响应。或者,你可以通过 Errors 或 BindingResult 参数在控制器内部处理验证错误,如下面的示例所示:
Java
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
BindingResult result) {
// ...
}1
2
3
4
5
2
3
4
5
1.3.15. @RequestBody
你可以使用 @RequestBody 注解来通过 HttpMessageConverter 读取并反序列化请求体到一个对象中。以下示例使用了一个 @RequestBody 参数:
Java
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}1
2
3
4
2
3
4
你可以使用 MVC 配置的消息转换器选项来配置或自定义消息转换。
你可以将 @RequestBody 与 javax.validation.Valid 或 Spring 的 @Validated 注解结合使用,这两者都会导致应用标准的 Bean 验证。默认情况下,验证错误会导致 MethodArgumentNotValidException,这会被转换为 400(BAD_REQUEST)响应。或者,你可以通过 Errors 或 BindingResult 参数在控制器内部处理验证错误,如下面的示例所示:
Java
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
// ...
}1
2
3
4
2
3
4
1.3.16. HttpEntity
HttpEntity 与使用 @RequestBody 大致相同,但它基于一个可以暴露请求头和请求体的容器对象。以下示例展示了这一点:
Java
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}1
2
3
4
2
3
4
1.3.17. @ResponseBody
你可以在方法上使用 @ResponseBody 注解,通过 HttpMessageConverter 将返回值序列化到响应体中。以下示例展示了这一点:
Java
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}1
2
3
4
5
2
3
4
5
@ResponseBody 也支持在类级别使用,这种情况下,它会被所有的控制器方法继承。这就是 @RestController 的效果,它不过是一个标有 @Controller 和 @ResponseBody 的元注解。
你可以将 @ResponseBody 与响应式类型一起使用。更多详情请参见异步请求和响应式类型。
你可以使用 MVC 配置的消息转换器选项来配置或自定义消息转换。
你可以将 @ResponseBody 方法与 JSON 序列化视图结合使用。详情请参见 Jackson JSON。
1.3.18. ResponseEntity
ResponseEntity 类似于 @ResponseBody,但它包含了状态和头信息。例如:
Java
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).body(body);
}1
2
3
4
5
6
2
3
4
5
6
Spring MVC 支持使用单值响应式类型异步生成 ResponseEntity,并/或为主体使用单值和多值响应式类型。这允许以下类型的异步响应:
ResponseEntity<Mono<T>>或ResponseEntity<Flux<T>>使得响应状态和头信息立即可知,而主体在稍后的时间点异步提供。如果主体由 0..1 个值组成,使用Mono;如果它可以产生多个值,使用Flux。Mono<ResponseEntity<T>>在稍后的时间点异步提供所有三个元素 —— 响应状态、头信息和主体。这允许响应状态和头信息根据异步请求处理的结果而变化。
1.3.19. Jackson JSON
Spring 提供了对 Jackson JSON 库的支持。
1.3.19.1. JSON 视图
Spring MVC 提供了对 Jackson 的序列化视图的内置支持,这允许只渲染对象中的一部分字段。要在 @ResponseBody 或 ResponseEntity 控制器方法中使用它,你可以使用 Jackson 的 @JsonView 注解来激活一个序列化视图类,如下面的示例所示:
Java
@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;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Note:
@JsonView允许一个视图类数组,但你只能在每个控制器方法中指定一个。如果你需要激活多个视图,你可以使用一个复合接口。
如果你想以编程方式做上述操作,而不是声明一个 @JsonView 注解,你可以将返回值用 MappingJacksonValue 包装,并使用它来提供序列化视图:
Java
@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;
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
对于依赖视图解析的控制器,你可以将序列化视图类添加到模型中,如下面的示例所示:
Java
@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";
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
1.4. Model
你可以使用 @ModelAttribute 注解:
在
@RequestMapping方法的方法参数上,用于从模型中创建或访问一个对象,并通过WebDataBinder将其绑定到请求。作为
@Controller或@ControllerAdvice类中的方法级注解,帮助在任何@RequestMapping方法调用之前初始化模型。在
@RequestMapping方法上,标记其返回值是一个模型属性。
本节讨论的是 @ModelAttribute 方法 —— 前述列表中的第二项。一个控制器可以有任意数量的 @ModelAttribute 方法。同一控制器中的所有这些方法都会在 @RequestMapping 方法之前被调用。@ModelAttribute 方法也可以通过 @ControllerAdvice 在控制器之间共享。更多详情请参见控制器增强部分。
@ModelAttribute 方法有灵活的方法签名。它们支持许多与 @RequestMapping 方法相同的参数,除了 @ModelAttribute 本身或与请求体相关的任何内容。
以下示例展示了一个 @ModelAttribute 方法:
Java
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}1
2
3
4
5
2
3
4
5
以下示例只添加了一个属性:
Java
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}1
2
3
4
2
3
4
Note:当没有明确指定名称时,会根据对象类型选择一个默认名称,这在
Conventions的 JavaDoc 中有解释。你总是可以通过使用重载的addAttribute方法或通过@ModelAttribute的name属性(对于返回值)来分配一个明确的名称。
你也可以在 @RequestMapping 方法上使用 @ModelAttribute 作为方法级别的注解,此时 @RequestMapping 方法的返回值会被解释为模型属性。这通常不是必需的,因为它是 HTML 控制器中的默认行为,除非返回值是一个字符串,否则该字符串将被解释为视图名称。@ModelAttribute 也可以自定义模型属性名称,如下面的示例所示:
Java
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}1
2
3
4
5
6
2
3
4
5
6
1.5. DataBinder
@Controller 或 @ControllerAdvice 类可以拥有初始化 WebDataBinder 实例的 @InitBinder 方法,这些方法又可以:
将请求参数(即表单或查询数据)绑定到模型对象。
将基于字符串的请求值(如请求参数、路径变量、头部、Cookie 等)转换为控制器方法参数的目标类型。
在渲染 HTML 表单时,将模型对象值格式化为字符串值。
@InitBinder 方法可以注册特定于控制器的 java.beans.PropertyEditor 或 Spring 的 Converter 和 Formatter 组件。此外,你还可以使用 MVC 配置在全局共享的 FormattingConversionService 中注册 Converter 和 Formatter 类型。
@InitBinder 方法支持 @RequestMapping 方法支持的许多相同的参数,除了 @ModelAttribute(命令对象)参数。通常,它们被声明为带有 WebDataBinder 参数(用于注册)和 void 返回值。以下列表显示了一个示例:
Java
@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));
}
// ...
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
另外,当你通过共享的 FormattingConversionService 使用基于 Formatter 的设置时,你可以重复使用相同的方法,并注册特定于控制器的 Formatter 实现,如下面的示例所示:
Java
@Controller
public class FormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
// ...
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
1.5.1. 模型设计
在 Web 应用程序的上下文中,数据绑定涉及将 HTTP 请求参数(即表单数据或查询参数)绑定到模型对象及其嵌套对象的属性。
只有遵循 JavaBeans 命名约定的 public 属性才会暴露给数据绑定 —— 例如,firstName 属性的 public String getFirstName() 和 public void setFirstName(String) 方法。
Note:模型对象及其嵌套对象图有时也被称为命令对象(Command Object)、表单支持对象(Form-Backing Object)或 POJO(Plain Old Java Object)。
默认情况下,Spring 允许绑定到模型对象图中的所有公共属性。这意味着你需要仔细考虑模型具有哪些公共属性,因为客户端可能会针对任何公共属性路径,即使是在给定用例中不期望被针对的属性路径。
例如,给定一个 HTTP 表单数据端点,恶意客户端可能会为存在于模型对象图中但不是浏览器中呈现的 HTML 表单的一部分的属性提供值。这可能导致在模型对象及其任何嵌套对象上设置数据,而这些数据不期望被更新。
推荐的方法是使用一个专用的模型对象,只暴露与表单提交相关的属性。例如,在一个用于更改用户电子邮件地址的表单上,模型对象应声明一组最小的属性,如下面的 ChangeEmailForm 所示。
Java
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;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
如果你不能或不想为每个数据绑定用例使用专用的模型对象,你必须限制允许进行数据绑定的属性。理想情况下,你可以通过在 WebDataBinder 上调用 setAllowedFields() 方法来注册允许的字段模式。
例如,要在你的应用程序中注册允许的字段模式,你可以在 @Controller 或 @ControllerAdvice 组件中实现一个 @InitBinder 方法,如下所示:
Java
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
除了注册允许的模式外,还可以通过 DataBinder 及其子类中的 setDisallowedFields() 方法注册不允许的字段模式。但是,请注意,“允许列表” 比 “拒绝列表” 更安全。因此,应优先考虑 setAllowedFields() 而不是 setDisallowedFields()。
请注意,与允许的字段模式的匹配是区分大小写的,而与不允许的字段模式的匹配则不区分大小写。此外,即使一个字段恰好也匹配允许列表中的模式,如果它匹配了一个不允许的模式,也不会被接受。
Note
当直接将你的领域模型暴露给数据绑定时,正确配置允许和不允许的字段模式非常重要。否则,这将是一个巨大的安全风险。
此外,强烈建议你在数据绑定场景中不要使用来自你的领域模型的类型,如 JPA 或 Hibernate 实体作为模型对象。
1.6. 异常
@Controller 和 @ControllerAdvice 类可以拥有 @ExceptionHandler 方法来处理来自控制器方法的异常,如下面的示例所示:
Java
@Controller
public class SimpleController {
// ...
@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
异常可能匹配到正在传播的顶级异常(例如,直接抛出的 IOException)或者匹配到包装异常中的嵌套原因(例如,IOException 包装在 IllegalStateException 内部)。从 5.3 版本开始,这可以在任意原因级别进行匹配,而以前只考虑了直接的原因。
对于匹配异常类型,最好将目标异常声明为方法参数,如前面的示例所示。当多个异常方法匹配时,通常优先选择根异常匹配而不是原因异常匹配。更具体地说,ExceptionDepthComparator 用于根据异常类型从抛出异常类型的深度对异常进行排序。
或者,注解声明可能会缩小要匹配的异常类型,如下面的示例所示:
Java
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
// ...
}1
2
3
4
2
3
4
你甚至可以使用具有非常通用参数签名的特定异常类型列表,如下面的示例所示:
Java
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
// ...
}1
2
3
4
2
3
4
Note
根异常和原因异常匹配之间的区别可能会让人感到惊讶。
在前面显示的
IOException变体中,该方法通常会将实际的FileSystemException或RemoteException实例作为参数调用,因为它们都是从IOException扩展出来的。然而,如果任何这样的匹配异常在一个包装异常中传播,而这个包装异常本身就是一个IOException,那么传入的异常实例就是那个包装异常。在
handle(Exception)变体中的行为甚至更简单。在包装场景中,这总是用包装异常调用的,实际匹配的异常可以在这种情况下通过ex.getCause()找到。只有当这些被抛出为顶级异常时,传入的异常才是实际的FileSystemException或RemoteException实例。
我们通常建议你在参数签名中尽可能具体,以减少根异常和原因异常类型之间的不匹配的可能性。考虑将一个多匹配方法分解为多个 @ExceptionHandler 方法,每个方法通过其签名匹配一个特定的异常类型。
在多个 @ControllerAdvice 的安排中,我们建议你在一个优先级对应的 @ControllerAdvice 上声明你的主要根异常映射。虽然根异常匹配优先于原因,但这是在给定的控制器或 @ControllerAdvice 类的方法中定义的。这意味着在优先级更高的 @ControllerAdvice Bean 上的原因匹配优先于任何匹配(例如,根)在优先级较低的 @ControllerAdvice Bean 上。
最后但同样重要的是,@ExceptionHandler 方法实现可以选择通过以其原始形式重新抛出来退出处理给定的异常实例。这在你只对根级别的匹配或在无法静态确定的特定上下文中的匹配感兴趣的场景中非常有用。一个被重新抛出的异常会通过剩余的解析链传播,就好像给定的 @ExceptionHandler 方法一开始就没有匹配过一样。
Spring MVC 中对 @ExceptionHandler 方法的支持是建立在 DispatcherServlet 级别的 HandlerExceptionResolver 机制上的。
1.6.1. 方法参数
@ExceptionHandler 方法支持以下参数:
| 方法参数 | 描述 |
|---|---|
| Exception 类型 | 用于访问引发的异常。 |
HandlerMethod | 用于访问引发异常的控制器方法。 |
WebRequest,NativeWebRequest | 无需直接使用 Servlet API,即可通用地访问请求参数以及请求和会话属性。 |
javax.servlet.ServletRequest,javax.servlet.ServletResponse | 选择任何特定的请求或响应类型(例如,ServletRequest 或 HttpServletRequest 或 Spring 的 MultipartRequest 或 MultipartHttpServletRequest)。 |
javax.servlet.http.HttpSession | 强制存在会话。因此,这样的参数永远不会为 null。注意,会话访问不是线程安全的。如果允许多个请求并发访问会话,应考虑将 RequestMappingHandlerAdapter 实例的 synchronizeOnSession 标志设置为 true。 |
java.security.Principal | 当前已认证的用户 —— 可能是一个已知的特定 Principal 实现类。 |
HttpMethod | 请求的 HTTP 方法。 |
java.util.Locale | 当前请求的区域设置,由最具体的 LocaleResolver 确定 —— 实际上,是配置的 LocaleResolver 或 LocaleContextResolver。 |
java.util.TimeZone,java.time.ZoneId | 与当前请求关联的时区,由 LocaleContextResolver 确定。 |
java.io.OutputStream,java.io.Writer | 用于访问由 Servlet API 暴露的原始响应体。 |
java.util.Map,org.springframework.ui.Model,org.springframework.ui.ModelMap | 用于访问错误响应的模型。始终为空。 |
RedirectAttributes | 指定在重定向的情况下使用的属性 —— (即要附加到查询字符串的属性)和 Flash 属性,这些属性将被临时存储,直到重定向后的请求。参见重定向属性和 Flash 属性。 |
@SessionAttribute | 用于访问任何会话属性,与由于类级别 @SessionAttributes 声明而存储在会话中的模型属性形成对比。有关更多详细信息,请参见 @SessionAttribute。 |
@RequestAttribute | 用于访问请求属性。有关更多详细信息,请参见 @RequestAttribute。 |
@ExceptionHandler 方法支持的参数1.6.2. 返回值
@ExceptionHandler 方法支持以下返回值:
| 返回值 | 描述 |
|---|---|
@ResponseBody | 返回值通过 HttpMessageConverter 实例转换并写入响应。参见 @ResponseBody。 |
HttpEntity<B>,ResponseEntity<B> | 返回值指定整个响应(包括 HTTP 头和主体)通过 HttpMessageConverter 实例转换并写入响应。参见 ResponseEntity。 |
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 注解,那么就认为它已经完全处理了响应。如果控制器进行了积极的 ETag 或 lastModified 时间戳检查(参见控制器的详细信息),也是如此。如果以上都不是, void 返回类型也可以表示 REST 控制器的 “无响应主体” 或 HTML 控制器的默认视图名称选择。 |
| 任何其他返回值 | 如果返回值没有匹配到上述任何一项,并且不是简单类型(由 BeanUtils#isSimpleProperty 确定),那么默认情况下,它会被视为要添加到模型的模型属性。如果它是一个简单类型,它将保持未解决。 |
@ExceptionHandler 方法支持的返回值1.6.3. REST API 异常
对于 REST 服务来说,一个常见的需求是在响应体中包含错误详情。Spring 框架并不会自动做这个,因为响应体中错误详情的表示是特定于应用的。然而,@RestController 可以使用带有 ResponseEntity 返回值的 @ExceptionHandler 方法来设置响应的状态和主体。这样的方法也可以在 @ControllerAdvice 类中声明,以全局应用。
实现了全局异常处理并在响应体中包含错误详情的应用应该考虑扩展 ResponseEntityExceptionHandler,它为 Spring MVC 引发的异常提供处理,并提供自定义响应体的钩子。要使用这个,创建一个 ResponseEntityExceptionHandler 的子类,用 @ControllerAdvice 对其进行注解,覆盖必要的方法,并将其声明为 Spring Bean。
1.7. 控制器增强
@ExceptionHandler、@InitBinder 和 @ModelAttribute 方法只适用于它们所声明的 @Controller 类或类层次结构。如果它们是在 @ControllerAdvice 或 @RestControllerAdvice 类中声明的,那么它们适用于任何控制器。此外,从 5.3 版本开始,@ControllerAdvice 中的 @ExceptionHandler 方法可以用来处理来自任何 @Controller 或任何其他处理器的异常。
@ControllerAdvice 是用 @Component 元注解的,因此可以通过组件扫描将其注册为 Spring Bean。@RestControllerAdvice 是用 @ControllerAdvice 和 @ResponseBody 元注解的,这意味着 @ExceptionHandler 方法将通过响应体消息转换渲染其返回值,而不是通过 HTML 视图。
在启动时,RequestMappingHandlerMapping 和 ExceptionHandlerExceptionResolver 检测控制器增强 Bean 并在运行时应用它们。来自 @ControllerAdvice 的全局 @ExceptionHandler 方法在来自 @Controller 的本地方法之后应用。相反,全局 @ModelAttribute 和 @InitBinder 方法在本地方法之前应用。
@ControllerAdvice 注解具有让你缩小它们适用的控制器和处理器集合的属性。例如:
Java
// 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 {}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
前面示例中的选择器在运行时进行评估,如果大量使用,可能会对性能产生负面影响。有关更多详细信息,请参阅 @ControllerAdvice 的 JavaDoc。