Bean Validation对象验证标准

words: 2k    views:    time: 8min

Java’s standard for object validation最早在Java EE6中提出,作为Bean Validation 1.0(JSR-303)。它定义了一种在Java对象上执行声明性验证的方式,提供了一个运行时的数据验证框架。更多详细内容可以参考官网:https://beanvalidation.org

Bean Validationn可以让代码变得更简洁清晰,让开发人员在定义数据模型时不必考虑实现框架的限制。当然它不止提供了一些基本的constraint,也可以自定义验证规则,在实际的开发中,可以根据自己的需要组合或开发出更加合适的constraint。

依赖

validation-api主要提供了一套用于在Java Bean对象上执行声明性验证的标准接口和注解,但不包含具体实现,而是留给不同的厂商或项目来提供实现

1
2
3
4
5
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
</dependency>

hibernate-validator是Bean Validation规范的一个实现,它实现了validation-api中定义的接口和功能

1
2
3
4
5
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.3.Final</version>
</dependency>

一般在基于springboot开发的Java应用中,只需添加对应的starter依赖即可。spring-boot-starter-web在2.3.0版本之后取消了集成,需要自己添加依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.7.0</version>
</dependency>

@Valid与@Validated

javax.validation.Valid是标准规范validation-api提供的注解,而org.springframework.validation.annotation.Validated是Spring自定义的一个注解,在spring-context包中,其增强了分组功能,作为JSR-303在spring中的一个变体。它们都可以对方法和参数进行校验,两者可以兼容,但也有一些区别:

区别 @Valid @Validated
提供者 JSR-303规范 spring
分组验证 不支持 支持
嵌套验证 支持 不支持
标记位置 CONSTRUCTOR, FIELD, TYPE_USE, METHOD, PARAMETER TYPE, METHOD, PARAMETER

spring中的Validated还是基于Aop来增强实现的,MethodValidationPostProcessor在Bean的初始化完成之后,会判断类是否被@Validated标记,然后MethodValidationInterceptor会拦截所有方法,执行校验逻辑。最后委派给Validator执行参数和返回值校验,并得到ConstraintViolation进行处理。

实践

参数校验

关于参数校验,用的比较多的应该是在spring mvc的Controller中,用来对接口请求参数进行验证。如下所示:

  • 在Controller上标记@Validated,针对Get类的请求,对参数直接进行校验;
  • 如果是复合对象参数,需要在参数前标记@Validated或@Valid,然后再在对应的类属性上标记校验注解;
InfoController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Validated
@RestController
@RequestMapping("/api/v1/info")
public class InfoController {

@GetMapping("/get")
public Response<Info> get(@NotNull(message = "id can't be null") Integer id) {
return Response.success();
}

@PostMapping("/add")
public Response<Void> add(@Validated Info info) {
return Response.success();
}

@RequestMapping("/edit")
public Response<Void> edit(@Valid Info info) {
return Response.success();
}
}

其实在Service接口层声明参数校验也是一种比较好的方式,这样更方便对返回值进行校验,但这里需要使用@Valid声明,@Validated无效

InfoService.java
1
2
3
4
5
6
public interface InfoService {

@Valid @NotNull(message = "返回值不能为空") Info get(@NotNull(message = "id不能为空") Integer id);

void add(@Valid Info info);
}

然后在对应的实现类上需要标记@Validated

InfoServiceImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Validated
@Service
public class InfoServiceImpl implements InfoService {

@Override
public Info get(Integer id) {
// ...
}

@Override
public Info add(Info info) {
// ...
}
}
分组校验

分组校验主要针对接口参数实体类复用的场景,比如同样一个属性,在这个接口不需要校验,而在另一个接口又需要校验。一种办法是使用VO、BO、DO那种方式将不同接口的参数类完全分开,这样就避免了复用问题。但我们不推荐这种方式,容易导致类型泛滥,我们希望定义尽量精简的类型,如果这样,那么可以使用groups对声明的校验规则进行分组

Info.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class Info {

@NotNull(message = "id can't be null", groups = {ApiEdit.class})
private Long id;

@NotNull(message = "id can't be null", groups = {ApiAdd.class, ApiEdit.class})
private String version;

@NotNull(message = "id can't be null", groups = {ApiAdd.class, ApiEdit.class})
private String application;

private String description;
}

然后在声明@Validated也需要指定对哪些分组生效

InfoController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/api/v1/test")
public class InfoController {

@PostMapping("/add")
public Response<Void> add(@Validated(ApiAdd.class) Info info) {
return Response.success();
}

@RequestMapping("/edit")
public Response<Void> edit(@Validated(ApiEdit.class) Info info) {
return Response.success();
}
}
嵌套校验

嵌套校验比较常见的是一种场景是集合类参数,如果要对集合类参数进行校验,可以使用@Valid声明参数,并在Controller上声明@Validated

InfoController.java
1
2
3
4
5
6
7
8
9
10
@Validated
@RestController
@RequestMapping("/api/v1/test")
public class TestController {

@PostMapping("/list")
public Response<Void> list(@Valid @RequestBody List<Info> info) {
return Response.success();
}
}

有的场景下,需要对复合对象中的一些类型进行校验,那么也可以使用@Valid来声明

InfoList.java
1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
public class InfoList {

@Valid
@NotNull(message = "列表不能为空")
private List<Info> list;

@Valid
@NotNull(message = "info不能为空")
private Info info;

//...
}
自定义校验

这里我们定义一个手机号校验规则

  • 定义注解

@Repeatable和List定义可以让该注解在同一个位置重复多次,通常是不同的配置,比如不同的分组和消息
@Constraint指明约束的验证器,需要实现javax.validation.ConstraintValidator接口
payload有效负载,可以通过payload来标记一些需要特殊处理的操作

Mobile.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Constraint(validatedBy = {MobileValidator.class})
@Repeatable(Mobile.List.class)
@Documented
@Retention(RUNTIME)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
public @interface Mobile {

String message() default "非法手机号码";

Class<?>[] groups() default {};

String regexp() default "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";

Class<? extends Payload>[] payload() default {};

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@interface List {
Mobile[] value();
}
}
  • 定义验证器

验证器的两个类型参数,分别是要验证的注解类,和验证器可以处理的元素类型。

MobileValidator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MobileValidator implements ConstraintValidator<Mobile, String> {

private Pattern pattern;

@Override
public void initialize(Mobile mobile) {
pattern = Pattern.compile(mobile.regexp());
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// Bean验证规范建议将空值视为有效,如果要求非空,需要自己使用@NotNull显式声明
if (value == null) {
return true;
}
return pattern.matcher(value).matches();
}
}

附录

Validation 2.0(JSR-380) 相关的22个注解:

  • 非空检查

    注解 支持类型 备注
    @Null Object 为null
    @NotNull Object 不为null
    @NotBlank CharSequence 不为null,且必须有一个非空格字符
    @NotEmpty CharSequence, Collection, Map, Array 不为null,且length/size > 0
  • Boolean判断

    注解 支持类型 备注
    @AssertTrue boolean, Boolean 为true
    @AssertFalse boolean, Boolean 为false
  • 日期检查

    注解 支持类型 备注
    @Future Date、Calendar等 日期为当前时间之后
    @FutureOrPresent Date、Calendar等 验证日期为当前时间或之后
    @Past Date、Calendar等 日期为当前时间之前
    @PastOrPresent Date、Calendar等 验证日期为当前时间或之前
  • 数值检查

    注解 支持类型 备注
    @Max BigDecimal, BigInteger, byte, short, int, long 小于或等于
    @Min BigDecimal, BigInteger, byte, short, int, long 大于或等于
    @DecimalMax BigDecimal, BigInteger, byte, short, int, long, CharSequence 小于或等于
    @DecimalMin BigDecimal, BigInteger, byte, short, int, long, CharSequence 大于或等于
    @Negative BigDecimal, BigInteger, byte, short, int, long, float, double 负数
    @NegativeOrZero BigDecimal, BigInteger, byte, short, int, long, float, double 负数或零
    @Positive BigDecimal, BigInteger, byte, short, int, long, float, double 正数
    @PositiveOrZero BigDecimal, BigInteger, byte, short, int, long, float, double 正数或零
    @Digits(integer = 3, fraction = 2) BigDecimal, BigInteger, byte, short, int, long, CharSequence 精度限制,整数位数和小数位上限
  • 其它

    注解 支持类型 备注
    @Pattern CharSequence 匹配正则表达式
    @Email CharSequence 邮箱地址
    @Size CharSequence, Collection, Map, Array 大小范围length/size

hibernate-validator的扩展:

注解 支持类型 备注
@Length String 字符串长度范围
@Range 数值类型和String 指定范围
@URL URL地址验证


参考:
1.https://developer.aliyun.com/article/888561