Spring Validation最佳实践

  • 前言
  • 一、简单使用
  • 二、使用步骤
    • 1.引入依赖
    • 2.requestBody 参数校验
      • (1)在 DTO对象的字段上声明约束注解:
      • (2)在接口方法参数上声明校验注解:
    • 3.requestParam/PathVariable 参数校验
    • 4.统一异常处理
    • 5.分组校验
      • (1)约束注解上声明适用的分组信息 groups:
      • (2)@Validated 注解上指定校验分组:
    • 6.嵌套校验
    • 7.集合校验
      • (1)包装 List 类型,并声明 @Valid 注解:
    • 8.自定义校验
      • (1)自定义约束注解:
      • (2)实现 ConstraintValidator 接口编写约束校验器:
    • 9.编程式校验
    • 10.快速失败(Fail Fast)
    • 11.@Valid 和 @Validated 区别
  • 最后

前言

本文会详细介绍 Spring Validation 各种场景下的最佳实践及其实现原理,死磕到底!


一、简单使用

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

二、使用步骤

1.引入依赖

如果 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 的参数校验实战!

2.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对象。

(1)在 DTO对象的字段上声明约束注解:

如:@NotNull , @Length(min = 2, max = 10)等。

@Data
public 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;
}

(2)在接口方法参数上声明校验注解:

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

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

3.requestParam/PathVariable 参数校验

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

@RequestMapping("/api/user")
@RestController
@Validated
public 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);}
}

4.统一异常处理

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

@RestControllerAdvice
public class CommonExceptionHandler {@ExceptionHandler({MethodArgumentNotValidException.class})@ResponseStatus(HttpStatus.OK)@ResponseBodypublic Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {BindingResult bindingResult = ex.getBindingResult();StringBuilder sb = new StringBuilder("校验失败:");for (FieldError fieldError : bindingResult.getFieldErrors()) {sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");}String msg = sb.toString();return Result.fail(BusinessCode.参数校验失败, msg);}@ExceptionHandler({ConstraintViolationException.class})@ResponseStatus(HttpStatus.OK)@ResponseBodypublic Result handleConstraintViolationException(ConstraintViolationException ex) {return Result.fail(BusinessCode.参数校验失败, ex.getMessage());}
}

进阶使用

5.分组校验

在实际项目中,可能多个方法需要使用同一个 DTO 类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在 DTO 类的字段上加约束注解无法解决这个问题。因此,spring-validation 支持了分组校验的功能,专门用来解决这类问题。还是上面的例子,比如保存 User 的时候,UserId 是可空的,但是更新 User 的时候,UserId的值必须 >= 10000000000000000L;其它字段的校验规则在两种情况下一样。这个时候使用分组校验的代码示例如下:

(1)约束注解上声明适用的分组信息 groups:

@Data
public class UserDTO {@Min(value = 10000000000000000L, groups = Update.class)private Long userId;@NotNull(groups = {Save.class, Update.class})@Length(min = 2, max = 10, groups = {Save.class, Update.class})private String userName;@NotNull(groups = {Save.class, Update.class})@Length(min = 6, max = 20, groups = {Save.class, Update.class})private String account;@NotNull(groups = {Save.class, Update.class})@Length(min = 6, max = 20, groups = {Save.class, Update.class})private String password;/*** 保存的时候校验分组*/public interface Save {}/*** 更新的时候校验分组*/public interface Update {}
}

(2)@Validated 注解上指定校验分组:

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

6.嵌套校验

前面的示例中,DTO 类里面的字段都是基本数据类型和 String 类型。但是实际场景中,有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验。比如,上面保存User信息的时候同时还带有 Job 信息。需要注意的是,此时 DTO 类的对应字段必须标记 @Valid 注解。

@Data
public class UserDTO {@Min(value = 10000000000000000L, groups = Update.class)private Long userId;@NotNull(groups = {Save.class, Update.class})@Length(min = 2, max = 10, groups = {Save.class, Update.class})private String userName;@NotNull(groups = {Save.class, Update.class})@Length(min = 6, max = 20, groups = {Save.class, Update.class})private String account;@NotNull(groups = {Save.class, Update.class})@Length(min = 6, max = 20, groups = {Save.class, Update.class})private String password;@NotNull(groups = {Save.class, Update.class})@Validprivate Job job;@Datapublic static class Job {@Min(value = 1, groups = Update.class)private Long jobId;@NotNull(groups = {Save.class, Update.class})@Length(min = 2, max = 10, groups = {Save.class, Update.class})private String jobName;@NotNull(groups = {Save.class, Update.class})@Length(min = 2, max = 10, groups = {Save.class, Update.class})private String position;}/*** 保存的时候校验分组*/public interface Save {}/*** 更新的时候校验分组*/public interface Update {}
}

嵌套校验可以结合分组校验一起使用。
还有就是嵌套集合校验会对集合里面的每一项都进行校验,例如 List 字段会对这个list里面的每一个 Job 对象都进行校验。
集合只能作为类的属性校验,不能作为接口方法的参数校验,否则无法校验集合里对象的每一个字段。

7.集合校验

如果请求体直接传递了 JSON 数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用 java.util.Collection 下的 List 或者 Set 来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数。

(1)包装 List 类型,并声明 @Valid 注解:

public class ValidationList<E> implements List<E> {@Delegate // @Delegate是lombok注解,可以代表未实现的方法,不需要反复重写@Valid // 一定要加@Valid注解public List<E> list = new ArrayList<>();// 一定要记得重写toString方法@Overridepublic String toString() {return list.toString();}
}

@Delegate 注解受 Lombok 版本限制,1.18.6 以上版本可支持。如果校验不通过,会抛出 NotReadablePropertyException,同样可以使用统一异常进行处理。

比如,我们需要一次性保存多个 User 对象,Controller 层的方法可以这么写:

@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) {// 校验通过,才会执行业务逻辑处理return Result.ok();
}

8.自定义校验

业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。自定义 Spring Validation 非常简单,假设我们自定义加密 id(由数字或者 a-f 的字母组成,32-256 长度)校验,主要分为两步。

(1)自定义约束注解:

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {// 默认错误消息String message() default "加密id格式错误";// 分组Class<?>[] groups() default {};// 负载Class<? extends Payload>[] payload() default {};
}

(2)实现 ConstraintValidator 接口编写约束校验器:

public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");@Overridepublic boolean isValid(String value, ConstraintValidatorContext context) {// 不为null才进行校验if (value != null) {Matcher matcher = PATTERN.matcher(value);return matcher.find();}return true;}
}

这样我们就可以使用 @EncryptId 进行参数校验了!

9.编程式校验

上面的示例都是基于注解来实现自动校验的,在某些情况下,我们可能希望以编程方式调用验证。这个时候可以注入 javax.validation.Validator 对象,然后再调用其 API。

@Autowired
private javax.validation.Validator globalValidator;// 编程式校验
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class);// 如果校验通过,validate为空;否则,validate包含未校验通过项if (validate.isEmpty()) {// 校验通过,才会执行业务逻辑处理} else {for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) {// 校验失败,做其它逻辑System.out.println(userDTOConstraintViolation);}}return Result.ok();
}

10.快速失败(Fail Fast)

Spring Validation 默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启 Fali Fast 模式,一旦校验失败就立即返回。

@Bean
public Validator validator() {ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure()// 快速失败模式.failFast(true).buildValidatorFactory();return validatorFactory.getValidator();
}

11.@Valid 和 @Validated 区别


最后

本文摘自下面公众号:
参考文档

Spring Validation最佳实践相关推荐

  1. Spring Validation最佳实践及其实现原理,参数校验没那么简单!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:六点半起床 juejin.im/post/685654110 ...

  2. Spring Validation 最佳实践及其实现原理,参数校验没那么简单!

    之前也写过一篇关于Spring Validation使用的文章,不过自我感觉还是浮于表面,本次打算彻底搞懂Spring Validation.本文会详细介绍Spring Validation各种场景下 ...

  3. Java Bean Validation 最佳实践

    <h1 class="postTitle"><a id="cb_post_title_url" class="postTitle2& ...

  4. Spring Boot 最佳实践

    转载自  Spring Boot 最佳实践 Spring Boot是用于开发微服务的最流行的Java框架.在本文中,我将与您分享自2016年以来我在专业开发中使用Spring Boot所采用的最佳实践 ...

  5. bean validation校验方法参数_Spring Validation最佳实践及其实现原理,参数校验没那么简单!

    本文同名博客老炮说Java:https://www.laopaojava.com/,每天更新Spring/SpringMvc/SpringBoot/实战项目等文章资料 顺便再给大家推荐一套Spring ...

  6. java事务设计iofo,Spring事务最佳实践

    结论: 1. Spring事务不要用@Transaction(PS:如果用的话最好加到service层的方法上面而不要加到dao层.Controller层),会有一些情况导致事务回滚失败. 2. 最好 ...

  7. Spring JDBC最佳实践(2)

    2019独角兽企业重金招聘Python工程师标准>>> 使用DataSourceUtils进行Connection的管理 由上节代码可知,JdbcTemplate在获取Connect ...

  8. Spring Boot 最佳实践(五)Spring Data JPA 操作 MySQL 8

    ## 一.Spring Data JPA 介绍 JPA(Java Persistence API)Java持久化API,是 Java 持久化的标准规范,Hibernate是持久化规范的技术实现,而Sp ...

  9. Spring Boot 最佳实践(四)模板引擎Thymeleaf集成

    ## 一.Thymeleaf介绍 Thymeleaf是一种Java XML / XHTML / HTML5模板引擎,可以在Web和非Web环境中使用.它更适合在基于MVC的Web应用程序的视图层提供X ...

最新文章

  1. 服务器控件调用JS方法
  2. CentOS快捷键总结
  3. R3 data related to category and hierarchy mapping logic in CRM
  4. 请画图说明tcp/ip协议栈_5年Android程序员面试字节跳动两轮后被完虐,请查收给你的面试指南 - Android木子李老师...
  5. 怎么样把Java的字符串转化为字节数组?
  6. Exchange 2010 迁移到 Exchange 2013 (二)迁移用户邮箱
  7. 中小学生应不应该学英语?
  8. 华为最强AI芯片麒麟980发布:全球首款7nm手机芯片,双核NPU,6项世界第一
  9. Github发现优秀的开源项目
  10. Excel数据透视表按指定文字顺序排序方法
  11. 云计算的定义、本质、技术和未来
  12. 网络基础应用层--HTTP协议
  13. mysql的id生成uuid
  14. Smartbi如何解决传统报表工具制作的问题
  15. springboot异步和切面_SpringBoot强化篇(八)-- Spring AOP
  16. SX1276/77/78学习笔记3 - SX1278 的 FIFO 工作流程和应用注意事项
  17. 一个简单的指数增强策略实现
  18. 智慧医院的建设现状分析
  19. starrtc的使用
  20. 一场沙龙三位大咖30条金句!你关心的5G问题都在这儿了

热门文章

  1. 关于深度学习在生物学领域的应用分析Applications of Deep Learning in Biomedicine
  2. 上海轨道交通十一号线,上海地铁十一号线,上海地铁11号线,上海地铁十一号线线路图...
  3. php实现单笔转账到支付宝功能
  4. android音乐播放器 单曲循环,[Android] MediaPlayer单曲循环不卡顿
  5. 信用炒作成为新型网络犯罪 揭秘×××黑色产业链
  6. 3.基于梯度的攻击——PGD
  7. 基于STM32单片机的智能手环设计(OLED显示)(Proteus仿真+程序+报告)
  8. PTA 7-31 求圆周长和面积
  9. 用手机测试你的肺活量!?
  10. 金链盟大赛新亮点|第一创业证券用区块链创新其报价业务