Spring 中的Bean 验证 (JSR-303, JSR-380)

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation-beanvalidation https://www.yourbatman.cn/x2y/55d56c0b.html

JSR-303

这个JSR提出很早了(2009年), 它为 基于注解的 JavaBean验证定义元数据模型和API, 通过使用XML验证描述符覆盖和扩展元数据; JSR-303主要是对JavaBean 进行验证, 如方法级别(方法参数/返回值), 依赖注入等的验证是没有指定的;

作为开山之作, 它规定了Java数据校验的模型和API, 这就是Java Bean Validation 1.0版本;

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.0.0.GA</version>
</dependency>

注解 (303)

注解支持类型含义null值是否校验
@AssertFalsebool元素必须是false
@AssertTruebool元素必须是true
@DecimalMaxNumber的子类型(浮点数除外)以及String元素必须是一个数字, 且值必须最大值
@DecimalMin同上元素必须是一个数字, 且值必须>=最大值
@Max同上同上
@Min同上同上
@Digits同上元素构成是否合法(整数部分和小数部分)
@Future时间类型(包括JSR310)元素必须为一个将来(不包含相等)的日期(比较精确到毫秒)
@Past同上元素必须为一个过去(不包含相等)的日期(比较精确到毫秒)
@NotNullany元素不能为null
@Nullany元素必须为null
@Pattern字符串元素需符合指定的正则表达式
@SizeString/Collection/Map/Array元素大小需在指定范围中

所有注解均可标注在: 方法, 字段, 注解, 构造器, 入参等几乎任何地方

JSR-380

当下主流版本, 也就是我们所说的Java Bean Validation 2.0 和Jakarta Bean Validation 2.0版本;

他俩除了叫法不一样, 除了GAV上有变化, 其它地方没任何改变; 
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
 
<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>2.0.1</version>
</dependency>

注解 (380)

相较于1.x版本, 2.0版本在其基础上新增了9个实用注解, 总数到了22个;

注解支持类型含义null值是否校验
@Email字符串元素必须为电子邮箱地址
@NotEmpty容器类型集合的Size必须大于0
@NotBlank字符串字符串必须包含至少一个非空白的字符
@Positive数字类型元素必须为正数(不包括0)
@PositiveOrZero同上同上(包括0)
@Negative同上元素必须为负数(不包括0)
@NegativeOrZero同上同上(包括0)
@PastOrPresent时间类型在@Past基础上包括相等
@FutureOrPresent时间类型在@Futrue基础上包括相等

Hibernate 扩展的注解

Hibernate Validator 附加的 constraint

@Email 被注释的元素必须是电子邮箱地址 @Length 被注释的字符串的大小必须在指定的范围内 @NotEmpty 被注释的字符串的必须非空 @Range 被注释的元素必须在合适的范围内

验证配置对象

org.springframework.validation.annotation.Validated

文档 boot-features-external-config-validation

每当使用Spring的@Validated注解对 @ConfigurationProperties 类进行批注时, Spring Boot 就会尝试对其进行验证; 您可以直接在配置类上使用JSR-303 javax.validation 约束注释; 为此, 请确保在类路径上有兼容的JSR-303实现, 然后将约束注释添加到字段中

@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {
 
	@NotNull
	private InetAddress remoteAddress;
 
	// ... getters and setters
 
}

in short 进行JSR 303 标准(javax.validation)验证对象, 必须保证引入有兼容的 JSR-303的实现, 一般有 hibernate-validator

验证输入表单

https://spring.io/guides/gs/validating-form-input/

in short 进行JSR (javax.validation)验证对象, Bean Validation 2.0

@RestController
public class ItemController {
 
    @RequestMapping("/item/add")
    public void addItem(@Validated Item item, BindingResult bindingResult) {
        doSomething();
    }
}
public class Item {
 
    @NotNull(message = "id不能为空")
    @Min(value = 1, message = "id必须为正整数")
    private Long id;
 
    @Valid // 嵌套验证必须用@Valid
    @NotNull(message = "props不能为空")
    @Size(min = 1, message = "props至少要有一个自定义属性")
    private List<Prop> props;
}
 

@Validated 和 @Valid

@Valid由javax提供, 标准JSR-303规范, 配合BindingResult可以直接提供参数验证结果; @Validated 由 Spring Validator 提供 (Spring’s JSR-303规范, 是标准JSR-303的一个变种)

@Valid: 可以用在方法, 构造函数, 方法参数和成员属性(字段)上 @Valid: 用在方法入参上无法单独提供嵌套验证功能(prop里面的prop属性); 能够用在成员属性(字段)上,提示验证框架进行嵌套验证; 能配合嵌套验证注解@Valid进行嵌套验证; @Valid:不支持分组校验 JSR-303规范, 还没有吸收分组的功能

@Validated: 可以用在类型, 方法和方法参数上; 但是**不能用在成员属性(字段)**上 @Validated: 用在方法入参上无法单独提供嵌套验证功能(prop里面的prop属性); 不能用在成员属性(字段)上,也无法提示框架进行嵌套验证; 能配合嵌套验证注解@Valid进行嵌套验证; @Validated:支持分组,但是无法在嵌套中分组

分组校验

新增和修改对于实体的校验规则是不同的

例如id是自增的时, 新增时id要为空,修改则必须不为空; 新增和修改, 若用的恰好又是同一种实体, 那就需要用到分组校验;

校验注解都有一个groups 属性

javax.validation.constraints.NotNull

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {
 
	String message() default "{javax.validation.constraints.NotNull.message}";
 
	Class<?>[] groups() default { };
...

它是一个数组任意 自定义Class 类型, 可以将校验注解分组

用法

@Data
public class User {
 
    @Null(message = "新增id不能存在值" , groups = Groups.Add.class)
    @NotNull(message = "修改需要指定id" , groups = Groups.Update.class)
    private Integer id;
    @NotBlank(message = "用户名不能为空")
    @NotNull
    public @interface Update {}
    public @interface Add {}
...
 
 
 
 
  @PostMapping("")
    public Result save (@Validated(Groups.Add.class) User user)  {
        return Result.ok();
    }
 

实例-注解校验

//DTO
public class RtUpdateDTO implements Serializable {
    public RtUpdateDTO(){}
    // 订单编号
    @NotBlank(message = "订单编号不能为空!")
    private String orderNo;
}
 
 
// at controller 
@RestController
@RequestMapping("api/uploader")
public class IndexRest {
 
    @PostMapping("/addSellReturn")
    public ResponseStatus addSellReturn(@Validated @RequestBody RtUpdateDTO up) {
        indexService.addSellReturn(up);
        return new ResponseStatus(null);
    }
 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseStatus handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request){
        BindingResult bindingResult = e.getBindingResult();
        //拿到第一个 验证失败的信息
        String fristNotValidMessage = bindingResult.getAllErrors().get(0).getDefaultMessage();
        ResponseStatus status = new ResponseStatus();
        status.setMsg(fristNotValidMessage);//订单编号不能为空!
        status.setCode(400);
        return status;
    }
}
 

实例-编程式校验

//编程式检验
@Autowired
private javax.validation.Validator globalValidator;
...
{
    globalValidator.validate(object, Create.class)
}

踩坑指南

springboot在2.3之后, spring-boot-starter-web 去除了validate依赖 需要自己引入实现, 不然注解无效

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <scope>compile</scope>
</dependency>
 

自定义校验

Spring Validation允许用户自定义校验

  1. 定义校验逻辑
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
 
public class ContainsDataValidator implements ConstraintValidator<ContainsDataValid,String> {
	// 全局变量存放值集合
    private List<String> values = new ArrayList<>();
	
	// 注解初始化时执行
    @Override
    public void initialize(ContainsDataValid constraintAnnotation) {
		// 获取注解中的值
        String[] strList = constraintAnnotation.values();
		// 赋值给全局变量
        values = Arrays.stream(strList).collect(Collectors.toList());
    }
 
	// 自定义的校验规则
    @Override
    public boolean isValid(String o, ConstraintValidatorContext constraintValidatorContext) {
    	// o 为实体属性的值
    	// 判断值是否属于集合中的元素,true 检验通过,false校验不通过
        return values.contains(o);
    }
}
  1. 定义注解
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {ContainsDataValidator.class})// 标明由哪个类执行校验逻辑
public @interface ContainsDataValid {
 
    // 校验出错时默认返回的消息
    String message() default "字段值不正确";
 
    Class<?>[] groups() default { };
 
    Class<? extends Payload>[] payload() default { };
 
    String[] values() default {}; // 指定值
 
    /**
     * 同一个元素上指定多个该注解时使用
     */
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        ContainsDataValid[] value();
    }
 
}