Implementing Controllers
控制器提供了对应用程序行为的访问,这些行为通常通过一个服务接口来定义。控制器解释用户输入,并将其转换为由视图展示给用户的模型。Spring 以非常抽象的方式实现了控制器,使得你能够创建各种各样的控制器。
Spring 2.5 引入了一种基于注解的编程模型,用于 MVC 控制器,这种模型使用了如 @RequestMapping
、@RequestParam
、@ModelAttribute
等注解。此注解支持适用于 Servlet MVC 和 Portlet MVC。以这种风格实现的控制器不需要扩展特定的基类或实现特定的接口。此外,这些控制器通常不直接依赖于 Servlet 或 Portlet API,尽管你可以轻松地配置对 Servlet 或 Portlet 功能的访问。
java
@Controller
public class HelloWorldController {
@RequestMapping("/helloWorld")
public String helloWorld(Model model) {
model.addAttribute("message", "Hello World!");
return "helloWorld";
}
}
在 Spring MVC 中,@Controller
和 @RequestMapping
注解提供了灵活的方法命名和签名方式。在这个具体的例子中,方法接受一个 Model
参数并返回一个 String
类型的视图名称,但在后面的章节中解释了可以使用各种其他方法参数和返回值。@Controller
和 @RequestMapping
以及许多其他注解构成了 Spring MVC 实现的基础。本节将记录这些注解及其在 Servlet 环境中最常用的用法。
- @Controller 注解 :
- 这个注解用于标记一个类作为 Spring MVC 控制器。被标记为
@Controller
的类会被 Spring 框架扫描和注册为一个处理器,能够处理 Web 请求。
- 这个注解用于标记一个类作为 Spring MVC 控制器。被标记为
- @RequestMapping 注解 :
- 用于定义请求 URL 与处理器方法之间的映射关系。可以应用于类上和方法上,灵活地定义请求路径。
- 方法级别的
@RequestMapping
注解覆盖类级别的注解,并可以指定请求类型(GET, POST 等)以及路径变量、请求参数等。
- 方法参数和返回值 :
- 在 Spring MVC 中,控制器方法可以接受多种类型的参数和返回多种类型的值。例如:
- Model:用于在视图中传递数据。
- HttpServletRequest 和 HttpServletResponse:直接访问原始的请求和响应对象。
- @RequestParam:绑定请求参数到方法参数。
- @PathVariable:绑定 URL 路径变量到方法参数。
- @ModelAttribute:将请求参数绑定到一个模型对象,并将其添加到模型中。
- String:作为视图名称返回。
- ResponseEntity:用于构建复杂的响应,包括响应头和状态码。
- 在 Spring MVC 中,控制器方法可以接受多种类型的参数和返回多种类型的值。例如:
- 其他常用注解 :
- @GetMapping, @PostMapping 等:这些注解是
@RequestMapping
的特定类型,用于简化常见 HTTP 方法的映射。 - @RequestBody:将请求体绑定到方法参数。
- @ResponseBody:将方法返回值直接写入 HTTP 响应体,而不是解析为视图名称。
- @GetMapping, @PostMapping 等:这些注解是
示例代码
以下是一些常用注解的示例:
Java
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@Controller
public class ExampleController {
// 处理 GET 请求 /hello
@GetMapping("/hello")
public String hello(@RequestParam(name = "name", defaultValue = "World") String name, Model model) {
model.addAttribute("name", name);
return "hello"; // 返回视图名称 "hello"
}
// 处理 GET 请求 /user/{id}
@GetMapping("/user/{id}")
public String getUser(@PathVariable("id") Long userId, Model model) {
// 模拟获取用户数据
User user = userService.findById(userId);
model.addAttribute("user", user);
return "userProfile"; // 返回视图名称 "userProfile"
}
// 处理 POST 请求 /submit
@PostMapping("/submit")
public ResponseEntity<String> submit(@RequestBody FormData formData) {
// 处理表单数据
formService.process(formData);
return ResponseEntity.ok("Form submitted successfully");
}
}
- @Controller:标记类为控制器。
- @RequestMapping:定义请求映射。
- 多种参数和返回值:灵活处理请求和响应。
- 常用注解:简化开发过程,提供更直观的映射方式。
这些注解和方法构成了 Spring MVC 实现的基础,使得开发人员可以以非常灵活和简洁的方式处理 Web 请求。
Defining a controller with @Controller
@Controller
注解表明一个特定的类充当控制器的角色。Spring 并不要求你继承任何控制器基类或引用 Servlet API。然而,如果需要,你仍然可以引用特定于 Servlet 的特性。
@Controller
注解作为被注解类的一个立场标志,表明其角色。调度器会扫描这些被注解的类,以查找映射的方法,并检测 @RequestMapping
注解(见下一节)。
你可以在调度器的上下文中使用标准的 Spring bean 定义显式地定义带注解的控制器 bean。然而,@Controller
立场标志也允许自动检测,这与 Spring 通用的类路径组件类检测和自动注册 bean 定义的支持相一致。
为了启用此类注解控制器的自动检测,你需要在配置中添加组件扫描。使用 spring-context 模式,如以下 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.springframework.samples.petclinic.web"/>
<!-- ... -->
</beans>
解释
@Controller
注解:用于标记一个类为控制器。这个类不需要继承任何特定的基类,也不需要直接引用 Servlet API,但如果需要,可以引用特定的 Servlet 特性。@Controller
作为立场标志 :表明被注解类的角色。Spring 的调度器会扫描这些类以查找映射的方法,并检测@RequestMapping
注解。- 显式定义控制器 bean:可以在调度器的上下文中显式定义带注解的控制器 bean。
- 自动检测和注册 :
@Controller
允许自动检测,并与 Spring 的类路径组件类检测和自动注册支持一致。 - 组件扫描 :为了启用自动检测,需要在配置中添加组件扫描。示例中展示了如何使用
spring-context
模式来进行组件扫描。
Mapping Requests With @RequestMapping
你可以使用 @RequestMapping
注解将 URL(例如 /appointments)映射到整个类或特定的处理方法上。通常情况下,类级别的注解将特定的请求路径(或路径模式)映射到一个表单控制器上,而方法级别的注解则通过特定的 HTTP 请求方法(如 "GET"、"POST" 等)或 HTTP 请求参数条件来缩小主要映射范围。
以下来自 Petcare 示例的例子展示了一个在 Spring MVC 应用中使用此注解的控制器:
Java
@Controller
@RequestMapping("/appointments")
public class AppointmentsController {
private final AppointmentBook appointmentBook;
@Autowired
public AppointmentsController(AppointmentBook appointmentBook) {
this.appointmentBook = appointmentBook;
}
@RequestMapping(method = RequestMethod.GET)
public Map<String, Appointment> get() {
return appointmentBook.getAppointmentsForToday();
}
@RequestMapping(path = "/{day}", method = RequestMethod.GET)
public Map<String, Appointment> getForDay(@PathVariable @DateTimeFormat(iso=ISO.DATE) Date day, Model model) {
return appointmentBook.getAppointmentsForDay(day);
}
@RequestMapping(path = "/new", method = RequestMethod.GET)
public AppointmentForm getNewForm() {
return new AppointmentForm();
}
@RequestMapping(method = RequestMethod.POST)
public String add(@Valid AppointmentForm appointment, BindingResult result) {
if (result.hasErrors()) {
return "appointments/new";
}
appointmentBook.addAppointment(appointment);
return "redirect:/appointments";
}
}
在上面的例子中,@RequestMapping
被多次使用。首先在类型(类)级别使用,表明该控制器中的所有处理方法都相对于 /appointments
路径。get()
方法进一步使用了 @RequestMapping
注解:它只接受 GET 请求,这意味着对 /appointments
的 HTTP GET 请求将调用这个方法。add()
方法有类似的定义,而 getNewForm()
方法则将 HTTP 方法和路径的定义结合在一起,因此对 appointments/new
的 GET 请求将由该方法处理。
getForDay()
方法展示了 @RequestMapping
的另一种用法:URI 模板。(参见名为"URI 模板模式"的部分)
类级别的 @RequestMapping
不是必须的。如果没有它,所有路径都是绝对路径,而不是相对路径。以下来自 PetClinic 示例应用程序的例子展示了一个使用 @RequestMapping
的多操作控制器:
java
@Controller
public class ClinicController {
private final Clinic clinic;
@Autowired
public ClinicController(Clinic clinic) {
this.clinic = clinic;
}
@RequestMapping("/")
public void welcomeHandler() {
}
@RequestMapping("/vets")
public ModelMap vetsHandler() {
return new ModelMap(this.clinic.getVets());
}
}
上面的例子没有指定 GET、PUT、POST 等 HTTP 方法,因为 @RequestMapping
默认映射所有的 HTTP 方法。使用 @RequestMapping(method=GET)
或 @GetMapping
可以缩小映射范围。
Composed @RequestMapping Variants
组合的 @RequestMapping
变体
Spring Framework 4.3 引入了以下方法级别的组合变体 @RequestMapping
注解,这些注解有助于简化常见 HTTP 方法的映射,并更好地表达被注解处理方法的语义。例如,@GetMapping
可以被理解为 GET @RequestMapping
。
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
以下示例展示了一个修改后的 AppointmentsController
,与上一节相比,使用组合的 @RequestMapping
注解进行了简化。
java
@Controller
@RequestMapping("/appointments")
public class AppointmentsController {
private final AppointmentBook appointmentBook;
@Autowired
public AppointmentsController(AppointmentBook appointmentBook) {
this.appointmentBook = appointmentBook;
}
@GetMapping
public Map<String, Appointment> get() {
return appointmentBook.getAppointmentsForToday();
}
@GetMapping("/{day}")
public Map<String, Appointment> getForDay(@PathVariable @DateTimeFormat(iso=ISO.DATE) Date day, Model model) {
return appointmentBook.getAppointmentsForDay(day);
}
@GetMapping("/new")
public AppointmentForm getNewForm() {
return new AppointmentForm();
}
@PostMapping
public String add(@Valid AppointmentForm appointment, BindingResult result) {
if (result.hasErrors()) {
return "appointments/new";
}
appointmentBook.addAppointment(appointment);
return "redirect:/appointments";
}
}
@Controller and AOP Proxying
在某些情况下,控制器可能需要在运行时被 AOP 代理装饰。一个例子是如果你选择在控制器上直接使用 @Transactional
注解。当这种情况发生时,特别是对于控制器,我们建议使用基于类的代理。这通常是控制器的默认选择。然而,如果控制器必须实现一个非 Spring 上下文回调的接口(例如 InitializingBean
,*Aware
等),你可能需要显式配置基于类的代理。例如,使用 <tx:annotation-driven/>
时,可以更改为 <tx:annotation-driven proxy-target-class="true"/>
。
New Support Classes for @RequestMapping methods in Spring MVC 3.1
Spring MVC 3.1 中 @RequestMapping
方法的新支持类
Spring 3.1 引入了一组新的 @RequestMapping
方法支持类,分别是 RequestMappingHandlerMapping
和 RequestMappingHandlerAdapter
。推荐使用这些支持类,它们甚至是利用 Spring MVC 3.1 及以后版本的新特性所必需的。通过 MVC 命名空间和 MVC Java 配置,这些新的支持类默认启用,但如果不使用这些配置,则必须显式配置。以下部分描述了旧支持类和新支持类之间的一些重要区别。
在 Spring 3.1 之前,类型级和方法级的请求映射分两个阶段进行检查------首先由 DefaultAnnotationHandlerMapping
选择控制器,然后由 AnnotationMethodHandlerAdapter
缩小要调用的具体方法。
在 Spring 3.1 中,新支持类 RequestMappingHandlerMapping
是唯一决定哪个方法应该处理请求的地方。可以将控制器方法视为一组独特的端点,每个方法的映射信息都来自类型和方法级的 @RequestMapping
信息。
这使得一些新的可能性成为现实。例如,HandlerInterceptor
或 HandlerExceptionResolver
现在可以期望基于对象的处理器是 HandlerMethod
,这使得它们可以检查确切的方法、其参数和关联的注解。URL 的处理不再需要分布在不同的控制器中。
此外,有些事情不再可能:
- 先使用
SimpleUrlHandlerMapping
或BeanNameUrlHandlerMapping
选择一个控制器,然后根据@RequestMapping
注解缩小方法范围。 - 依赖方法名作为回退机制来消除两个没有明确路径映射但在其他方面匹配的
@RequestMapping
方法之间的歧义,例如通过 HTTP 方法。在新支持类中,@RequestMapping
方法必须唯一映射。 - 只有一个默认方法(没有明确路径映射),在没有其他控制器方法更具体匹配时处理请求。在新支持类中,如果找不到匹配的方法,则会引发 404 错误。
以上特性在现有的支持类中仍然受支持。然而,要利用 Spring MVC 3.1 的新特性,你需要使用新的支持类。
URI Template Patterns
URI 模板可以用于在 @RequestMapping
方法中方便地访问 URL 的选定部分。
URI 模板是一个类似 URI 的字符串,包含一个或多个变量名。当你为这些变量替换值时,模板就变成了一个 URI。URI 模板的建议 RFC 定义了 URI 如何参数化。例如,URI 模板 https://www.example.com/users/{userId}
包含变量 userId
。将值 fred
分配给变量后,得到的 URI 是 https://www.example.com/users/fred
。
在 Spring MVC 中,你可以在方法参数上使用 @PathVariable
注解,将其绑定到 URI 模板变量的值:
java
@GetMapping("/users/{userId}")
public String getUser(@PathVariable String userId, Model model) {
// 使用 userId 参数进行处理
return "userProfile";
}
在这个例子中,@PathVariable
注解将方法参数 userId
绑定到 URI 模板变量 userId
的值。这样,/users/fred
的请求会将字符串 fred
作为 userId
参数传递给 getUser
方法。
- 为了处理
@PathVariable
注解,Spring MVC 需要通过名称找到匹配的 URI 模板变量。你可以在注解中指定它:
java
@GetMapping("/owners/{ownerId}")
public String findOwner(@PathVariable("ownerId") String theOwner, Model model) {
// 实现省略
}
- 或者,如果 URI 模板变量名与方法参数名匹配,你可以省略该细节。只要你的代码是用调试信息编译的,或者在 Java 8 上使用
parameters
编译标志,Spring MVC 就会将方法参数名与 URI 模板变量名匹配:
java
@GetMapping("/owners/{ownerId}")
public String findOwner(@PathVariable String ownerId, Model model) {
// 实现省略
}
- 一个方法可以有任意数量的
@PathVariable
注解:
java
@GetMapping("/owners/{ownerId}/pets/{petId}")
public String findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
Owner owner = ownerService.findOwner(ownerId);
Pet pet = owner.getPet(petId);
model.addAttribute("pet", pet);
return "displayPet";
}
-
当
@PathVariable
注解用于Map<String, String>
参数时,地图会填充所有的 URI 模板变量。 -
一个 URI 模板可以通过类型和方法级别的
@RequestMapping
注解来组装。因此,可以使用类似/owners/42/pets/21
的 URL 来调用findPet()
方法。
java
@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {
@RequestMapping("/pets/{petId}")
public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
// 实现省略
}
}
@PathVariable
参数可以是任何简单类型,例如 int
、long
、Date
等。Spring 会自动转换为适当的类型,如果转换失败,会抛出 TypeMismatchException
。你也可以注册支持解析其他数据类型的支持。请参阅"方法参数和类型转换"以及"自定义 WebDataBinder 初始化"部分。
URI Template Patterns with Regular Expressions
有时你需要更精确地定义 URI 模板变量。考虑 URL "/spring-web/spring-web-3.0.5.jar"
,如何将其分解成多个部分?
@RequestMapping
注解支持在 URI 模板变量中使用正则表达式。语法是 {varName:regex}
,其中第一部分定义变量名,第二部分是正则表达式。例如:
java
@RequestMapping("/spring-web/{symbolicName:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{extension:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String extension) {
// ...
}
在这个例子中,@RequestMapping
注解使用了正则表达式来匹配 URI 模板变量。{symbolicName:[a-z-]+}
匹配符号名称部分,{version:\\d\\.\\d\\.\\d}
匹配版本号部分,而 {extension:\\.[a-z]+}
匹配扩展名部分。这样,你可以更精确地处理复杂的 URL 模式。
Path Patterns
路径模式
除了 URI 模板,@RequestMapping
注解和所有组合的 @RequestMapping
变体还支持 Ant 风格的路径模式(例如,/myPath/*.do
)。URI 模板变量和 Ant 风格的通配符也可以组合使用(例如,/owners/*/pets/{petId}
)。
Path Pattern Comparison
路径模式比较
当一个 URL 匹配多个模式时,使用排序来找到最具体的匹配。
具有较少 URI 变量和通配符的模式被认为是更具体的。例如,/hotels/{hotel}/*
具有 1 个 URI 变量和 1 个通配符,被认为比 /hotels/{hotel}/**
更具体,后者有 1 个 URI 变量和 2 个通配符。
如果两个模式的变量和通配符数量相同,较长的模式被认为更具体。例如,/foo/bar*
比 /foo/*
更长,因此被认为更具体。
当两个模式的变量和长度相同时,通配符较少的模式被认为更具体。例如,/hotels/{hotel}
比 /hotels/*
更具体。
还有一些其他的特殊规则:
- 默认映射模式
/**
比其他任何模式都不具体。例如,/api/{a}/{b}/{c}
更具体。 - 像
/public/**
这样的前缀模式比不包含双通配符的其他模式更不具体。例如,/public/path3/{a}/{b}/{c}
更具体。
详细信息请参阅 AntPatternComparator
在 AntPathMatcher
中。请注意,PathMatcher
可以自定义(详见配置 Spring MVC 部分的 "路径匹配")。
Path Patterns with Placeholders
使用占位符的路径模式
@RequestMapping
注解中的模式支持使用 ${...}
占位符来匹配本地属性和/或系统属性以及环境变量。这在控制器映射的路径需要通过配置进行定制时非常有用。有关占位符的更多信息,请参阅 PropertyPlaceholderConfigurer
类的 Java 文档。
Suffix Pattern Matching
后缀模式匹配
默认情况下,Spring MVC 执行 ".*" 后缀模式匹配,因此映射到 /person
的控制器也隐式地映射到 /person.*
。这使得通过 URL 路径请求资源的不同表示变得容易(例如 /person.pdf
, /person.xml
)。
可以关闭后缀模式匹配或将其限制为一组明确注册用于内容协商的路径扩展名。通常建议这样做以最小化与常见请求映射(如 /person/{id}
)的歧义,其中点可能并不代表文件扩展名,例如 /person/joe@email.com
与 /person/joe@email.com.json
。此外,正如下面的注释中所解释的,后缀模式匹配以及内容协商在某些情况下可能被用于尝试恶意攻击,因此有充分的理由有意义地限制它们。
有关后缀模式匹配配置的信息,请参阅 Section 22.16.11, "Path Matching";有关内容协商配置的信息,请参阅 Section 22.16.6, "Content Negotiation"。
Suffix Pattern Matching and RFD
后缀模式匹配与反射文件下载(RFD)
反射文件下载(RFD)攻击首次在 Trustwave 2014 年的一篇论文中描述。该攻击类似于 XSS,因为它依赖于输入(例如查询参数、URI 变量)在响应中反射。然而,与在 HTML 中插入 JavaScript 不同,RFD 攻击依赖于浏览器切换到执行下载并根据文件扩展名(例如 .bat, .cmd)将响应视为可执行脚本(如果双击)。
在 Spring MVC 中,@ResponseBody
和 ResponseEntity
方法处于风险中,因为它们可以呈现不同的内容类型,客户端可以通过 URL 路径扩展名请求这些内容。然而,请注意,仅禁用后缀模式匹配或禁用用于内容协商的路径扩展名使用不足以防止 RFD 攻击。
为了全面防范 RFD,Spring MVC 在呈现响应体之前添加了 Content-Disposition:inline;filename=f.txt
标头,以建议一个固定且安全的下载文件名。仅当 URL 路径包含一个既不在白名单中也未明确注册用于内容协商的文件扩展名时才会这样做。然而,当 URL 直接输入到浏览器中时,这可能会有副作用。
许多常见的路径扩展名默认情况下被列入白名单。此外,REST API 调用通常不应直接在浏览器中使用。然而,使用自定义 HttpMessageConverter
实现的应用程序可以明确注册文件扩展名用于内容协商,对于这些扩展名将不会添加 Content-Disposition
标头。参见 Section 22.16.6, "Content Negotiation"。
注意 这最初是作为 CVE-2015-5211 的一部分引入的。以下是报告中的其他建议:
- 编码而不是转义 JSON 响应。这也是 OWASP XSS 的建议。有关如何使用 Spring 进行此操作的示例,请参见 spring-jackson-owasp。
- 将后缀模式匹配配置为关闭或限制为仅显式注册的后缀。
- 使用
useJaf
和ignoreUnknownPathExtensions
属性将内容协商配置为 false,这将导致对于具有未知扩展名的 URL 返回 406 响应。然而,如果 URL 自然地预期在末尾有一个点,这可能不是一个选项。 - 向响应添加
X-Content-Type-Options: nosniff
标头。Spring Security 4 默认执行此操作。
Matrix Variables
矩阵变量
URI 规范 RFC 3986 定义了在路径段中包含键值对的可能性。规范中没有使用特定的术语。通常使用"URI 路径参数"这一通用术语,虽然来源于 Tim Berners-Lee 的旧帖子中的更独特的"矩阵 URI"也常被使用并且相当知名。在 Spring MVC 中,这些被称为矩阵变量。
矩阵变量可以出现在任何路径段中,每个矩阵变量用";"(分号)分隔。例如:"/cars;color=red;year=2012"。多个值可以用","(逗号)分隔"color=red,green,blue",或者变量名可以重复"color=red;color=green;color=blue"。
如果预期 URL 包含矩阵变量,请求映射模式必须使用 URI 模板表示。这确保了无论矩阵变量是否存在以及以何种顺序提供,请求都可以正确匹配。
下面是提取矩阵变量"q"的示例:
java
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
由于所有路径段都可能包含矩阵变量,有时需要更具体地标识预期变量的位置:
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
}
可以将矩阵变量定义为可选,并指定默认值:
java
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
可以在 Map 中获取所有矩阵变量:
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" : 11, "s" : 23]
}
请注意,要启用矩阵变量的使用,必须将 RequestMappingHandlerMapping
的 removeSemicolonContent
属性设置为 false。默认情况下,它设置为 true。
【提示】
MVC Java 配置和 MVC 命名空间都提供了启用矩阵变量的选项。
如果使用 Java 配置,"使用 MVC Java 配置进行高级自定义"部分描述了如何自定义 RequestMappingHandlerMapping
。
在 MVC 命名空间中,<mvc:annotation-driven>
元素有一个 enable-matrix-variables
属性,应将其设置为 true。默认情况下,它设置为 false。
示例配置:
xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven enable-matrix-variables="true"/>
</beans>
Consumable Media Types
可消费的媒体类型
你可以通过指定可消费的媒体类型列表来缩小主映射范围。只有当请求头中的 Content-Type 与指定的媒体类型匹配时,请求才会被匹配。例如:
java
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet, Model model) {
// 省略实现
}
可消费的媒体类型表达式也可以使用否定形式,如 !text/plain
,以匹配除 Content-Type 为 text/plain
的所有请求。还可以考虑使用 MediaType
提供的常量,如 APPLICATION_JSON_VALUE
和 APPLICATION_JSON_UTF8_VALUE
。
【提示】
consumes
条件支持在类型级别和方法级别使用。与大多数其他条件不同,在类型级别使用时,方法级别的可消费类型会覆盖类型级别的可消费类型,而不是扩展它们。
Producible Media Types
可生成的媒体类型
你可以通过指定一组可生成的媒体类型来缩小主映射范围。只有当 Accept
请求头匹配这些值之一时,请求才会被匹配。此外,使用 produces
条件可以确保用于生成响应的实际内容类型符合 produces
条件中指定的媒体类型。例如:
java
@GetMapping(path = "/pets/{petId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Pet getPet(@PathVariable String petId, Model model) {
// 实现省略
}
[注意]
请注意,produces
条件中指定的媒体类型也可以可选地指定字符集。例如,在上面的代码片段中,我们指定了与 MappingJackson2HttpMessageConverter
中默认配置相同的媒体类型,包括 UTF-8 字符集。
就像 consumes
一样,可生成的媒体类型表达式也可以用否定形式,例如 !text/plain
,以匹配所有 Accept
请求头值不是 text/plain
的请求。同时可以考虑使用 MediaType
提供的常量,如 APPLICATION_JSON_VALUE
和 APPLICATION_JSON_UTF8_VALUE
。
[提示]
produces
条件支持在类型级别和方法级别使用。与大多数其他条件不同,当在类型级别使用时,方法级别的可生成类型会覆盖而不是扩展类型级别的可生成类型。
Request Parameters and Header Values
请求参数条件
你可以通过请求参数条件来缩小请求匹配范围,比如 "myParam"
,"!myParam"
,或 "myParam=myValue"
。前两个用于测试请求参数的存在/不存在,第三个用于测试特定参数值。以下是一个带有请求参数值条件的示例:
java
@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
// 实现省略
}
}
同样的方式可以用于测试请求头的存在/不存在或根据特定请求头值进行匹配:
java
@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {
@GetMapping(path = "/pets", headers = "myHeader=myValue")
public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
// 实现省略
}
}
[提示]
虽然你可以使用媒体类型通配符(例如 "content-type=text/*"
会匹配 "text/plain"
和 "text/html"
)来匹配 Content-Type
和 Accept
请求头的值,但推荐分别使用 consumes
和 produces
条件。这些条件专门用于此目的。
HTTP HEAD and HTTP OPTIONS
@RequestMapping 方法和 HTTP 方法的映射
映射到 "GET" 的 @RequestMapping 方法也会隐式映射到 "HEAD",即不需要显式声明 "HEAD"。HTTP HEAD 请求的处理方式类似于 HTTP GET 请求,只是不会写入响应体,而是计算字节数并设置 "Content-Length" 头。
@RequestMapping 方法内置对 HTTP OPTIONS 的支持。默认情况下,HTTP OPTIONS 请求通过设置 "Allow" 响应头来处理,该响应头包含所有与匹配的 URL 模式显式声明的 HTTP 方法。当没有显式声明 HTTP 方法时,"Allow" 头会被设置为 "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS"。理想情况下,应始终声明 @RequestMapping 方法要处理的 HTTP 方法,或者使用专门的组合 @RequestMapping 变体(参见"组合 @RequestMapping 变体"部分)。
尽管没有必要,@RequestMapping 方法也可以映射到并处理 HTTP HEAD 或 HTTP OPTIONS,或两者。
Defining @RequestMapping handler methods
@RequestMapping 处理方法的灵活签名
@RequestMapping 处理方法可以有非常灵活的签名。支持的方法参数和返回值在以下部分中进行了描述。大多数参数可以以任意顺序使用,唯一的例外是 BindingResult 参数。这将在下一节中详细描述。
注意 :
Spring 3.1 引入了一组用于 @RequestMapping 方法的支持类,分别称为 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter。建议使用它们,甚至在利用 Spring MVC 3.1 及以后版本的新功能时是必须的。这些新的支持类默认在 MVC 命名空间中启用,并且可以与 MVC Java 配置一起使用,但如果两者都不使用,则必须显式配置。
Supported method argument types
支持的方法参数类型
以下是支持的方法参数类型:
-
请求或响应对象(Servlet API):选择任何特定的请求或响应类型,例如 ServletRequest 或 HttpServletRequest。
-
会话对象(Servlet API):类型为 HttpSession。这种类型的参数强制要求存在相应的会话。因此,这样的参数永远不会为 null。
注意: 会话访问可能不是线程安全的,特别是在 Servlet 环境中。如果允许多个请求并发访问会话,请考虑将 RequestMappingHandlerAdapter 的 "synchronizeOnSession" 标志设置为 "true"。
-
org.springframework.web.context.request.WebRequest 或 org.springframework.web.context.request.NativeWebRequest:允许进行通用的请求参数访问以及请求/会话属性访问,而无需绑定到原生的 Servlet/Portlet API。
-
java.util.Locale:用于当前请求的区域设置,由最具体的区域解析器确定,在 MVC 环境中为配置的 LocaleResolver / LocaleContextResolver。
-
java.util.TimeZone(Java 6+) / java.time.ZoneId(在 Java 8 上):用于与当前请求关联的时区,由 LocaleContextResolver 确定。
-
java.io.InputStream / java.io.Reader:用于访问请求的内容。这是 Servlet API 暴露的原始 InputStream/Reader。
-
java.io.OutputStream / java.io.Writer:用于生成响应的内容。这是 Servlet API 暴露的原始 OutputStream/Writer。
-
org.springframework.http.HttpMethod:用于 HTTP 请求方法。
-
java.security.Principal:包含当前已认证的用户。
-
使用 @PathVariable 注解的参数:用于访问 URI 模板变量。参见"URI 模板模式"部分。
-
使用 @MatrixVariable 注解的参数:用于访问 URI 路径段中的名值对。参见"矩阵变量"部分。
-
使用 @RequestParam 注解的参数:用于访问特定的 Servlet 请求参数。参数值将转换为声明的方法参数类型。参见"使用 @RequestParam 将请求参数绑定到方法参数"部分。
-
使用 @RequestHeader 注解的参数:用于访问特定的 Servlet 请求 HTTP 头。参数值将转换为声明的方法参数类型。参见"使用 @RequestHeader 映射请求头属性"部分。
-
使用 @RequestBody 注解的参数:用于访问 HTTP 请求体。参数值将使用 HttpMessageConverters 转换为声明的方法参数类型。参见"使用 @RequestBody 映射请求体"部分。
-
使用 @RequestPart 注解的参数:用于访问 "multipart/form-data" 请求部分的内容。参见"从编程客户端处理文件上传请求"部分和"Spring 的 multipart(文件上传)支持"部分。
-
使用 @SessionAttribute 注解的参数:用于访问现有的、永久性的会话属性(例如用户认证对象),而不是在控制器工作流中通过 @SessionAttributes 暂时存储在会话中的模型属性。
-
使用 @RequestAttribute 注解的参数:用于访问请求属性。
-
HttpEntity<?> 参数:用于访问 Servlet 请求的 HTTP 头和内容。请求流将使用 HttpMessageConverters 转换为实体体。参见"使用 HttpEntity"部分。
-
java.util.Map / org.springframework.ui.Model / org.springframework.ui.ModelMap:用于丰富暴露给 Web 视图的隐式模型。
-
org.springframework.web.servlet.mvc.support.RedirectAttributes:用于指定重定向时使用的确切属性集,并添加 flash 属性(在服务器端暂时存储以便在重定向后提供给请求的属性)。参见"将数据传递到重定向目标"部分和"使用 flash 属性"部分。
-
命令或表单对象:用于将请求参数绑定到 bean 属性(通过 setter)或直接绑定到字段,具有可自定义的类型转换,具体取决于 @InitBinder 方法和/或 HandlerAdapter 配置。参见 RequestMappingHandlerAdapter 的 webBindingInitializer 属性。这些命令对象及其验证结果将默认作为模型属性暴露,使用命令类名,例如类型为 some.package.OrderAddress 的命令对象的模型属性名为 "orderAddress"。可以在方法参数上使用 ModelAttribute 注解来自定义使用的模型属性名。
-
org.springframework.validation.Errors / org.springframework.validation.BindingResult:用于前面的命令或表单对象的验证结果(紧跟在方法参数之后的参数)。
-
org.springframework.web.bind.support.SessionStatus:用于标记表单处理已完成,从而触发清理在处理程序类型级别由 @SessionAttributes 注解指示的会话属性。
-
org.springframework.web.util.UriComponentsBuilder:用于准备相对于当前请求的主机、端口、方案、上下文路径和 servlet 映射的文字部分的 URL 的构建器。
Errors 或 BindingResult 参数必须紧跟在绑定的模型对象之后,因为方法签名可能有多个模型对象,Spring 将为每个模型对象创建一个单独的 BindingResult 实例,因此以下示例将无法工作:
java
@PostMapping
public String processSubmit(@ModelAttribute("pet") Pet pet, Model model, BindingResult result) {
// 实现省略
}
注意,Pet 和 BindingResult 之间有一个 Model 参数。要使其工作,你需要按如下方式重新排序参数:
java
@PostMapping
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result, Model model) {
// 实现省略
}
注意 :
JDK 1.8 的 java.util.Optional 支持作为具有 required 属性的注解(例如 @RequestParam、@RequestHeader 等)的方法参数类型。在这些情况下使用 java.util.Optional 等同于设置 required=false。
Supported method return types
支持的返回类型
以下是支持的返回类型:
- ModelAndView 对象,模型将隐式地通过命令对象和 @ModelAttribute 注解的参考数据访问方法进行丰富。
- Model 对象,视图名称通过 RequestToViewNameTranslator 隐式确定,模型将隐式地通过命令对象和 @ModelAttribute 注解的参考数据访问方法进行丰富。
- Map 对象,用于暴露模型,视图名称通过 RequestToViewNameTranslator 隐式确定,模型将隐式地通过命令对象和 @ModelAttribute 注解的参考数据访问方法进行丰富。
- View 对象,模型将隐式地通过命令对象和 @ModelAttribute 注解的参考数据访问方法进行确定。处理方法还可以通过声明 Model 参数(见上文)以编程方式丰富模型。
- String 值,被解释为逻辑视图名称,模型将隐式地通过命令对象和 @ModelAttribute 注解的参考数据访问方法进行确定。处理方法还可以通过声明 Model 参数(见上文)以编程方式丰富模型。
- void 如果方法自行处理响应(直接写入响应内容,声明一个 ServletResponse / HttpServletResponse 类型的参数以此目的)或视图名称通过 RequestToViewNameTranslator 隐式确定(处理方法签名中不声明响应参数)。
- 如果方法使用 @ResponseBody 注解,则返回类型写入响应 HTTP 主体。返回值将使用 HttpMessageConverters 转换为声明的方法参数类型。参见"使用 @ResponseBody 映射响应主体"部分。
- HttpEntity<?> 或 ResponseEntity<?> 对象,用于访问 Servlet 响应 HTTP 头和内容。实体主体将使用 HttpMessageConverters 转换为响应流。参见"使用 HttpEntity"部分。
- HttpHeaders 对象,返回一个无主体的响应。
- Callable<?>,当应用程序希望在由 Spring MVC 管理的线程中异步生成返回值时返回。
- DeferredResult<?>,当应用程序希望从自己选择的线程中生成返回值时返回。
- ListenableFuture<?> 或 CompletableFuture<?>/CompletionStage<?>,当应用程序希望通过线程池提交生成值时返回。
- ResponseBodyEmitter,用于异步地将多个对象写入响应;也支持作为 ResponseEntity 的主体。
- SseEmitter,用于异步地将服务器发送事件写入响应;也支持作为 ResponseEntity 的主体。
- StreamingResponseBody,用于异步地将内容写入响应 OutputStream;也支持作为 ResponseEntity 的主体。
- 任何其他返回类型都被视为要暴露给视图的单个模型属性,使用方法级别上的 @ModelAttribute 指定的属性名(或基于返回类型类名的默认属性名)。模型将隐式地通过命令对象和 @ModelAttribute 注解的参考数据访问方法进行丰富。
Binding request parameters to method parameters with @RequestParam
使用 @RequestParam 注解将请求参数绑定到控制器中的方法参数。
以下代码片段展示了该注解的用法:
java
@Controller
@RequestMapping("/pets")
@SessionAttributes("pet")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, ModelMap model) {
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
默认情况下,使用此注解的参数是必需的,但您可以通过将 @RequestParam 的 required 属性设置为 false 来指定参数是可选的(例如,@RequestParam(name="id", required=false))。
如果目标方法参数类型不是 String,则会自动应用类型转换。详见"方法参数和类型转换"部分。
当 @RequestParam 注解用于 Map<String, String> 或 MultiValueMap<String, String> 参数时,地图会填充所有请求参数。
Mapping the request body with the @RequestBody annotation
使用 @RequestBody 注解映射请求体
@RequestBody 方法参数注解表明一个方法参数应绑定到 HTTP 请求体的值。例如:
java
@PutMapping("/something")
public void handle(@RequestBody String body, Writer writer) throws IOException {
writer.write(body);
}
您可以使用 HttpMessageConverter 将请求体转换为方法参数。HttpMessageConverter 负责从 HTTP 请求消息转换为对象,并从对象转换为 HTTP 响应体。RequestMappingHandlerAdapter 使用以下默认的 HttpMessageConverters 来支持 @RequestBody 注解:
- ByteArrayHttpMessageConverter 将字节数组转换。
- StringHttpMessageConverter 将字符串转换。
- FormHttpMessageConverter 将表单数据转换为/从 MultiValueMap<String, String>。
- SourceHttpMessageConverter 将 javax.xml.transform.Source 转换为/从。
有关这些转换器的更多信息,请参阅消息转换器。还请注意,如果使用 MVC 命名空间或 MVC Java 配置,默认情况下会注册更广泛的消息转换器。有关更多信息,请参见第 22.16.1 节 "启用 MVC Java 配置或 MVC XML 命名空间"。
如果您打算读取和写入 XML,您需要配置 MarshallingHttpMessageConverter 和特定的 Marshaller 以及 org.springframework.oxm 包中的 Unmarshaller 实现。以下示例展示了如何在配置中直接执行此操作,但如果您的应用程序通过 MVC 命名空间或 MVC Java 配置进行配置,请参见第 22.16.1 节 "启用 MVC Java 配置或 MVC XML 命名空间"。
xml
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<util:list id="beanList">
<ref bean="stringHttpMessageConverter"/>
<ref bean="marshallingHttpMessageConverter"/>
</util:list>
</property>
</bean>
<bean id="stringHttpMessageConverter"
class="org.springframework.http.converter.StringHttpMessageConverter"/>
<bean id="marshallingHttpMessageConverter"
class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
<property name="marshaller" ref="castorMarshaller"/>
<property name="unmarshaller" ref="castorMarshaller"/>
</bean>
<bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller"/>
@RequestBody 方法参数可以使用 @Valid 注解进行标注,在这种情况下,它将使用配置的 Validator 实例进行验证。当使用 MVC 命名空间或 MVC Java 配置时,如果类路径中有 JSR-303 实现,JSR-303 验证器会自动配置。
与 @ModelAttribute 参数类似,可以使用 Errors 参数来检查错误。如果没有声明此类参数,将引发 MethodArgumentNotValidException。该异常在 DefaultHandlerExceptionResolver 中处理,会向客户端发送 400 错误。
[注意]
有关通过 MVC 命名空间或 MVC Java 配置来配置消息转换器和验证器的信息,请参见第 22.16.1 节 "启用 MVC Java 配置或 MVC XML 命名空间"。
Mapping the response body with the @ResponseBody annotation
使用 @ResponseBody 注解映射响应体
@ResponseBody 注解类似于 @RequestBody。这个注解可以放在方法上,并表明返回类型应该直接写入 HTTP 响应体(而不是放在 Model 中,或解释为视图名称)。例如:
java
@GetMapping("/something")
@ResponseBody
public String helloWorld() {
return "Hello World";
}
上述示例将导致文本 "Hello World" 被写入 HTTP 响应流。
与 @RequestBody 类似,Spring 使用 HttpMessageConverter 将返回的对象转换为响应体。有关这些转换器的更多信息,请参阅上一节和消息转换器。
Creating REST Controllers with the @RestController annotation
在实现 REST API 时,通常会使用控制器来仅提供 JSON、XML 或自定义 MediaType 内容。为了方便起见,可以在控制器类上使用 @RestController 注解,而不是在所有的 @RequestMapping 方法上都加上 @ResponseBody 注解。
@RestController 是一个组合了 @ResponseBody 和 @Controller 的标注注解。除此之外,它还为您的控制器赋予了更多的意义,并且在框架的未来版本中可能会携带更多的语义。
与普通的 @Controller 类似,@RestController 也可以由 @ControllerAdvice 或 @RestControllerAdvice bean 来辅助。有关详细信息,请参阅"使用 @ControllerAdvice 和 @RestControllerAdvice 对控制器进行建议"的章节。
Using HttpEntity
HttpEntity 类似于 @RequestBody 和 @ResponseBody。除了可以访问请求和响应的主体,HttpEntity(以及专门用于响应的子类 ResponseEntity)还允许访问请求和响应的头信息,如下所示:
java
@RequestMapping("/something")
public ResponseEntity<String> handle(HttpEntity<byte[]> requestEntity) throws UnsupportedEncodingException {
String requestHeader = requestEntity.getHeaders().getFirst("MyRequestHeader");
byte[] requestBody = requestEntity.getBody();
// 对请求头和请求体进行处理
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("MyResponseHeader", "MyValue");
return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED);
}
上面的示例获取了名为 MyRequestHeader 的请求头的值,并将请求体读取为字节数组。它向响应中添加了 MyResponseHeader,向响应流中写入 "Hello World",并将响应状态码设置为 201(Created)。
与 @RequestBody 和 @ResponseBody 类似,Spring 使用 HttpMessageConverter 来转换请求和响应流中的数据。有关这些转换器的更多信息,请参阅前面的部分和消息转换器章节。
Using @ModelAttribute on a method
@ModelAttribute 注解可以用于方法或方法参数。本节解释了它在方法上的使用,而下一节将解释它在方法参数上的使用。
在方法上使用 @ModelAttribute 表示该方法的目的是添加一个或多个模型属性。这样的方法支持与 @RequestMapping 方法相同的参数类型,但不能直接映射到请求上。相反,同一个控制器中的 @ModelAttribute 方法会在 @RequestMapping 方法之前被调用。以下是几个例子:
java
// 添加一个属性
// 方法的返回值作为名为 "account" 的模型属性添加到模型中
// 您可以通过 @ModelAttribute("myAccount") 自定义名称
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountManager.findAccount(number);
}
// 添加多个属性
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountManager.findAccount(number));
// 添加更多...
}
@ModelAttribute 方法用于填充模型中常用的属性,例如填充下拉列表的州或宠物类型,或检索一个像 Account 这样的命令对象,以便在 HTML 表单上表示数据。后一种情况将在下一节进一步讨论。
请注意 @ModelAttribute 方法的两种风格。在第一种风格中,方法通过返回值隐式地添加一个属性。在第二种风格中,方法接受一个 Model 参数,并向其中添加任意数量的模型属性。您可以根据需要选择这两种风格中的任意一种。
一个控制器可以有任意数量的 @ModelAttribute 方法。所有这些方法都会在同一个控制器的 @RequestMapping 方法之前被调用。
@ModelAttribute 方法也可以定义在 @ControllerAdvice 注解的类中,这样的方法适用于多个控制器。有关详细信息,请参阅 "Advising controllers with @ControllerAdvice and @RestControllerAdvice" 章节。
[提示]
当模型属性名称未明确指定时会发生什么?在这种情况下,基于其类型将分配一个默认名称给模型属性。例如,如果方法返回类型为 Account 的对象,默认名称为 "account"。您可以通过 @ModelAttribute 注解的值来更改它。如果直接向 Model 添加属性,请使用适当的重载 addAttribute(...) 方法 - 即带有或不带有属性名称的方法。
@ModelAttribute 注解也可以用于 @RequestMapping 方法。在这种情况下,@RequestMapping 方法的返回值被解释为一个模型属性,而不是视图名称。视图名称则基于视图名称约定派生,类似于返回 void 的方法 - 参见 22.13.3 节,"默认视图名称"。
Using @ModelAttribute on a method argument
在方法参数上使用 @ModelAttribute
如前一节所述,@ModelAttribute 可以用于方法或方法参数。本节解释其在方法参数上的使用。
方法参数上的 @ModelAttribute 表示该参数应从模型中检索。如果模型中不存在该参数,则应首先实例化该参数并将其添加到模型中。一旦存在于模型中,该参数的字段应从所有具有匹配名称的请求参数中填充。这在 Spring MVC 中称为数据绑定,是一种非常有用的机制,使您不必逐个解析每个表单字段。
java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { }
在上述示例中,Pet 实例可能来自哪里?有几种选择:
- 它可能已经存在于模型中,因为使用了 @SessionAttributes ------ 参见 "Using @SessionAttributes to store model attributes in the HTTP session between requests" 部分。
- 它可能已经存在于模型中,因为在同一个控制器中有一个 @ModelAttribute 方法 ------ 如前一节所述。
- 它可能基于 URI 模板变量和类型转换器进行检索(详细说明见下文)。
- 它可能使用其默认构造函数进行实例化。
@ModelAttribute 方法是一种从数据库中检索属性的常见方式,这些属性可以通过 @SessionAttributes 在请求之间存储。在某些情况下,使用 URI 模板变量和类型转换器来检索属性可能很方便。以下是一个示例:
java
@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) {
// ...
}
在此示例中,模型属性的名称(即 "account")与 URI 模板变量的名称匹配。如果您注册了一个可以将字符串 account 值转换为 Account 实例的 Converter<String, Account>,那么上述示例将无需 @ModelAttribute 方法即可工作。
下一步是数据绑定。WebDataBinder 类通过名称匹配请求参数名称 ------ 包括查询字符串参数和表单字段 ------ 与模型属性字段。匹配字段在必要时经过类型转换(从字符串到目标字段类型)后被填充。数据绑定和验证在第 9 章 "验证、数据绑定和类型转换" 中讨论。控制器级别自定义数据绑定过程在 "Customizing WebDataBinder initialization" 部分讨论。
由于数据绑定,可能会有错误,例如缺少必填字段或类型转换错误。要检查此类错误,请在 @ModelAttribute 参数之后立即添加一个 BindingResult 参数:
java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {
if (result.hasErrors()) {
return "petForm";
}
// ...
}
使用 BindingResult 可以检查是否发现错误,如果有错误,通常会重新呈现相同的表单,并使用 Spring 的 表单标签显示错误。
请注意,在某些情况下,可能需要访问模型中的属性而无需数据绑定。对于这种情况,可以将 Model 注入控制器或使用注解中的 binding 标志:
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 AccountUpdateForm form, BindingResult result,
@ModelAttribute(binding=false) Account account) {
// ...
}
除了数据绑定之外,还可以使用自定义验证器调用验证,传递与记录数据绑定错误的 BindingResult 相同的对象。这允许在一个地方累积数据绑定和验证错误,然后将其反馈给用户:
java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {
new PetValidator().validate(pet, result);
if (result.hasErrors()) {
return "petForm";
}
// ...
}
或者,可以通过添加 JSR-303 的 @Valid 注解自动调用验证:
java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) {
if (result.hasErrors()) {
return "petForm";
}
// ...
}
有关如何配置和使用验证的详细信息,请参见 9.8 节 "Spring Validation" 和第 9 章 "验证、数据绑定和类型转换"。
Using @SessionAttributes to store model attributes in the HTTP session between requests
使用 @SessionAttributes 在请求之间将模型属性存储在 HTTP 会话中
在类型级别上,@SessionAttributes 注解声明了特定处理程序使用的会话属性。通常,这会列出模型属性的名称或模型属性的类型,这些属性应透明地存储在会话或某些会话存储中,作为后续请求之间的表单支持 bean。
以下代码片段展示了此注解的用法,指定了模型属性名称:
java
@Controller
@RequestMapping("/editPet.do")
@SessionAttributes("pet")
public class EditPetForm {
// ...
}
在这个示例中,@SessionAttributes("pet") 表示在请求之间需要将名为 "pet" 的模型属性存储在会话中。这通常用于在多个请求之间保持表单数据,从而支持多步表单处理或其他需要在请求之间保持状态的情况。
通过 @SessionAttributes 注解,Spring MVC 会在处理完请求后自动将指定的模型属性存储到 HTTP 会话中,并在后续请求中自动从会话中恢复这些属性。
Using @SessionAttribute to access pre-existing global session attributes
使用 @SessionAttribute 访问预先存在的全局会话属性
如果需要访问由全局管理的预先存在的会话属性(即由控制器之外的组件管理,如过滤器),可以在方法参数上使用 @SessionAttribute 注解:
java
@RequestMapping("/")
public String handle(@SessionAttribute User user) {
// ...
}
在这个例子中,@SessionAttribute User user
表示从会话中获取名为 user
的属性。如果这个属性存在,它会被注入到 handle
方法中。如果属性不存在,Spring 会抛出异常。
对于需要添加或删除会话属性的用例,考虑将 org.springframework.web.context.request.WebRequest
或 javax.servlet.http.HttpSession
注入到控制器方法中,这样可以更灵活地管理会话属性。
对于作为控制器工作流一部分的临时模型属性存储,建议使用 @SessionAttributes
注解,如前面提到的"使用 @SessionAttributes 将模型属性存储在 HTTP 会话中"的部分。
Using @RequestAttribute to access request attributes
使用 @RequestAttribute 访问请求属性
类似于 @SessionAttribute,@RequestAttribute 注解可以用于访问由过滤器或拦截器创建的预先存在的请求属性:
java
@RequestMapping("/")
public String handle(@RequestAttribute Client client) {
// ...
}
在这个例子中,@RequestAttribute Client client
表示从请求中获取名为 client
的属性。如果这个属性存在,它会被注入到 handle
方法中。如果属性不存在,Spring 会抛出异常。
Working with "application/x-www-form-urlencoded" data
处理 "application/x-www-form-urlencoded" 数据
之前的部分介绍了如何使用 @ModelAttribute 支持来自浏览器客户端的表单提交请求。对于来自非浏览器客户端的请求,也推荐使用相同的注解。然而,当涉及到处理 HTTP PUT 请求时,有一个显著的区别。浏览器可以通过 HTTP GET 或 HTTP POST 提交表单数据,而非浏览器客户端也可以通过 HTTP PUT 提交表单。这就带来了一个挑战,因为 Servlet 规范要求 ServletRequest.getParameter*() 方法仅支持对 HTTP POST 请求的表单字段访问,而不支持 HTTP PUT 请求。
为了支持 HTTP PUT 和 PATCH 请求,spring-web 模块提供了一个名为 HttpPutFormContentFilter 的过滤器,可以在 web.xml 中进行配置:
xml
<filter>
<filter-name>httpPutFormFilter</filter-name>
<filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>httpPutFormFilter</filter-name>
<servlet-name>dispatcherServlet</servlet-name>
</filter-mapping>
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
上述过滤器会拦截内容类型为 application/x-www-form-urlencoded
的 HTTP PUT 和 PATCH 请求,从请求的主体中读取表单数据,并包装 ServletRequest,以便通过 ServletRequest.getParameter*() 方法访问表单数据。
[注意]
由于 HttpPutFormContentFilter 会消耗请求的主体,因此不应为依赖其他转换器处理 application/x-www-form-urlencoded
的 PUT 或 PATCH URL 配置此过滤器。这包括 @RequestBody MultiValueMap<String, String> 和 HttpEntity<MultiValueMap<String, String>>。
Mapping cookie values with the @CookieValue annotation
@CookieValue 注解允许将方法参数绑定到 HTTP Cookie 的值。
假设接收到的 HTTP 请求中包含以下 Cookie:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
下面的代码示例演示了如何获取 JSESSIONID Cookie 的值:
java
@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@CookieValue("JSESSIONID") String cookie) {
//...
}
如果目标方法参数类型不是 String,将自动应用类型转换。有关更多信息,请参见"方法参数和类型转换"部分。
此注解适用于 Servlet 和 Portlet 环境中的注解处理方法。
Mapping request header attributes with the @RequestHeader annotation
@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-Encoding 和 Keep-Alive 请求头的值:
java
@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@RequestHeader("Accept-Encoding") String encoding,
@RequestHeader("Keep-Alive") long keepAlive) {
//...
}
如果方法参数类型不是 String,将自动应用类型转换。有关更多信息,请参见"方法参数和类型转换"部分。
当 @RequestHeader 注解用于 Map<String, String>、MultiValueMap<String, String> 或 HttpHeaders 参数时,该 Map 将被填充所有的头部值。
[提示]
内置支持将逗号分隔的字符串转换为字符串数组/集合或其他类型,这些类型被类型转换系统所知。例如,带有 @RequestHeader("Accept") 注解的方法参数可以是 String 类型,也可以是 String[] 或 List 类型。
此注解适用于 Servlet 和 Portlet 环境中的注解处理方法。
Method Parameters And Type Conversion
从请求中提取的基于字符串的值,包括请求参数、路径变量、请求头和 cookie 值,可能需要转换为方法参数或字段的目标类型(例如,将请求参数绑定到 @ModelAttribute 参数中的字段)。如果目标类型不是 String,Spring 会自动转换为适当的类型。所有简单类型,如 int、long、Date 等,均受到支持。
您可以通过以下两种方式进一步自定义转换过程:
-
WebDataBinder:通过自定义 WebDataBinder 初始化来定制数据绑定和类型转换过程。有关更多信息,请参见"自定义 WebDataBinder 初始化"部分。
-
FormattingConversionService:通过注册格式化程序来实现类型转换。有关更多信息,请参见第 9.6 节,"Spring 字段格式化"。
这样,Spring MVC 可以灵活地将请求数据转换为合适的 Java 对象,从而简化了数据处理和绑定的工作。
Customizing WebDataBinder initialization
要通过 Spring 的 WebDataBinder 使用 PropertyEditors 自定义请求参数绑定,可以使用以下方法之一:
-
在控制器中使用 @InitBinder 注解的方法:在控制器中定义一个带有 @InitBinder 注解的方法,以注册自定义的 PropertyEditor 或 Formatter。这将允许您自定义如何将请求参数绑定到方法参数或模型属性中。
java@Controller public class MyController { @InitBinder public void initBinder(WebDataBinder binder) { binder.registerCustomEditor(MyType.class, new MyTypeEditor()); } @RequestMapping("/example") public String handleRequest(@RequestParam MyType myParam) { // ... } }
-
在 @ControllerAdvice 类中使用 @InitBinder 注解的方法:如果您希望在多个控制器之间共享自定义的绑定逻辑,可以在 @ControllerAdvice 注解的类中定义 @InitBinder 方法。这种方式提供了跨控制器的统一绑定策略。
java@ControllerAdvice public class GlobalBindingAdvice { @InitBinder public void initBinder(WebDataBinder binder) { binder.registerCustomEditor(MyType.class, new MyTypeEditor()); } }
-
提供自定义的 WebBindingInitializer:您可以实现 WebBindingInitializer 接口并在 Spring 配置中注册自定义的 WebBindingInitializer。这种方式适用于全局范围内的绑定初始化,而不仅仅是在特定的控制器或 @ControllerAdvice 中。
javapublic class MyWebBindingInitializer implements WebBindingInitializer { @Override public void initBinder(WebDataBinder binder, WebRequest request) { binder.registerCustomEditor(MyType.class, new MyTypeEditor()); } }
然后,在 Spring 配置中注册这个初始化器:
xml<bean class="org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver"/> <bean id="webBindingInitializer" class="com.example.MyWebBindingInitializer"/>
有关更多详细信息,请参见"使用 @ControllerAdvice 和 @RestControllerAdvice 对控制器进行建议"部分。
Customizing data binding with @InitBinder
@InitBinder
注解用于自定义数据绑定,允许在控制器中配置如何将请求参数绑定到方法参数上。这个注解标记的方法用于初始化 WebDataBinder
,以便对表单对象进行自定义绑定处理。
下面是对两种常见自定义绑定方法的总结:
使用 PropertyEditor
如果需要注册自定义的 PropertyEditor
来处理特定类型的转换,可以使用 registerCustomEditor
方法。例如,下面的代码演示了如何为 Date
类型注册一个自定义的日期编辑器:
java
@Controller
public class MyFormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
使用 Formatter
从 Spring 4.2 开始,可以使用 Formatter
来处理格式化需求,替代 PropertyEditor
。这种方法在需要格式化服务时特别有用,例如:
java
@Controller
public class MyFormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
// ...
}
这两种方法都可以用来定制数据绑定的方式,以适应不同的数据格式和需求。如果有其他问题或需要进一步的解释,请随时问我!
Configuring a custom WebBindingInitializer
配置自定义 WebBindingInitializer
要外部化数据绑定初始化,可以提供自定义的 WebBindingInitializer
实现,并通过为 AnnotationMethodHandlerAdapter
提供自定义 bean 配置来启用它,从而覆盖默认配置。
以下示例来自 PetClinic 应用程序,演示了如何使用自定义的 WebBindingInitializer
实现 org.springframework.samples.petclinic.web.ClinicBindingInitializer
,该实现配置了 PetClinic 控制器所需的 PropertyEditors
。
xml
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="cacheSeconds" value="0"/>
<property name="webBindingInitializer">
<bean class="org.springframework.samples.petclinic.web.ClinicBindingInitializer"/>
</property>
</bean>
使用 @ControllerAdvice
另外,你也可以在标注了 @ControllerAdvice
的类中定义 @InitBinder
方法,这样它们将应用于匹配的控制器。这为配置数据绑定提供了另一种选择。有关更多详细信息,请参见"使用 @ControllerAdvice 和 @RestControllerAdvice 为控制器提供建议"一节。
Advising controllers with @ControllerAdvice and @RestControllerAdvice
使用 @ControllerAdvice 和 @RestControllerAdvice 为控制器提供建议
@ControllerAdvice
注解是一个组件注解,它允许通过类路径扫描自动检测实现类。使用 MVC 命名空间或 MVC Java 配置时,该注解会自动启用。
被标注为 @ControllerAdvice
的类可以包含 @ExceptionHandler
、@InitBinder
和 @ModelAttribute
注解的方法,这些方法将应用于所有控制器层次结构中的 @RequestMapping
方法,而不仅仅是声明它们的控制器层次结构。
@RestControllerAdvice
是一个替代选项,其中 @ExceptionHandler
方法默认假定具有 @ResponseBody
语义,即返回值会直接写入 HTTP 响应体中。
@ControllerAdvice
和 @RestControllerAdvice
都可以针对特定的控制器子集:
java
// 目标所有标注了 @RestController 的控制器
@ControllerAdvice(annotations = RestController.class)
public class AnnotationAdvice {}
// 目标所有位于特定包中的控制器
@ControllerAdvice("org.example.controllers")
public class BasePackageAdvice {}
// 目标所有可以赋值给特定类的控制器
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class AssignableTypesAdvice {}
有关 @ControllerAdvice
的更多详细信息,请查阅其官方文档。
Jackson Serialization View Support
Jackson 序列化视图支持
使用 Jackson 的 Serialization Views 进行条件序列化
在 Spring MVC 中,可能有时需要根据上下文条件筛选将被序列化到 HTTP 响应体中的对象。为了实现这一功能,Spring MVC 内置了对 Jackson 序列化视图(Serialization Views)的支持。
在 @ResponseBody 方法中使用 @JsonView
要在 @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");
}
}
示例类
java
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
注解用于指定在响应体中要序列化的视图。方法 getUser()
将只返回 username
字段,因为 WithoutPasswordView
视图只包含 username
字段。
注意事项
- 尽管
@JsonView
支持指定多个类,但在控制器方法上仅支持指定一个视图类。如果需要启用多个视图,可以考虑使用复合接口(composite interface)。
在视图解析器中使用序列化视图
对于依赖视图解析的控制器,只需将序列化视图类添加到模型中:
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";
}
}
在上面的示例中,将 User.WithoutPasswordView.class
作为模型属性添加,并在视图解析中使用该视图。
通过以上方法,可以根据不同的视图需求动态控制序列化的字段,实现灵活的数据展示和安全控制。
Jackson JSONP Support
Jackson JSONP 支持
为了为 @ResponseBody
和 ResponseEntity
方法启用 JSONP 支持,可以声明一个扩展 AbstractJsonpResponseBodyAdvice
的 @ControllerAdvice
Bean,如下所示,其中构造函数的参数指定 JSONP 查询参数的名称:
java
@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
public JsonpAdvice() {
super("callback");
}
}
对于依赖视图解析的控制器,当请求包含名为 jsonp
或 callback
的查询参数时,JSONP 将自动启用。这些名称可以通过 jsonpParameterNames
属性进行自定义。
注意
自 Spring Framework 4.3.18 起,JSONP 支持已被弃用,并将在 Spring Framework 5.1 中移除。建议改用 CORS (跨源资源共享) 代替 JSONP。
Asynchronous Request Processing
Spring MVC 异步请求处理
Spring MVC 3.2 引入了基于 Servlet 3 的异步请求处理。控制器方法不再像平常那样返回一个值,而是可以返回一个 java.util.concurrent.Callable
对象,由 Spring MVC 管理的线程来生成返回值。与此同时,主 Servlet 容器线程会退出并释放,以处理其他请求。Spring MVC 使用 TaskExecutor
在一个单独的线程中调用 Callable
,当 Callable
返回时,请求会被分派回 Servlet 容器,以使用 Callable
返回的值继续处理。以下是一个这样的控制器方法示例:
java
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public String call() throws Exception {
// ...
return "someView";
}
};
}
另一个选项是控制器方法返回一个 DeferredResult
实例。在这种情况下,返回值也将由任何线程生成,即不是由 Spring MVC 管理的线程。例如,结果可能是响应某个外部事件,如 JMS 消息、定时任务等。以下是一个这样的控制器方法示例:
java
@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<String>();
// 将 deferredResult 保存到某个地方...
return deferredResult;
}
// 在其他线程中...
deferredResult.setResult(data);
如果没有 Servlet 3.0 异步请求处理的知识,这可能会很难理解。以下是一些关于底层机制的基本事实:
-
将 ServletRequest 置于异步模式 :通过调用
request.startAsync()
来实现。这样做的主要效果是 Servlet 和任何 Filter 可以退出,但响应将保持打开状态,以便稍后完成处理。 -
AsyncContext :调用
request.startAsync()
返回AsyncContext
,可以用来进一步控制异步处理。例如,它提供了dispatch
方法,类似于 Servlet API 中的转发,但允许应用程序在 Servlet 容器线程上恢复请求处理。 -
DispatcherType:ServletRequest 提供对当前 DispatcherType 的访问,可用于区分处理初始请求、异步分派、转发和其他调度类型。
基于上述内容,使用 Callable
进行异步请求处理的事件序列如下:
- 控制器返回一个
Callable
。 - Spring MVC 开始异步处理,并将
Callable
提交给TaskExecutor
以在单独的线程中处理。 DispatcherServlet
和所有 Filter 退出 Servlet 容器线程,但响应保持打开状态。Callable
生成一个结果,Spring MVC 将请求分派回 Servlet 容器以继续处理。DispatcherServlet
再次被调用,处理使用Callable
异步生成的结果继续进行。
使用 DeferredResult
的事件序列非常类似,只不过由应用程序从任何线程生成异步结果:
- 控制器返回一个
DeferredResult
并将其保存在某个内存队列或列表中,以便可以访问。 - Spring MVC 开始异步处理。
DispatcherServlet
和所有配置的 Filter 退出请求处理线程,但响应保持打开状态。- 应用程序从某个线程设置
DeferredResult
,Spring MVC 将请求分派回 Servlet 容器。 DispatcherServlet
再次被调用,处理使用异步生成的结果继续进行。
有关异步请求处理的动机以及何时使用它,请阅读这系列博客文章。
Exception Handling for Async Requests
异步请求处理中的异常
如果从控制器方法返回的 Callable
在执行时抛出异常,会发生什么?简短的回答是:处理方式与控制器方法直接抛出异常时相同。详细解释是,当 Callable
抛出异常时,Spring MVC 会将异常作为结果分派到 Servlet 容器,这会导致请求处理恢复时使用异常,而不是控制器方法的返回值。
异常处理机制:
-
使用
Callable
时 :当
Callable
执行期间抛出异常,Spring MVC 会将异常传递回 Servlet 容器,处理流程将恢复时使用这个异常,而不是Callable
的返回值。异常会被传递到常规异常处理机制中。 -
使用
DeferredResult
时 :在使用
DeferredResult
时,你可以选择调用setResult
或setErrorResult
方法:setResult
方法用于设置正常的结果。setErrorResult
方法用于设置异常结果(异常实例)。
例子:
如果 Callable
抛出一个异常,Spring MVC 会处理这个异常,像处理控制器方法直接抛出的异常一样。如果你使用 DeferredResult
,你可以决定是设置正常结果还是错误结果,以便在异步处理时更好地控制异常处理。
Intercepting Async Requests
拦截异步请求
使用 AsyncHandlerInterceptor
HandlerInterceptor
可以实现 AsyncHandlerInterceptor
接口,以便在异步处理开始时调用 afterConcurrentHandlingStarted
回调方法。这将代替 postHandle
和 afterCompletion
方法在异步处理开始时被调用。
注册拦截器
CallableProcessingInterceptor
和DeferredResultProcessingInterceptor
:
这些拦截器允许在异步请求的生命周期中更深层次地集成,例如处理超时事件。可以通过注册这些拦截器来实现更复杂的异步处理逻辑。
DeferredResult
方法
DeferredResult
类型提供了以下方法,用于处理超时和完成事件:
onTimeout(Runnable)
:当异步处理超时时调用的回调。onCompletion(Runnable)
:当异步处理完成时调用的回调。
使用 WebAsyncTask
当使用 Callable
时,可以将其包装在 WebAsyncTask
实例中,这也提供了超时和完成的注册方法。
例子
java
public class MyAsyncInterceptor implements AsyncHandlerInterceptor {
@Override
public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 异步处理开始时调用
}
// 其他方法...
}
@Controller
public class MyController {
@GetMapping("/async")
public WebAsyncTask<String> asyncMethod() {
return new WebAsyncTask<>(5000, new Callable<String>() {
@Override
public String call() throws Exception {
// 异步处理逻辑
return "result";
}
});
}
}
在上面的例子中,MyAsyncInterceptor
实现了 AsyncHandlerInterceptor
接口的 afterConcurrentHandlingStarted
方法,用于处理异步处理开始的事件。WebAsyncTask
示例展示了如何包装一个 Callable
以处理超时和完成事件。
HTTP Streaming
HTTP 流式传输
控制器方法可以使用 DeferredResult
和 Callable
异步生成返回值,这可以用于实现例如长轮询(long polling)的技术,其中服务器可以尽快向客户端推送事件。
如果你想在单个 HTTP 响应上推送多个事件怎么办?这种技术与长轮询相关,称为"HTTP 流式传输"(HTTP Streaming)。Spring MVC 通过 ResponseBodyEmitter
返回值类型使其成为可能。ResponseBodyEmitter
可以用来发送多个对象,而不是像 @ResponseBody
那样发送一个对象,每个发送的对象都会通过 HttpMessageConverter
写入响应中。
下面是一个示例:
java
@RequestMapping("/events")
public ResponseBodyEmitter handle() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
// 保存 emitter 到某个地方..
return emitter;
}
// 在其他线程中
emitter.send("Hello once");
// 稍后再发送
emitter.send("Hello again");
// 最后完成
emitter.complete();
请注意,ResponseBodyEmitter
也可以作为 ResponseEntity
的主体,以便自定义响应的状态和头信息。
HTTP Streaming With Server-Sent Events
使用服务器发送事件(Server-Sent Events)的 HTTP 流式传输
SseEmitter
是 ResponseBodyEmitter
的一个子类,它提供了对服务器发送事件(Server-Sent Events,简称 SSE)的支持。SSE 是一种"HTTP 流式传输"技术的变种,不同之处在于从服务器推送的事件按照 W3C 服务器发送事件规范进行格式化。
SSE 可以用于它的预期目的,即从服务器向客户端推送事件。在 Spring MVC 中,这很容易实现,只需返回一个 SseEmitter
类型的值即可。
然而,请注意 Internet Explorer 不支持 SSE。对于更高级的 Web 应用程序消息传递场景,例如在线游戏、协作、金融应用等,最好考虑使用 Spring 的 WebSocket 支持。WebSocket 包括 SockJS 风格的 WebSocket 模拟,能回退到支持范围非常广泛的浏览器(包括 Internet Explorer),并且在一个更加以消息为中心的架构中,通过发布-订阅模型与客户端交互的更高层次的消息模式。有关详细背景信息,请参见以下博客文章。
HTTP Streaming Directly To The OutputStream
直接将 HTTP 流式传输写入 OutputStream
ResponseBodyEmitter
允许通过 HttpMessageConverter
向响应写入对象,从而发送事件。这可能是最常见的情况,例如在写入 JSON 数据时。然而,有时绕过消息转换,直接写入响应的 OutputStream
更为有用,例如在文件下载的情况下。这可以通过 StreamingResponseBody
返回值类型来实现。
以下是一个示例:
java
@RequestMapping("/download")
public StreamingResponseBody handle() {
return new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
// write...
}
};
}
请注意,StreamingResponseBody
也可以用作 ResponseEntity
的主体,以便自定义响应的状态和头部。
Configuring Asynchronous Request Processing
Servlet Container Configuration
为应用程序配置 web.xml 确保更新到版本 3.0
xml
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
...
</web-app>
异步支持必须通过 web.xml 中的 <async-supported>true</async-supported>
子元素在 DispatcherServlet
上启用。此外,任何参与异步请求处理的过滤器必须配置为支持 ASYNC 分派类型。为 Spring 框架提供的所有过滤器启用 ASYNC 分派类型应该是安全的,因为它们通常扩展了 OncePerRequestFilter
,并且具有检查过滤器是否需要参与异步分派的运行时检查。
以下是一些示例 web.xml 配置:
xml
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<filter>
<filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
<filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>
</web-app>
如果使用 Servlet 3,通过例如 WebApplicationInitializer
的基于 Java 的配置,您也需要设置 "asyncSupported" 标志以及 ASYNC 分派类型,就像在 web.xml 中一样。为简化所有这些配置,考虑扩展 AbstractDispatcherServletInitializer
或更好的 AbstractAnnotationConfigDispatcherServletInitializer
,它们自动设置这些选项并使注册 Filter
实例变得非常容易。
Spring MVC Configuration
MVC Java 配置和 MVC 命名空间提供了配置异步请求处理的选项。WebMvcConfigurer
有 configureAsyncSupport
方法,而 <mvc:annotation-driven>
有一个 <async-support>
子元素。
这些选项允许您配置用于异步请求的默认超时值,如果未设置,该值取决于底层的 Servlet 容器(例如,在 Tomcat 上为 10 秒)。您还可以配置一个 AsyncTaskExecutor
来执行从控制器方法返回的 Callable
实例。强烈建议配置此属性,因为默认情况下 Spring MVC 使用 SimpleAsyncTaskExecutor
。MVC Java 配置和 MVC 命名空间还允许您注册 CallableProcessingInterceptor
和 DeferredResultProcessingInterceptor
实例。
如果需要为特定的 DeferredResult
覆盖默认超时值,可以使用适当的类构造函数。同样,对于 Callable
,您可以将其包装在 WebAsyncTask
中,并使用适当的类构造函数来自定义超时值。WebAsyncTask
的类构造函数还允许提供一个 AsyncTaskExecutor
。
Testing Controllers
spring-test
模块提供了对测试注解控制器的一流支持。详见第 15.6 节,"Spring MVC 测试框架"。