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值是否校验 |
|---|---|---|---|
| @AssertFalse | bool | 元素必须是false | 否 |
| @AssertTrue | bool | 元素必须是true | 否 |
| @DecimalMax | Number的子类型(浮点数除外)以及String | 元素必须是一个数字, 且值必须⇐最大值 | 否 |
| @DecimalMin | 同上 | 元素必须是一个数字, 且值必须>=最大值 | 否 |
| @Max | 同上 | 同上 | 否 |
| @Min | 同上 | 同上 | 否 |
| @Digits | 同上 | 元素构成是否合法(整数部分和小数部分) | 否 |
| @Future | 时间类型(包括JSR310) | 元素必须为一个将来(不包含相等)的日期(比较精确到毫秒) | 否 |
| @Past | 同上 | 元素必须为一个过去(不包含相等)的日期(比较精确到毫秒) | 否 |
| @NotNull | any | 元素不能为null | 是 |
| @Null | any | 元素必须为null | 是 |
| @Pattern | 字符串 | 元素需符合指定的正则表达式 | 否 |
| @Size | String/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值是否校验 |
|---|---|---|---|
| 字符串 | 元素必须为电子邮箱地址 | 否 | |
| @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允许用户自定义校验
- 定义校验逻辑
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);
}
}- 定义注解
@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();
}
}