Bootstrap

Spring MVC学习(5)—基于注解的Controller控制器的配置全解【一万字】

基于最新Spring 5.x,详细介绍了Spring MVC的基于注解的Controller控制器的配置全解,包括基于注解的Controller控制器的声明、Request Mapping请求映射的规则、基于@RequestMapping的处理器方法的参数、返回值的配置等

此前我们介绍了Spring MVC的核心组件和请求执行流程,现在我们来学习Spring MVC的基于注解的Controller控制器的配置全解,包括基于注解的Controller控制器的声明、Request Mapping请求映射的规则、基于@RequestMapping的处理器方法的参数、返回值的配置等

Spring MVC 提供了基于注解的编程模型,@Controller和@RestController组件内部可以使用注解来表示请求映射、请求输入、异常处理等功能。注解提供了方法级别的Controller实现,从而不必扩展基类或实现特定的接口,减少了代码量、优化了项目结构!

一个常见的基于注解定义的控制器案例如下:

@Controller
public class HelloController {

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

该方法接受Model参数并以String的形式返回逻辑视图名称,但是还存在许多其他可配置的选项,下面我们会一一介绍!

Spring MVC学习 系列文章

Spring MVC学习(1)—MVC的介绍以及Spring MVC的入门案例

Spring MVC学习(2)—Spring MVC中容器的层次结构以及父子容器的概念

Spring MVC学习(3)—Spring MVC中的核心组件以及请求的执行流程

Spring MVC学习(4)—ViewSolvsolver视图解析器的详细介绍与使用案例

Spring MVC学习(5)—基于注解的Controller控制器的配置全解【一万字】

Spring MVC学习(6)—Spring数据类型转换机制全解【一万字】

Spring MVC学习(7)—Validation基于注解的声明式数据校验机制全解【一万字】

Spring MVC学习(8)—HandlerInterceptor处理器拦截器机制全解

Spring MVC学习(9)—项目统一异常处理机制详解与使用案例

Spring MVC学习(10)—文件上传配置、DispatcherServlet的路径配置、请求和响应内容编码

Spring MVC学习(11)—跨域的介绍以及使用CORS解决跨域问题

1 控制器的声明

我们可以通过在 Servlet 的WebApplicationContext中使用标准Spring bean 的定义方式来定义Controller控制器bean。

@Controller注解采用@Component作为元注解,支持@ComponentScan和<context:component-scan/>的组件扫描,同时@Controller注解还会指示标注的类被作为 Web 组件的角色,如果采用@Service、@Component等其他组件扫描注解,那么内部的方法级别的控制器不会生效!

@RestController 是一个组合注解,它组合使用了 @Controller 和 @ResponseBody元注解,指示其类中的每种方法都继承了类级别的@ResponseBody注解,因此返回的数据将直接写入response响应正文中,而不会使用ViewSolvsolver和View进行视图模版解析和渲染。

注意,如果需要写入response响应正文中是JSON数据并且能自动将返回的对象格式化为JSON,那么还需要JSON转换的依赖:

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson
-databind -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.9.8</version>
</dependency>

1.1 AOP 代理

Controller控制器中支持AOP 代理,例如在类上添加@Transactional进行事务控制,但这需要Spring MVC的配置文件定义了事务的支持!

由于Controller控制器中的方法一般都不是从接口中继承的,因此建议使用基于类的动态代理,但是如果Controller实现了除了容器回调接口(例如InitializingBean、DisposableBean、Closeable、AutoCloseable接口以及Aware接口包括其子接口)、标志性接口、内部接口的的其他合理接口,那么为了让控制机方法的AOP配置生效,可能需要显式配置基于类的代理,通常是配置<tx:annotation-driven proxy-target-class="true"/>或者@EnableTransactionManagement(proxyTargetClass = true)

2 Request Mapping请求映射配置

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

更常见的还有用于筛选特定HTTP方法的@RequestMapping变体:@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping。

上面的这些基于特定HTTP方法的注解是一种组合注解,在RESTFUL风格的项目中,大多数的控制器方法都应该映射到特定的HTTP请求方法,因此更多的使用这些特性化的注解,而不是使用@RequestMapping,后者默认情况下与所有类型的HTTP方法匹配。但是,在类级别仍可以使用@RequestMapping来表示共享映射。

下面示例要求具体HTTP请求方法的方法级映射:

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

    /**
     * 查询的方法,采用get请求
     */
    @GetMapping("/{id}")
    public Person get(@PathVariable Long id) {
        // ...
    }

    /**
     * 插入的方法,采用post请求
     */
    @PostMapping
    public void add(@RequestBody Person person) {
        // ...
    }

    /**
     * 更新的方法,采用put请求
     */
    @PutMapping
    public void update(@RequestBody Person person) {
        // ...
    }

    /**
     * 删除的方法,采用delete请求
     */
    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        // ...
    }
}

2.1 URI 模式

我们可以使用 global模式和通配符来映射请求,简单的说,请求的URL路径支持模式匹配!Spring MVC 使用 PathMatcher 协定和来自spring-core的 AntPathMatcher 实现进行 URI 路径匹配。

下面是Pattern以及对应的匹配规则:

Pattern Description Example
? 匹配一个字符"/pages/t?st.html" 匹配 "/pages/test.html" 和"/pages/t3st.html"
* 匹配路径段中的零个或多个字符"/resources/.png" 匹配 "/resources/file.png"。"/projects/*/versions" 匹配 "/projects/spring/versions" 不匹配 "/projects/spring/boot/versions"
** 匹配零个或多个路径段,直到路径结束"/resources/**" 匹配 "/resources/file.png" 和 "/resources/images/file.png"
{name} 匹配路径段,并捕获它作为名为"name"的控制器方法的参数变量的值 "/projects/{project}/versions" 匹配" /projects/spring/versions" 和 captures project=spring
{name:[a-z]+} 匹配满足"[a-z]+"正则表达式的路径段,并捕获它作为名为"name"的控制器方法的参数变量的值"/projects/{project:[a-z]+}/versions" 匹配"/projects/spring/versions" 不匹配 "/projects/spring1/versions"

2.1.1 @PathVariable

捕获的 URI 中的变量可以通过@PathVariable注解注入到方法参数中进行访问,默认情况下参数名和URI中的变量名一致,如下例所示,

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

实际上参数名和URI中的变量名不一致也可以,此时需要在@PathVariable注解中指明使用的变量:

@GetMapping("/owners/{ownerId}")
public void findPet(@PathVariable("ownerId") Long id) {
    // ...
}

我们可以同时在类和方法级别中声明 URI 变量并使用,如下例所示:

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

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

URI变量将自动转换为参数相应的类型,如果不能转换将引发TypeMismatchException异常。默认情况下支持简单类型(int、long、Date 等),您可以注册对任何其他数据类型的支持,可以通过 WebDataBinder(DataBinder)或通过FormattingConversionService注册格式器Formatters来自定义类型转换规则。

2.1.2 正则表达式

语法[varName:regex]声明带有正则表达式的URI变量,在一个路径段中,可以多次使用该语法。例如,以下方法可以提取URI路径段中的名称、版本和文件扩展名:

@RestController
public class RegexController {

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

访问/spring-web-3.0.5.jar,将会得到:
在这里插入图片描述

2.1.3 占位符

URI路径模式还可以支持嵌入${…}占位符,这些占位符在启动时通过PropertyPlaceHolderConfigurer针对本地、系统system、环境environment和其他属性源中的属性来解析。因此我们可以使用它根据某些外部配置对基本 URL 进行参数化。

URI配置文件UriPlaceholder.properties(需要加载到容器中)如下:

uri1=tx
uri2=xt
uri3=xxx

Controller如下:

@RestController
@RequestMapping("/${uri1}")
class PlaceholderController {

    @GetMapping("/${uri2}/{${uri3}}/{id}")
    public void handle(@PathVariable Long id, @PathVariable String xxx) {
        System.out.println(id);
        System.out.println(xxx);
    }
}

访问/tx/xt/aaa/111,结果如下:

在这里插入图片描述

2.1.4 后缀匹配

默认情况下,Spring MVC支持“.*”的后缀匹配,比如映射到 /person 的控制器也隐式映射到/person.*。这样的好处是,可以通过文件扩展名用于来表示请求的内容类型(content type)以用于响应(而不是使用Accept请求头) - 例如/person.pdf,/person.xml等。

但是后来文件扩展名的使用已经以各种方式证明是有问题的。使用 URI 变量、路径参数和 URI 编码时,可能会导致歧义,还可能引起RFD攻击,目前AcceptHeader是首选的内容协商判定依据。

如果要完全禁用文件扩展名匹配,必须设置以下两项:

  1. PathMatchConfigurer.setUseSuffixPatternMatch(false)。
  2. ContentNegotiationConfigurer.preferredPathExtension(false)。

2.2 匹配Content-Type

我们可以根据请求的Content-Type(客户端发送的实体数据类型)缩小请求映射范围,如下案例:

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

consumes属性是一个String数组,可以传递一个或多个media type媒体类型字符串,其中至少一个与请求的Content-Type匹配!

此外,consumes支持否定表达式,例如!text/plain表示除text/plain之外的任何Content-Type。

同理,我们可以在类级别声明共享的consumes属性。但是,与大多数其他请求映射属性不同,在类级别使用时,方法级使用该同名属性将会重写,而不是扩展类级声明,简单的说就是如果方法上也有consumes属性,那么就不会采用、合并类级别上的consumes属性。

在设置consumes的媒体类型值的时候,可以使用MediaType类,该类为常用媒体类型(如APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE)提供了可使用的字符串常量。

2.3 匹配Accept

我们可以根据请求的Accept(客户端希望接受的数据类型)缩小请求映射范围,如下案例:

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

produces属性是一个String数组,可以传递一个或多个media type媒体类型字符串,其中至少一个与请求的Accept匹配!

此外,produces支持否定表达式,例如!text/plain表示除text/plain之外的任何Accept。
同理,我们可以在类级别声明共享的produces属性。但是,与大多数其他请求映射属性不同,在类级别使用时,方法级使用该同名属性将会重写,而不是扩展类级声明,简单的说就是如果方法上也有produces属性,那么就不会采用、合并类级别上的produces属性。

在设置produces的媒体类型值的时候,可以使用MediaType类,该类为常用媒体类型(如APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE)提供了可使用的字符串常量。

2.4 匹配参数

我们可以根据请求参数条件缩小请求映射范围。可以测试存在请求参数(myParam)、缺少请求参数(!myParam)或特定值(myParam=myValue)是否存在。

下面的示例演示如何测试特定参数值:

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

上面的案例中,除了URL路径必须匹配之外,还必须存在myParam参数并且值等于myValue,请求才会映射到该控制器方法!

2.5 匹配请求头

我们可以根据请求头缩小请求映射范围。可以测试存在请求头(myHeader)、缺少请求头(! myHeader)或特定值(myHeader=myValue)是否存在。

下面的示例演示如何测试特定请求头值:

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

上面的案例中,除了URL路径必须匹配之外,还必须存在myHeader请求头并且值等于myValue,请求才会映射到该控制器方法!

由于Content-Type和Accept都是请求头参数,因此它们也可以使用headers属性匹配,但是最好还是使用consumes和produces属性。

2.6 HTTP HEAD、OPTIONS方法

@GetMapping和@RequestMapping(method=RequestMethod.GET)透明的支持HTTP HEAD 方法进行请求映射,控制器方法不需要更改。response中的Content-Length头将会设置为将要写入的字节数而无需实际写入响应。也就是说,HTTP HEAD 请求的处理就像是 HTTP GET 一样,只是,不写入响应正文,而是计算响应字节数并设置Content-Length标头。HEAD请求一般用来测试超链接的有效性,可用性和最近修改。

对于HTTP OPTIONS请求,默认情况下将对应的控制器注解中的method属性(表示支持的HTTP 方法)的值设置为“Allow”响应头的值,其中RequestMethod.GET支持GET,HEAD,OPTIONS方法,其他的类型则支持本类型方法和OPTIONS方法,也可以通过response手动设置“Allow”头信息!

对于没有HTTP方法声明的@RequestMapping,Allow头将被设置为GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS。但是,控制器方法应始终声明支持的HTTP方法(例如通过使用HTTP方法特定的变体:@GetMapping,@PostMapping等)。

2.7 组合注解

Spring MVC支持使用组合注解进行请求映射。这些注解本身使用@RequestMapping作为元注解,并且旨在以更狭窄,更具体的用途重新声明请求映射规则!

@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@DeleteMapping 和 @PatchMapping是组合注解的实例。提供这些特性化注解的原因是,在RESTful风格的开发规范中,大多数控制器方法应该映射到特定的 HTTP 方法,以表达特定的操作,而不是使用@RequestMapping,因为默认情况下它匹配所有 HTTP 方法。

以@GetMapping为例,可以看到,其内部采用了@RequestMapping注解作为元注解,并且限制了请求方法为RequestMethod.GET,即仅支持GET,HEAD,OPTIONS请求。

在这里插入图片描述

3 Handler Methods处理器方法

基于@RequestMapping的处理器方法具有灵活的方法签名,可以从一系列受支持的控制器方法参数和返回值中选择所需要开发方式!

3.1 方法参数

方法支持JDK 8 的 java.util.Optional作为方法参数,并可以结合具有required属性的注解(例如@RequestParam、@RequestHeader ),采用Optional包装的参数等效于required=false。

下表描述了受支持的控制器方法参数和注解,任何参数都不支持Reactive相关类型。

控制器方法参数说明
WebRequest, NativeWebRequest用于对请求参数、请求(request)和会话(session)属性的通用访问,而无需直接使用 Servlet API。
javax.servlet.ServletRequest, javax.servlet.ServletResponse请求和响应,可以指定特性的类型,比如HttpServletRequest、MultipartHttpServletRequest
javax.servlet.http.HttpSessionsession,如果使用了该参数,那么永远不会为null。请注意,session访问不是线程安全的。如果允许多个请求同时访问session,请考虑将RequestMappingHandlerAdapter实例的synchronizeOnSession标志设置为 true。
javax.servlet.http.PushBuilderServlet 4.0 的PushBuilder用于实现服务器资源推送。如果客户端不支持 HTTP/2 ,则注入的 PushBuilder 实例可能为 null。
java.security.Principal当前经过身份验证的用户 – 如果已知,可能是特定的Principal实现类。
HttpMethod当前请求的 HTTP 方法。
java.util.Locale当前请求的区域设置,由可用的LocaleResolver区域解析器确定。
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 路径段中的name-value键值对。
@RequestParam用于访问 Servlet 请求参数,支持多部分文件参数类型,参数值将自动转换为声明的方法参数类型。注意,对于简单类型的参数,可以不使用@RequestParam注解!
@RequestHeader用于访问请求头中的信息。请求头的值将自动转换为声明的方法参数类型。
@CookieValue用于访问 Cookie。Cookie值将转换为声明的方法参数类型。
@RequestBody用于访问 HTTP 请求正文(请求体)。正文内容转换为声明的方法参数类型,通过使用HttpMessageConverter来实现转换,比较重要。
HttpEntity用于访问请求头和正文。正文类型使用HttpMessageConverter转换。
@RequestPart用于访问multipart/form-data多部分请求中的一个部分,使用HttpMessageConverter转换类型。对于解析同时上传多中数据的请求的解析方便
java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap用于访问、设置model参数,model中的数据将可能用于视图渲染。
RedirectAttributes指定用于重定向时的属性。一个专门用于重定向之后还能带参数跳转的的工具类。
@ModelAttribute用于访问模型中的现有属性(如果不存在,则进行实例化),并应用数据绑定和验证。
Errors, BindingResult用于访问来自命令对象(即@ ModelAttribute参数)的数据验证和数据绑定中的错误或来自@RequestBody或@RequestPart参数的验证中的错误。必须在经过验证的方法参数后声明一个Errors或BindingResult参数。
SessionStatus + class-level @SessionAttributesSessionStatus#setComplete方法可以触发通过类级注解@SessionAttributes存储的session属性的清理,但是不会清理真正session中的属性。
UriComponentsBuilder用于构建相对于当前请求的host,port,scheme,context path以及servletmapping的URL。
@SessionAttribute用于访问已经存储的session域中的会话属性。
@RequestAttribute用于访问已创建的、预先存在的request域中的属性
任何其他参数如果方法参数不与此表中任何上面的类型匹配,并且它是一种简单类型(由 BeanUtils#isSimpleProperty确定),则它是作为一个@RequestParam,并且requir=false。否则,它将作为一个@ModelAttribute。

3.2 返回值

下表描述了受支持的控制器方法返回值以及注解,所有返回值都支持Reactive类型。

控制器方法返回值说明
@ResponseBody返回值将通过 HttpMessageConverter进行转换,并写入响应。
HttpEntity, ResponseEntity返回值用于指定完整的响应(包括HTTP状态码、头部信息以及响应体),响应体将通过HttpMessageConverter实例转换并写入响应。
HttpHeaders用于返回具有相应头且无响应正文的响应。
String一个视图名称,将通过ViewResolver来解析,并与模型数据一起使用(可以与@ModelAttribute方法结合,或者通过设置Model参数来配置模型数据)。
View一个视图实例,与模型数据一起使用(可以与@ModelAttribute方法结合,或者通过设置Model参数来配置模型数据)。
java.util.Map, org.springframework.ui.Model配置要添加到模型中的数据,视图名称则通过请求RequestToViewNameTranslator隐式的确定。
@ModelAttribute配置要添加到模型中的数据,视图名称则通过请求RequestToViewNameTranslator隐式的确定。
ModelAndView要使用的视图和模型属性,以及(可选)响应状态。
void如果具有void返回类型(或null返回值)的方法也具有ServletResponse、OutputStream参数或@ResponseStatus注解,则认为该方法已经完全处理了响应。如果没有上面的条件,那么指示没有响应正文,或者选择默认的视图名!
DeferredResult用于从异步线程返回任何上述类型的返回值,用于支持异步请求。
Callable在Spring MVC 管理的线程中异步生成上述任何返回值,用于支持异步请求。
ListenableFuture, java.util.concurrent.CompletionStage, java.util.concurrent.CompletableFutureDeferredResult的便捷替代方案。
ResponseBodyEmitter, SseEmitter使用HttpMessageConverter实现异步发出对象流写入到响应中,也可以作为ResponseEntity的响应体。用于实现SSE–server send event,是一种服务端推送的技术。
StreamingResponseBody以异步方式写入响应输出流,也可以作为ResponseEntity的响应体。
Reactive 类型 – Reactor、RxJava 或其他ReactiveAdapterRegistryDeferredResult的替代方法,其中包含收集到List的多值流(例如Flux,Observable)。
任何其他返回值如果返回值与上述任何条件不匹配,并且不是简单类型(由 BeanUtils#isSimpleProperty 确定),并且返回值是字符串或 void,它们都被视为视图名称(通过RequestToViewNameTranslator进行默认视图名称选择)。简单类型的值仍无法解析。

3.3 Matrix Variables

根据URI规范RFC 3986,请求URL的路径段中支持键值对。 Spring MVC根据Tim Berners-Lee的一篇旧文章将这些键值对称为“矩阵变量(matrix variables)”,但是它们也可以被称为URI路径参数。

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

如果 URL 预期包含矩阵变量,则控制器方法的请求映射必须使用 URI 变量来屏蔽该矩阵变量内容,并确保请求可以独立于矩阵变量的顺序和存在成功匹配。

我们首先需要启用矩阵变量的使用。在JavaConfig配置中,您需要设置一个 UrlPathHelper bean,并设置removeSemicolonContent=false。在 MVC的XML 命名空间中,可以设置<mvc:annotation-driven enable-matrix-variables="true"/>

下面示例如何简单的使用矩阵变量:

@GetMapping("/matrix/{id}")
public void handle(@PathVariable String id, @MatrixVariable int q, @MatrixVariable int r) {
    System.out.println(id);
    System.out.println(q);
    System.out.println(r);
    //…………
}

访问/matrix/11;q=22;r=33,得到如下结果:

11
22
33

鉴于所有路径段可能都包含矩阵变量,并且不同的路径段中可能具有同名的矩阵变量,有时可能需要消除矩阵变量预期到底位于哪个路径变量中的歧义。

我们可以通过@MatrixVariable 的name和pathvar属性来区分,name表示矩阵变量名,pathVar表示当前矩阵变量所在的URI路径变量名!

@GetMapping("/matrix/{path1}/{path2}")
public void handle(@PathVariable int path1, @PathVariable int path2,
                    @MatrixVariable(name = "q", pathVar = "path1") int q1,
                    @MatrixVariable(name = "q", pathVar = "path2") int q2) {
    System.out.println(path1);
    System.out.println(path2);
    System.out.println(q1);
    System.out.println(q2);
    //…………
}

访问/matrix/11;q=22/33;q=44,结果如下:

11
33
22
44

矩阵变量如果不存在,默认将会抛出异常,当然可以通过required属性定义为可选变量以及通过defaultValue指定默认值,如下例所示:

@GetMapping("/matrix/req/{id}")
public void handle(@MatrixVariable(required = false, defaultValue = "110") int q) {
    System.out.println(q);
    //…………
}

访问/matrix/req/11,结果如下:

110

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

@GetMapping("/matrix/all/{all1}/{all2}")
public void handle(@MatrixVariable MultiValueMap<String, String> allmatrixVars,
                   @MatrixVariable(pathVar = "all1") MultiValueMap<String, String> all1MatrixVars,
                   @MatrixVariable(pathVar = "all2") MultiValueMap<String, String> all2MatrixVars
) {
    System.out.println(allmatrixVars);
    System.out.println(all1MatrixVars);
    System.out.println(all2MatrixVars);
    //…………
}

访问/matrix/all/11;q=22;r=33/44;q=55;s=66,结果如下:

{q=[22, 55], r=[33], s=[66]}
{q=[22], r=[33]}
{q=[55], s=[66]}

3.4 @RequestParam

我们可以使用@RequestParam注解将 Servlet 请求参数(即查询参数或表单数据)绑定到控制器方法参数上。如果目标方法参数类型不是String,则自动应用类型转换。

@RequestParam 注解的name或者value属性表示绑定的请求参数名称,如果不设置,那么默认查找和变量名同名的请求参数,如下所示:

@RestController
public class RequestParamController {

    @GetMapping("/requestParam1")
    public void handle(@RequestParam String str) {
        System.out.println(str);
        //…………
    }
}

访问/requestParam1?str=test,结果如下:

test

默认情况下,使用此注解标注的方法参数是必需的,如果没有对应的变量,将抛出异常,但可以通过将@RequestParam 注解的required标志设置为 false 或使用Java8的java.util.Optional包装原始参数类型来指定方法参数是可选的。

@GetMapping("/requestParam2")
public void handle2(@RequestParam Optional<String> str) {
    //是否存在该参数
    System.out.println(str.isPresent());
    //如果存在该参数,那么输出
    str.ifPresent(System.out::println);
    //…………
}

访问/requestParam2,结果如下:

false

required=false时,如果没有对应的请求参数,控制器方法对应的参数会被赋值为null,如果参数是基本类型,此时会抛出异常,因为基本类型不能赋值为null。可以使用基本类型的包装类,或者java.util.Optional,或者通过defaultValue属性设置默认值!

@GetMapping("/requestParam3")
public void handle3(@RequestParam(required = false,defaultValue = "123") long i) {
    System.out.println(i);
    //…………
}

访问/requestParam3,结果如下:

123

如果将参数类型声明为数组或列表则表示允许解析同一参数名称的多个参数值。

@GetMapping("/requestParam4")
public void handle4(@RequestParam long[] longs) {
    System.out.println(Arrays.toString(longs));
    //…………
}

访问/requestParam4?longs=11&longs=22,结果如下:

[11, 22]

当 @RequestParam 注解标注在一个Map<String, String>MultiValueMap<String, String>上,并且没在注解中指定的参数名称时,那么每个给定参数名称和对应的参数值都将被填充到该Map中。

@GetMapping("/requestParam5")
public void handle5(@RequestParam MultiValueMap<String, String> map) {
    System.out.println(map);
    //…………
}

访问/requestParam5?a=11&a=22&b=33,结果如下:

{a=[11, 22], b=[33]}

@RequestParam是可选的。默认情况下,任何简单值类型的参数(由 BeanUtils#isSimpleProperty确定),并且不由任何其他参数解析器解析,将被视为使用了@RequestParam 注解。

@GetMapping("/requestParam6")
public void handle5(int a, String b) {
    System.out.println(a);
    System.out.println(b);
    //…………
}

访问/requestParam6?a=11&b=bb,结果如下:

11
bb

3.5 @RequestHeader

可以使用@RequestHeader注解将请求头的数据绑定到控制器中的方法参数中。如果目标方法参数类型不是String,则自动应用类型转换。

name和value属性用于指定请求头参数名,如果不指定,那么将方法参数名作为请求头参数名。required用于指示该请求头是否是必须的,默认为true,required=false时,defaultValue用于在不存在该请求头时指定默认值。

@RestController
public class RequestHeaderController {

    @GetMapping("/header")
    public void handle(@RequestHeader(name = "Accept-Encoding", required = false) String acceptEncoding,
                       @RequestHeader(name = "Keep-Alive", required = false) String keepAlive) {
        System.out.println(acceptEncoding);
        System.out.println(keepAlive);
        //…………
    }
}

当 @RequestParam 注解标注在一个Map<String, String>MultiValueMap<String, String>HttpHeaders类型的参数上时,那么每个请求头的数据都将被填充到该Map中。

@GetMapping("/header/all")
public void handle2(@RequestHeader Map<String, String> map,
                    @RequestHeader MultiValueMap<String, String> multiValueMap,
                    @RequestHeader HttpHeaders httpHeaders) {
    System.out.println(map);
    System.out.println(multiValueMap);
    System.out.println(httpHeaders);
    //…………
}

Spring MVC内置支持可用于将逗号分隔的字符串转换为数组或字符串集合或类型转换系统已知的其他类型,这样就避免了手动转换操作。例如,用@RequestHeader(“Accept”)注解的方法参数可以是String类型,也可以是String[]List<String>类型。

3.6 @CookieValue

可以使用@CookieValue注解将 HTTP Cookie 的值绑定到控制器中的方法参数中。如果目标方法参数类型不是String,则自动应用类型转换。

namevalue属性用于指定cookie名,如果不指定,那么将方法参数名作为cookie名。required用于指示该cookie是否是必须的,默认为true,required=false时,defaultValue用于在不存在该cookie时指定默认值。

@RestController
public class CookieValueController {

    @GetMapping("/setCookie")
    public void handle(HttpServletResponse resp) throws IOException {
        //生成一个随机字符串
        String uuid = UUID.randomUUID().toString();
        System.out.println("setCookie: " + uuid);
        //创建Cookie对象,指定名字和值。Cookie类只有这一个构造器
        Cookie cookie = new Cookie("uuid", uuid);
        //在响应中添加Cookie对象
        resp.addCookie(cookie);
    }

    @GetMapping("/cookieValue")
    public void handle1(@CookieValue String uuid) {
        System.out.println("cookieValue: " + uuid);
        //…………
    }
}

首先访问/setCookie,设置一个uuid的cookie,然后访问/cookieValue尝试获取,结果如下:

setCookie: 1ad719b2-fdfd-489b-8f29-ee1e85eb2696
cookieValue: 1ad719b2-fdfd-489b-8f29-ee1e85eb2696
setCookie: 24646fa8-e033-411f-8f6c-b379830666c0
cookieValue: 24646fa8-e033-411f-8f6c-b379830666c0

3.7 @ModelAttribute

3.7.1 用在方法参数上

可以在方法参数上使用@ModelAttribute注解来注入参数属性,如果没找到该属性,则将其实例化,同时,还会使用属性名称与HTTP Servlet请求参数中的同名字段名称的值来进行填充属性,这称为数据绑定!默认的name属性根据参数类型推断而不是参数名。

默认情况下,任何不是简单值类型的参数(由 BeanUtils#isSimpleProperty 确定),并且不由任何其他参数解析器解析,都将被视为使用了@ModelAttribute 注解。

@ModelAttribute的工作大概分为两步:

  1. 第一个步是获取参数属性实例,获取规则为:
    1. 从model存储的数据中查找;
    2. 从@SessionAttributes存储的数据中查找;
    3. 通过Converter依次从URI路径变量、URL参数、Form表单中查找并尝试转换为对应参数类型(查找的参数名基于参数类型设置,通常是类名的小写),找到就返回;无法强制转化为Date参数。
    4. 调用默认构造器器创建对象。
      1. 如果有多个构造器且没有空构造器,将抛出异常,建议只提供一个构造函数!
      2. 如果是基本类型,将抛出异常!
  2. 在找到实例之后,第二步是通过WebDataBinder进行属性绑定和校验:
    1. 依次从URI路径变量、Form表单、URL参数中查找,如果找到同名属性的变量,那么该属性将赋值为找到的值(通过setter方法,并且设置到Converter的类型转换),否则,将使用空属性。URI路径变量、Form表单、URL参数中如果存在同名参数,则后面值的覆盖前面的值!
    2. 可以通过对参数添加 javax.validation.valid 注解或 Spring 的@Validated注解,在数据绑定后自动应用验证。
3.7.1.1 Converter转换器

依靠参数转换器Converter<String,T>可以从URI路径变量、URL参数、Form表单中获取同名参数并转换为参数对象,当然Converter还支持属性绑定时的类型转换!

查找顺序为URI路径变量、URL参数、Form表单参数,找到之后立即返回,不再向后查找!

如下案例,我们定义一个StringToMyModelConverter,它实现了org.springframework.core.convert.converter.Converter接口,并且实现了convert方法,该方法就是类型转换的方法,可以约定前端传递的数据的格式,然后后端按照格式解析!这里,我们约定传递的格式为JSON数据。

/**
 * 把字符串转换MyModel
 */
@Component
public class StringToMyModelConverter implements Converter<String, MyModel> {

    /**
     * String source    传入进来字符串
     *
     * @param source 传入的要被转换的字符串
     * @return 转换后的格式类型
     */
    @Override
    public MyModel convert(String source) {
        ObjectMapper mapper = new ObjectMapper();
        MyModel myModel = null;
        try {
            myModel = mapper.readValue(source, MyModel.class);
            System.out.println("-----------------");
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("转换异常!");
        }
        return myModel;
    }
}

在Spring MVC配置文件中配置类型转换器,将自定义的转换器注册到类型转换服务中去:

<!--conversion-service指定在字段绑定期间用于类型转换的转换服务的 bean 名称。 -->
<!--如果不指定,则表示注册默认常见类型转换器DefaultFormattingConversionService-->
<mvc:annotation-driven enable-matrix-variables="true" conversion-service="conversionService"/>

<!--配置类型转换服务工厂,它除了可以注入自定义类型转换器之外,还会默认创建DefaultConversionService-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <!--注入自定义转换器实例-->
            <ref bean="stringToMyModelConverter"/>
        </set>
    </property>
</bean>

控制器方法:

@GetMapping("/modelAttribute/{myModel}")
public void handle2(@ModelAttribute MyModel myModel) {
    System.out.println(myModel);
}

访问/modelAttribute/{“modelId”:“9999”,“modelType”:“999”,“modelName”:“666”},结果如下:

-----------------
MyModel{modelId=9999, modelType=999, modelName='666'}
3.7.1.2 数据绑定

找到或者创建属性实例之后,会进行数据绑定,依次从URI路径变量、Form表单、URL参数中查找数据,如果找到同名变量,那么将赋值为找到的值,否则,将使用空属性。在有必要的情况下,同样对属性应用Converter转换。

URI路径变量、Form表单、URL参数中如果存在同名参数,则后面的覆盖前面的!

可以通过将@ModelAttribute的binding属性设置为false来禁止数据绑定!

一个简单的案例如下,一个MyModel类:

/**
 * @author lx
 */
public class MyModel {
    private Long modelId;
    private Integer modelType;

    private String modelName;

    public void setModelId(Long modelId) {
        this.modelId = modelId;
    }

    public void setModelType(Integer modelType) {
        this.modelType = modelType;
    }

    public void setModelName(String modelName) {
        this.modelName = modelName;
    }


    @Override
    public String toString() {
        return "MyModel{" +
                "modelId=" + modelId +
                ", modelType=" + modelType +
                ", modelName='" + modelName + '\'' +
                '}';
    }
}

一个Controller:

@RestController
public class ModelAttributeController {

    @PostMapping("/modelAttribute/{modelId}/{modelType}")
    public void handle(@ModelAttribute MyModel myModel) {
        System.out.println(myModel);
    }
}

发送一个post请求,访问/modelAttribute/123/321,表单参数为“modelName:myModel”,结果如下:

MyModel{modelId=123, modelType=321, modelName='myModel'}

数据绑定可能会导致错误。默认情况下将抛出BindException给客户端。若要在控制器方法中检查此类错误并做出处理,可以在@ModelAttribute参数旁边的后一个参数添加BindingResult参数,该参数可以校验绑定是否成功!

在数据绑定之后,还可以通过添加@javax.validation.valid注解或者Spring的@Validated注解以及各种校验注解对数据进行非常方便的校验,这部分后面单独讲解!

3.7.2 用在方法上

被@ModelAttribute注解标注的方法会在控制器方法执行之前先执行,同时,@ModelAttribute方法可以和控制器方法一样的绑定、获取各种参数!通常用于辅助初始化model参数,实际项目中用的并不多。

如果用在@Controller类中,那么作用域就是当前Controller的控制器方法,如果用在@ControllerAdvice类中,那么作用域就是所有Controller的控制器方法。

修饰没有返回值的方法,此时可以在参数上声明一个Map、Model、ModelMap类型的参数,可以将数据存入model中,model中的数据使用范围是当前request。

修饰有返回值的方法。返回值会放到model中。此model属性的名称没有指定,它由返回值类型经过计算来定义,如方法返回User类型,那么属性值是User对象,属性的名称是user,即简单类名(实际上最后会通过Introspector#decapitalize方法得到),也可以通过@ModelAttribute(value=“myModelName”)指定名称。

如果将@ModelAttribute和@RequestMapping标注在同一个方法上,那么方法返回值将被作为model属性!

@ModelAttribute
public MyModel model() {
    System.out.println("---ModelAttribute方法执行---");
    MyModel myModel = new MyModel();
    myModel.setModelId(123L);
    myModel.setModelType(321);
    myModel.setModelName("ModelAttribute");
    System.out.println(myModel.hashCode());
    return myModel;
}

@GetMapping("/modelAttribute")
public void handle5(MyModel myModel) {
    System.out.println("---处理器方法支持---");
    System.out.println(myModel);
    System.out.println(myModel.hashCode());
}

3.8 @SessionAttributes和@SessionAttribute

@SessionAttributes是一个类级别的注释,标注在某个控制器类上之后,该控制器中存入model的属性将存入session中,并且其他的控制器方法将可以共享的使用的session中的model属性。

即,如果属性只存入model中,那么当前request的调用链之间的方法可以共享数据,加了@SessionAttributes之后,存入model中的数据实际上会属性存入session中,不同request请求之间可以共享数据。

@SessionAttributes的name或者value属性是一个String数组,用于指定要存入session的model数据的名称,也可以设置types属性,它是一个Class数组,用于指定要存入session的model数据的类型。

如果想要清除当前类中通过@SessionAttributes存储到session中的数据,可以将通过org.springframework.web.bind.support.SessionStatus作为参数注入控制器方法,并调用setComplete方法,该方法不会清理真正的session中的数据!

在方法参数上使用 @SessionAttribute 注解可以访问预先存在的session域中的会话属性,无论是通过传统Session对象设置的还是通过@SessionAttributes设置的!

如果需要添加或删除指定的session属性,可以将 org.springframe. web.context.request.WebRequest或 javax.servlet.httpSession作为参数注入控制器方法。

@RestController
@SessionAttributes({"myModel", "model"})
public class SessionAttributesController {

    /**
     * 通过@ModelAttribute存入的数据
     */
    @ModelAttribute
    public MyModel model() {
        MyModel myModel = new MyModel();
        myModel.setModelId(111L);
        myModel.setModelType(111);
        myModel.setModelName("ModelAttribute1");
        return myModel;
    }

    @GetMapping("/setsessionAttributes")
    public void handle1(Model model, HttpServletRequest request) {
        //手动存入的model数据
        MyModel myModel = new MyModel();
        myModel.setModelId(222L);
        myModel.setModelType(222);
        myModel.setModelName("ModelAttribute2");
        model.addAttribute("model", myModel);

        //手动存入session的数据
        HttpSession session = request.getSession();
        UUID uuid = UUID.randomUUID();
        System.out.println(uuid);
        session.setAttribute("id", uuid);
    }

    /**
     * 清理session
     */
    @GetMapping("/cleansessionAttributes")
    public void handle2(SessionStatus sessionStatus) {
        sessionStatus.setComplete();
    }
}
@RestController
public class SessionAttributesController2 {

    /**
     * @param myModel 尝试获取通过@ModelAttribute存入的数据
     * @param model   尝试获取手动存入的model数据
     * @param id      尝试获取手动存入session的数据
     */
    @GetMapping("/getsessionAttributes")
    public void handle1(@SessionAttribute(name = "myModel",required = false) MyModel myModel,
                        @SessionAttribute(name = "model",required = false) MyModel model,
                        @SessionAttribute String id) {
        System.out.println(myModel);
        System.out.println(model);
        System.out.println(id);
    }
}

首先访问/sessionAttributes1,随后访问/sessionAttributes2,将可以获取session中的model数据!

d8783090-da1e-495f-ac48-32a554009e20
MyModel{modelId=111, modelType=111, modelName='ModelAttribute1'}
MyModel{modelId=222, modelType=222, modelName='ModelAttribute2'}
d8783090-da1e-495f-ac48-32a554009e20

然后访问/cleansessionAttributes清除session,再次访问/sessionAttributes2,结果如下:

null
null
d8783090-da1e-495f-ac48-32a554009e20

3.9 @RequestAttribute

@SessionAttribute类似,可以使用@RequestAttribute注解访问之前创建的预先存在的request域中的属性。

@Controller
public class RequestAttributeController {

    @GetMapping("/requestAttribute1")
    public String handle1(HttpServletRequest request) {
        request.setAttribute("ra", "requestAttributeTest");
        return "/requestAttribute2";
    }


    @GetMapping("/requestAttribute2")
    @ResponseBody
    public void handle2(@RequestAttribute String ra) {
        System.out.println(ra);
    }

}

3.10 重定向参数

默认情况下,所有的model属性中的基本类型的属性,或者基本类型的数组、集合将自动追加为请求参数。

@Controller
public class RedirectAttributesController {

    @GetMapping("/redirectAttributes1")
    public String handle1(Model model) {
        model.addAttribute("a", "aaa");
        model.addAttribute("b", 111);
        model.addAttribute("c", new Object());
        model.addAttribute("d", new int[]{1, 2, 3});
        return "redirect:/redirectAttributes2";
    }

    @GetMapping("/redirectAttributes2")
    @ResponseBody
    public void handle2(@RequestParam MultiValueMap<String, String> map, HttpServletRequest request) {
        System.out.println(map);
        Map<String, String[]> parameterMap = request.getParameterMap();
        System.out.println(parameterMap);
    }
}

访问/redirectAttributes1,转发后的URL如下:

在这里插入图片描述

很明显,model中可能包含其他不需要追加到URL中的参数,因此这不是一个好方法。为此,处理器方法中可以声明类型为RedirectAttributes 的参数,并用它的addAttribute方法来指定要提供给重定向视图的确切属性,如果控制器方法参数中存在RedirectAttributes参数,并且该方法确实进行了重定向,则使用RedirectAttributes的内容,否则,将使用model的内容。

@GetMapping("/redirectAttributes3")
public String handle3(ModelMap model, RedirectAttributes redirectAttributes) {
    model.addAttribute("a", "aaa");
    model.addAttribute("b", 111);
    model.addAttribute("c", new Object());
    model.addAttribute("d", new int[]{1, 2, 3});

    redirectAttributes.addAttribute("a", "a");
    redirectAttributes.addAttribute("b", 222);
    redirectAttributes.addAttribute("d", new int[]{4, 5});
    return "redirect:/redirectAttributes4";
}

@GetMapping("/redirectAttributes4")
@ResponseBody
public void handle4(@RequestParam MultiValueMap<String, String> map, HttpServletRequest request) {
    Map<String, String[]> parameterMap = request.getParameterMap();
    System.out.println(parameterMap);
    System.out.println(map);
}

RequestMappingHandlerAdapter提供了一个名为ignoreDefaultModelOnRedirect的标志位属性,将此标志设置为 true 可确保在重定向中永远不会使用默认model属性,即使控制器方法中未声明RedirectAttributes参数也是如此。将其设置为 false 意味着如果控制器方法不声明RedirectAttributes参数,则默认model可能在重定向中使用,默认值为false。该参数还可以通过<mvc:annotation-driven>标签的ignore-default-model-on-redirect属性来配置。

RedirectAttributes的addAttribute方法同样会将属性追加到URL后面,对于隐私性不友好,并且对于复杂对象类型无法传递,为此我们可以使用另一个addFlashAttribute方法,flash attributes将会保存在HTTP session中(因此参数不会出现在URL中)。

@GetMapping("/redirectAttributes5")
public String handle5(ModelMap model, RedirectAttributes redirectAttributes) {
    model.addAttribute("a", "aaa");
    model.addAttribute("b", 111);
    model.addAttribute("c", new Object());
    model.addAttribute("d", new int[]{1, 2, 3});

    Object o = new Object();
    System.out.println(o);
    redirectAttributes.addFlashAttribute("a", "aaa");
    redirectAttributes.addFlashAttribute("b", 111);
    redirectAttributes.addFlashAttribute("c", o);
    redirectAttributes.addFlashAttribute("d", new int[]{1, 2, 3});
    return "redirect:/redirectAttributes6";
}

@GetMapping("/redirectAttributes6")
@ResponseBody
public void handle6(@RequestParam MultiValueMap<String, String> map, HttpServletRequest request,
                    @ModelAttribute("a") String a, @ModelAttribute("c") Object c, @ModelAttribute("d") int[] d) {
    //无法获取到参数
    Map<String, String[]> parameterMap = request.getParameterMap();
    System.out.println(parameterMap);
    System.out.println(map);
    //可以通过ModelAttribute获取到参数
    System.out.println(a);
    System.out.println(c);
    System.out.println(Arrays.toString(d));
}

访问/redirectAttributes5,结果如下:

java.lang.Object@612eabb7
{}
{}
aaa
java.lang.Object@612eabb7
[1, 2, 3]

并且重定向的URL中不再带有参数:

在这里插入图片描述

另外,如果重定向到一个页面视图中,那么能直接用el表达式获取对应的值!

如果存在URI变量,那么在重定向的URL中可以直接使用同名变量!

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

3.10.1 Flash Attributes

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

Spring MVC有两个主要的抽象类来支持Flash attributes。 一个是FlashMap,它用于保存Flash属性,而另一个是FlashMapManager,它用于存储,检索和管理FlashMap实例。

Flash attributes的支持始终处于“打开”状态,无需显式启用。但是,如果不使用它,则永远不会导致HTTP session的创建。在每个请求上,都有一个“input(输入)” FlashMap,其属性是从上一个请求(如果有)传递过来的,而“output(输出)” FlashMap的属性是为后续请求保存的。这两个FlashMap实例都可以通过RequestContextUtils中的getInputFlashMap和getOutputFlashMap静态方法在Spring MVC中的任何位置访问。

基于注解的控制器通常不需要直接使用FlashMap。相反,@RequestMapping方法可以接受RedirectAttributes类型的参数,并使用它为重定向方案添加Flash attributes。通过RedirectAttributes添加的Flash attributes会自动传播到“output(输出)” FlashMap。同样,重定向到目标后,来自“input(输入)” FlashMap的属性会自动添加到服务于目标URL的控制器的model中,这就是在上面的案例中可以使用@ModelAttribute绑定属性以及可以在模型页面中直接通过el获取属性的原因!

根据上面的描述,我们还可以在控制器方法中通过RequestContextUtils来获取RedirectAttributes传递的属性:

@GetMapping("/redirectAttributes7")
@ResponseBody
public void handle7(HttpServletRequest request,@ModelAttribute("a") String a, @ModelAttribute("c") Object c, @ModelAttribute("d") int[] d) {
    Map<String, ?> inputFlashMap = RequestContextUtils.getInputFlashMap(request);
    System.out.println(inputFlashMap);
    //可以通过ModelAttribute获取到参数
    System.out.println(a);
    System.out.println(c);
    System.out.println(Arrays.toString(d));
}

我们让/redirectAttributes5的控制器方法重定向到访问/redirectAttributes7,访问/redirectAttributes5之后,结果如下:

java.lang.Object@618c3937
FlashMap [attributes={a=aaa, b=111, c=java.lang.Object@618c3937, d=[I@6d2cd428}, targetRequestPath=/mvc/redirectAttributes7, targetRequestParams={}]
aaa
java.lang.Object@618c3937
[1, 2, 3]

可以看到,input的FlashMap中正好存储着传递的属性!

3.11 Multipart

在启用MultipartResolver后,带有multipart/form-data的POST请求的内容将被解析并作为常规请求参数进行访问,并且支持使用MultipartFile参数接收上传的文件数据。如下案例:

@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 files。

在使用 Servlet 3.0 的StandardServletMultipartResolver而不是Apache的CommonsMultipartResolver时,还可以声明javax.servlet.http.part而不是 Spring 的MultipartFile来作为方法参数或集合值类型。

multipart多部件内容还可以数据绑定到命令对象(即通过@ModelAttribute查找并绑定的参数对象)的某些属性上。

@RequestPartjavax.validation.Valid或Spring 的 @Validated一起使用,可用于多部分文件的校验!

关于文件上传,后面会专门有文章讲解!

3.12 @RequestBody

可以使用@RequestBody注解通过HttpMessageConverter读取请求正文(通常是JSON字符串)并反序列化为控制器方法的参数对象。在前端分离开发的项目中,通常是采用JSON数据交互,因此该注解非常常用!我们通过mvc配置自定义HttpMessageConverter。

另外,如果在@ModelAttribute方法中通过@RequestBody从输入流中获取了数据,那么在真正的控制器方法中便不能再次获取,因为ServletInputStream已在@ModelAttribute方法中被使用并被关闭。

如果是采用JSON数据交互,那么我们应该引入相关JSON依赖,Spring MVC默认依赖jackson来实现JSON的序列化和反序列化,如果存在该jar包,则Spring MVC会自动创建一个MappingJackson2HttpMessageConverter,而无需我们手动注册!

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson
-databind -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.9.8</version>
</dependency>

一个Controller:

@RestController
public class RequestBodyController {

    @PostMapping("/requestBody")
    public void handle(@RequestBody User user) {
        System.out.println(user);
        //…………
    }
}
public class User {

    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

访问/requestBody,并且请求体Content-Type为Content-Type格式,请求体中传递{“age”:12,“name”:“名字”},结果如下:

User{age=12, name='名字'}

成功的进行了JSON数据的反序列化和封装!

可以将@RequestBody注解标注的参数继续标注javax.validation.Valid或 Spring 的 @Validated注解,这两种注解都会导致应用标准 Bean 验证。默认情况下,验证错误会导致MethodArgumentNotValidException,该异常将转换为 400(BAD_REQUEST)响应。或者,可以通过Errors或BindingResult参数在控制器方法内处理异常。关于参数校验,后面会单独讲解!

3.13 HttpEntity

HttpEntity参数可以实现@RequestBody的功能,将请求体反序列化为对应泛型类型的对象实例,但是它还提供获取请求头HttpHeaders的功能!

@RestController
public class HttpEntityController {

    @PostMapping("/httpEntity")
    public void handle(HttpEntity<User> httpEntity) {
        //获取请求体
        User body = httpEntity.getBody();
        System.out.println(body);
        //获取请求头
        HttpHeaders headers = httpEntity.getHeaders();
        System.out.println(headers);
        //…………
    }
}

访问/httpEntity,并且请求体Content-Type为Content-Type格式,请求体中传递{“age”:12,“name”:“名字”},结果如下(我采用postman测试):

User{age=12, name='名字'}
[content-type:"application/json", user-agent:"PostmanRuntime/7.26.5", accept:"*/*", cache-control:"no-cache", postman-token:"9920f335-8cb8-498d-9eaf-3b8f74ee0adb", host:"localhost:8081", accept-encoding:"gzip, deflate, br", connection:"keep-alive", content-length:"26"]

3.14 @ResponseBody

我们可以在方法上使用@ResponseBody注解,表示将会通过HttpMessageConverter将控制器方法返回的实体进行序列化并且设置为响应体,而不会走视图解析的逻辑。

我们此前说的前后端分离以及JSON数据交互的开发方式中,就是通过@RequestBody将前端发送的请求的请求体中的JSON数据反序列化为控制器方法的参数对象实例,通过@ResponseBody将后端控制器方法返回的对象实例序列化为JSON数据然后写入相应体返回给前端!序列化和反序列化都依赖于HttpMessageConverter。

@ResponseBody也支持在@Controller类上使用,在这种情况下,该类的所有控制器方法都继承该注解。这和使用一个@RestController有同样的效果,它的内部将@Controller和@ResponseBody作为元注解。

简单的前后端分离的开发方式如下:

@RestController
public class ResponseBodyController {

    @PostMapping("/responseBody")
    public User handle(@RequestBody User user) {
        System.out.println(user);
        user.setAge(999);
        user.setName("responseBody");
        //返回的实体将会被序列化并且写入响应体,而不会走视图解析的逻辑
        return user;
    }
}

实际开发中,项目应该统一返回一个结果对象,可能包括响应码、消息、数据这三部分,而不仅仅是数据部分!

@ResponseBody同样适用于反应式类型的返回结果,比如Servlet3.0的异步请求返回的DeferredResult

3.15 ResponseEntity

ResponseEntity返回值可以实现@ResponseBody的功能,将返回的实体序列化为JSON数据并写入响应体,但是它还提供设置响应码和响应头的功能!

@RestController
public class ResponseEntityController {

    @PostMapping("/responseEntity")
    public ResponseEntity<User> handle(@RequestBody User user) {
        System.out.println(user);
        user.setAge(888);
        user.setName("responseEntity");
        return ResponseEntity.ok().header("header", "header").body(user);
    }
}

ResponseEntity同样适用于反应式类型的返回结果,比如Servlet3.0的异步请求返回的DeferredResult。

3.16 JSON Views

Spring默认了对Jackson JSON库的支持。

首先我们需要引入jackson的依赖:

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson
-databind -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.9.8</version>
</dependency>

Spring MVC 为Jackson的序列化视图(https://www.baeldung.com/jackson-json-view-annotation)提供内置支持,该视图可以实现只序列化对象中的指定字段。

若要将它与@ResponseBody或ResponseEntity控制器方法一起使用,可以使用 Jackson 的 @JsonView注解来激活序列化视图类,如下例所示:

/**
 * @author lx
 */
@RestController
public class JsonViewController {

    /**
     * 在方法上使用@JsonView注解
     */
    @GetMapping("/client")
    @JsonView(Client.WithoutPasswordView.class)
    public Client getUser() {
        return new Client("eric", "7!jd#h23");
    }
}
/**
 * @author lx
 */
public class Client {

    /**
     * 该接口用于指示返回的JSON视图没有password字段
     */
    public interface WithoutPasswordView {
    }

    /**
     * 该接口用于指示返回的JSON视图具有password字段
     */
    public interface WithPasswordView extends WithoutPasswordView {
    }

    /**
     * 在字段或者getter方法上标注@JsonView注解
     */
    @JsonView(WithoutPasswordView.class)
    private String username;
    @JsonView(WithPasswordView.class)
    private String password;


    public String getUsername() {
        return this.username;
    }

    public String getPassword() {
        return this.password;
    }

    public Client() {
    }

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

上面的案例中,对于实体的字段或者getter方法使用@JsonView注解,并且指定一个类型,在控制器方法上,同样使用@JsonView注解,该注解中具有一个类型,它表示对于实体中所有具有该类型及其父类型的@JsonView注解标注的字段或者getter方法对应的字段都进行序列化,其他字段则不进行序列化!

我们访问/client,结果如下:

在这里插入图片描述

可以看到,password字段并没有被序列化,如果我们将控制其方法的@JsonView的类型改为Client.WithPasswordView.class,那么结果如下:

在这里插入图片描述

可以看到,password字段已被被序列化!

实际上@JsonView注解允许传递一个视图类Class数组,即指定多个视图类,但每个控制器方法只能指定一个@JsonView注解。

3.16.1 Date字段格式化

默认情况下,Jackson序列化返回的JSON数据中,对于Date类型的字段返回毫秒的时间戳!如下案例:

@RequestMapping("/date")
@ResponseBody
public MyDate date() {
    MyDate myDate = new MyDate();
    myDate.setDate(new Date());
    return myDate;
}
public class MyDate {

    private Date date;

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }
}

访问/date,结果如下:

在这里插入图片描述

Jackson提供了@JsonFormat注解用以支持Date等特殊类型的序列化字段格式,如下配置:

public class MyDate {

    @JsonFormat(
            pattern = "yyyy-MM-dd HH:mm:ss",
            timezone = "GMT+8"
    )
    private Date date;

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }
}

其中,pattern属性表示Date序列化为字符串的样式,timezone表示时区,因为Jackson在序列化时间时是按照国际标准时间GMT进行格式化的,而在国内默认时区比GMT+8快8小时,因此需要设置时区为“GMT+8”!

再次访问/date,结果如下:

在这里插入图片描述

可以看到返回的date被格式化为了指定样式,这样前端就不必再对时间戳进行二次转换了,而是可以直接展示结果!

4 Controller Advice

通常,@ExceptionHandler,@InitBinder和@ModelAttribute方法在声明它们的@Controller类(或类层次结构)中生效。如果要使此这些方法更全局地应用(跨不同的控制器),则可以在标有@ControllerAdvice或@RestControllerAdvice的类中声明它们。

@ControllerAdvice注解采用@Component作为元注解诶,这意味着可以通过组件扫描将此类注册为Spring Bean。 @RestControllerAdvice则将@ControllerAdvice和@ResponseBody作为元注解,这意味着@ExceptionHandler方法的返回值可以通过HttpMessageConverter进行序列化(比如转换为JSON字符串)并添加到响应体。

在启动时,具有@RequestMapping和@ExceptionHandler方法的类将会检测@ControllerAdvice类型的Spring bean,然后在运行时应用其方法。全局@ExceptionHandler方法(来自@ControllerAdvice)在本地@ExceptionHandler方法(来自@Controller)之后应用。相比之下,全局@ModelAttribute和@InitBinder方法则在本地@ModelAttribute和@InitBinder方法之前应用。

多个@ControllerAdvice或@RestControllerAdvice类支持order排序,可实现Ordered、PriorityOrdered接口,或者采用@Order、@Priority注解。比较优先级为PriorityOrdered>Ordered>@Order>@Priority,如果没有order值,那么将返回Integer.MAX_VALUE,即最低优先级。

默认情况下,@ControllerAdvice方法可以应用于每个请求(就是所有控制器的所有方法),但可以使用注解上的属性将其缩小应用范围:

  1. @ControllerAdvice具有annotations属性,它是一个Annotation类型的Class数组,表示生效的目标是所有至少使用了其中一个给定注解的控制器!
  2. @ControllerAdvice具有value和basePackages属性,它是一个String类型的数组,表示生效的目标是所有至少属于其中一个包以及子包路径下的控制器!
  3. @ControllerAdvice具有assignableTypes属性,它是一个Class类型的数组,表示生效的目标是所有至少属于其中一个类型的控制器!
  4. 如果声明了多个选择器,那么只要某个控制器只需要符合其中一个。另外,选择器检查在运行时执行,因此添加许多选择器可能会对性能产生负面影响并增加复杂性。

@ExceptionHandler用于异常处理,@InitBinder用于数据绑定时的类型转换,我们后面会详细讲解!

相关文章:

  1. https://spring.io/
  2. Spring Framework 5.x 学习
  3. Spring Framework 5.x 源码

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

;