Spring Validation最佳实践及实现原理

作者:微信小助手

发布时间:2020-12-26T12:23:58

(给ImportNew加星标,提高Java技能)

转自:伍陆七,

链接:juejin.cn/post/6856541106626363399

之前也写过一篇关于 Spring Validation 使用的文章,不过自我感觉还是浮于表面,本次打算彻底搞懂 Spring Validation。本文会详细介绍 Spring Validation 各种场景下的最佳实践及其实现原理,死磕到底!


简单使用


Java API 规范(JSR303)定义了 Bean 校验的标准 validation-api,但没有提供实现。Hibernate Validation 是对这个规范的实现,并增加了校验注解如 @Email、@Length等。Spring Validation 是对 Hibernate Validation 的二次封装,用于支持 Spring MVC 参数自动校验。接下来,我们以 spring-boot 项目为例,介绍 Spring Validation 的使用。


引入依赖


如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator 依赖。如果 spring-boot 版本大于2.3.x,则需要手动引入依赖:


<dependency>    <groupId>org.hibernate</groupId>    <artifactId>hibernate-validator</artifactId>    <version>6.0.1.Final</version></dependency>


对于 Web 服务来说,为防止非法参数对业务造成影响,在 Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:


  1. POST、PUT 请求,使用 requestBody 传递参数;

  2. GET 请求,使用 requestParam/PathVariable 传递参数。


下面我们简单介绍下 requestBody 和 requestParam/PathVariable 的参数校验实战!


requestBody 参数校验


POST、PUT 请求一般会使用 requestBody 传递参数,这种情况下,后端使用 DTO 对象进行接收。只要给 DTO 对象加上 @Validated 注解就能实现自动参数校验。


比如,有一个保存 User 的接口,要求 userName 长度是 2-10,account 和 password 字段长度是 6-20。如果校验失败,会抛出 MethodArgumentNotValidException 异常,Spring 默认会将其转为 400(Bad Request)请求。


DTO 表示数据传输对象(Data Transfer Object),用于服务器和客户端之间交互传输使用的。在 spring-web 项目中可以表示用于接收请求参数的Bean对象。


  • 在 DTO 字段上声明约束注解:


@Datapublic class UserDTO {    private Long userId;
@NotNull @Length(min = 2, max = 10) private String userName;
@NotNull @Length(min = 6, max = 20) private String account;
@NotNull @Length(min = 6, max = 20) private String password;}


  • 在方法参数上声明校验注解:


@PostMapping("/save")public Result saveUser(@RequestBody @Validated UserDTO userDTO) {    // 校验通过,才会执行业务逻辑处理    return Result.ok();}


这种情况下,使用 @Valid 和 @Validated 都可以。


requestParam/PathVariable 参数校验


GET 请求一般会使用 requestParam/PathVariable 传参。如果参数比较多(比如超过6个),还是推荐使用 DTO 对象接收。否则,推荐将一个个参数平铺到方法入参中。在这种情况下,必须在 Controller 类上标注 @Validated 注解,并在入参上声明约束注解(如 @Min 等)。如果校验失败,会抛出 ConstraintViolationException 异常。代码示例如下:


@RequestMapping("/api/user")@RestController@Validatedpublic class UserController {    // 路径变量    @GetMapping("{userId}")    public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {        // 校验通过,才会执行业务逻辑处理        UserDTO userDTO = new UserDTO();        userDTO.setUserId(userId);        userDTO.setAccount("11111111111111111");        userDTO.setUserName("xixi");        userDTO.setAccount("11111111111111111");        return Result.ok(userDTO);    }
// 查询参数 @GetMapping("getByAccount") public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) { // 校验通过,才会执行业务逻辑处理 UserDTO userDTO = new UserDTO(); userDTO.setUserId(10000000000000003L); userDTO.setAccount(account); userDTO.setUserName("xixi"); userDTO.setAccount("11111111111111111"); return Result.ok(userDTO); }}

统一异常处理


前面说过,如果校验失败,会抛出 MethodArgumentNotValidException 或者 ConstraintViolationException 异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。比如我们系统要求无论发送什么异常,HTTP 的状态码必须返回 200,由业务码去区分系统的异常情况。