本文最后编辑于 前,其中的内容可能需要更新。
此篇将全面领略一下JSR-303中提议的Bean Validation特性
Bean Validation
Bean Validation是一种规范,为 JavaBean 和方法验证定义了一组元数据模型和 API 规范,而Hibernate Validator 目前来说应该算是最佳实现,Spring框架底层也依赖了Hibernate Validator。
Bean Validation中核心概念之一是:“约束”(constraint),其提供了一系列用于约束JavaBean的属性的注解,
位于javax.validation.constraints包下,使用这些注解和校验器(javax.validation.Validator
),我们可以很好的对JavaBean进行校验。

基本使用
定一个JavaBean
1 2 3 4 5 6
| public class Person {
@Email private String email; }
|
通过校验器校验JavaBean
1 2 3 4 5 6 7 8
| ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); Validator validator = validatorFactory.getValidator(); Person person = new Person("123456@xx.com"); Set<ConstraintViolation<Person>> violations = validator.validate(person); if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); }
|
注:每种约束都有支持的类型,如@Email显然不能约束Integer类型的属性,具体可用访问以文档或源代码注释为准。如果错误使用则将抛出UnexpectedTypeException异常。属性需要有对应的符合JavaBean规范的getter/setter
多重约束
可使用多个注解或重复使用同一注解(前提带有@Repeatable元注解)约束
1 2 3
| @Email @Size(max = 30) private String email;
|
Null值特殊性
通常情况对于JavaBean属性的约束是除了null值以外的,即必须有值才进行约束。如下约束,当email为null时,不进行约束校验。
1 2
| @Email private String email;
|
如果需要约束其值不能为null可使用@NotNull注解。这么设计的目的应该是使每个注解的职责足够单一
值得注意的是,对于字符串约束的注解@NotBlank(javax的注解,hibernate也有一个同名注解但已弃用),源码中已经表示除了约束空白字符串,还会约束null值。
故若要设计自定义的约束注解时,应该也要遵循这个特性。
级联校验
如有JavaBean中还有JavaBean,可通过@Valid触发级联校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Company {
@NotBlank private String address; }
public class Person {
@Email private String email; @Valid private Company company; }
|
@Valid可以看作是对JavaBean 的一种约束,即告诉Validation具体约束请进入JavaBean解析
分组校验
指定约束的分组,即在该分组(使用Class作为标识)下,才进行约束
1 2
| @Email(groups = Group.Insert.class) private String email;
|
校验时指定要校验的组
1
| Set<ConstraintViolation<Person>> violations = validator.validate(person, Group.Insert.class);
|
可以指定多个组
1 2
| @Email(groups = {Group.Insert.class, Group.Update.class}) private String email;
|
其实所有的约束默认分组为javax.validation.groups.Default
,校验时不指定分组的情况下,默认使用Default组
分组转换
在级联校验中,如果内部的JavaBean使用了其它分组,可以使用@ConvertGroup注解,通过分组转换,触发其内部的分组校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Company {
@NotBlank(groups = CompanyGroup.Chain.class) private String address; }
public class Person {
@Email private String email; @Valid @ConvertGroup(from = Group.Insert.class, to = CompanyGroup.Chain.class) @ConvertGroup(from = Default.class, to = CompanyGroup.Chain.class) private Company company; }
|
动态默认组策略(hibernate)
在Hibernate Validator中,我们可以通过制定默认组策略,额外提供要校验的组。
实现org.hibernate.validator.spi.group.DefaultGroupSequenceProvider
接口提供额外组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @GroupSequenceProvider(PotGroup.class) public class Pot {
private Integer code;
@NotBlank(groups = Group.Query.class) @Null(groups = Group.Insert.class) private String id;
@NotBlank private String color; }
public class PotGroup implements DefaultGroupSequenceProvider<Pot> {
@Override public List<Class<?>> getValidationGroups(Pot pot) {
List<Class<?>> groups = new ArrayList<>(); groups.add(Pot.class); if (pot != null) {
Integer code = pot.getCode(); if (code != null) {
switch (code) {
case 0: groups.add(Group.Insert.class); break; case 1: groups.add(Group.Query.class); break; default: groups.add(Group.Update.class); } } } return groups; } }
|
需要注意的是:
- 实现DefaultGroupSequenceProvider接口的getValidationGroups方法时,必须至少返回指定的泛型本身的class
- 此操作是提供额外的组,内置的Default依旧有效 ,如Provider提供了Group.Insert.class,则会校验Default组和Group.Insert.class组
- 只有触发默认组的校验时,才会以Provider提供的组进一步进行对应组的校验
如
1 2 3 4 5 6 7 8 9
| Pot pot = new Pot(); pot.setCode(0);
validator.validate(pot);
validator.validate(pot, Group.Query.class);
|
需要注意的是,校验组的顺序是根据返回的列表确定的,若途中有一组校验未通过,则后续的组将不会继续校验,且Default组优先级最高
违反约束的提示消息
通过message字段确定消息
1 2
| @Email(message = "邮箱无效") private String email;
|
获取消息
1 2 3 4
| Set<ConstraintViolation<Person>> violations = validator.validate(person); for (ConstraintViolation<T> violation : violations) { System.out.println(violation.getMessage()); }
|
当然标准场景消息肯定是要支持国际化,在官方约束注解中已经有默认实现,如@Email消息为“{javax.validation.constraints.Email.message}”,
1 2 3 4 5
| public @interface Email { String message() default "{javax.validation.constraints.Email.message}"; }
|
在Hibernate Validator实现中提供了国际化消息,即classpath下的ValidationMessages.properties
文件

脚本断言(hibernate)
在Hibernate Validator中,提供了脚本约束@ScriptAssert
注解,顾名思义,你可以通过脚本来对JavaBean进行更灵活更复杂的校验
1 2 3 4 5 6 7
| @ScriptAssert(script = "_this.id == null || _this.desc != null", alias = "_this", lang = "javascript", message = "{ws.spring.validate.pojo.Phone.message}")
public class Phone {
private Integer id; private String desc; }
|
上述代码的@ScriptAssert注解中,script部门便是要编写的脚本,语法则是使用lang属性指定的javascript,而alias表示脚本中使用”_this”代表当前JavaBean对象。
需要注意的是,该脚本更像是表达式,其返回的结果必须是布尔值。
为了方便维护及灵活编码,笔者常用实践更多是在JavaBean中提供判定方法,在脚本中调用该方法
如下,提供的方法必须是public的
1 2 3 4 5 6 7 8 9 10 11 12
| @ScriptAssert(script = "_this.isValid()", alias = "_this", lang = "javascript", message = "{ws.spring.validate.pojo.Phone.message}")
public class Phone {
private Integer id; private String desc;
public boolean isValid() { return this.id == null || this.desc != null; } }
|
至于脚本的执行,实际是通过JSR233 引入的脚本引擎javax.script.ScriptEngineManager
来支持的,默认提供了javascript的支持。在java9后ScriptEngineManager
被ServiceLoader
代替。
约束泛型
考虑以下JavaBean,是否可以约束集合内部的字符串为邮箱地址?
1 2 3 4
| public class Person { private List<String> emails; }
|
实际上我们只要编写成以下形式即可做到约束集合内的值
1
| private List<@Email String> emails;
|
那么再考虑一种情况,自定义的类型,如果也带了泛型,是否支持约束呢?
1 2 3 4
| public class Box<T> { private T entity; }
|
是否支持以下约束声明,以约束Box内部的entity为邮箱地址?
1 2 3 4
| public class Person { private Box<@Email String> box; }
|
很遗憾不支持!
以List之一的ArrayList举例,Bean Validation是怎么知道要校验的是ArrayList内部保存元素的数组,而不是其它如size等字段的呢?很显然背后有一套机制在支持,而我们自定义的类型Box明显是不被识别的,也就不会支持。
这便是提取器javax.validation.valueextraction.ValueExtractor
提取器
Bean Validation能够识别泛型类型并校验器内部的值依靠的正是ValueExtractor提取器。
不仅集合,对数组、Optional对象等进行校验时,Validation会通过ValueExtractor提取真正要进行校验的值。
Hibernate Validator提供了常用集合数组等的提取器

如果要实现上述我们对Box泛型的约束校验,就需要实现它的提取器,其中泛型部分使用@ExtractedValue注明
1 2 3 4 5 6 7 8 9 10 11 12
| public class BoxValueExtractor implements ValueExtractor<Box<@ExtractedValue ?>> {
static final String NODE_NAME = "<Box entity>";
public static final ValueExtractorDescriptor DESCRIPTOR = new ValueExtractorDescriptor(new BoxValueExtractor());
@Override public void extractValues(Box<?> originalValue, ValueReceiver receiver) { receiver.value(NODE_NAME, originalValue.getEntity()); } }
|
提取器通过SPI注册,文件名为META-INF/services/javax.validation.valueextraction.ValueExtractor

方法参数约束
Bean Validation还支持对方法参数和返回值进行约束校验,而非仅限于JavaBean
1 2 3 4 5 6
| public class SomeService { @NotNull public Integer validateBasic(@NotBlank String str) { } }
|
使用ExecutableValidator
校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); Validator validator = validatorFactory.getValidator();
ExecutableValidator ev = validator.forExecutables();
SomeService service = new SomeService(); Method method = SomeService.class.getMethod("validateBasic", String.class);
String str = ""; ExecutableValidator ev = validator.forExecutables(); Set<ConstraintViolation<SomeService>> violations1 = ev.validateParameters(service, method, new Object[]{str}); if (!violations1.isEmpty()) { throw new ConstraintViolationException(violations1); }
Integer retuenValue = 1; Set<ConstraintViolation<SomeService>> violations2 = ev.validateReturnValue(service, method, retuenValue); if (!violations2.isEmpty()) { throw new ConstraintViolationException(violations2); }
|
需要注意的是,Method需要和给定的service是对应契合的,如果存在继承或实现接口等操作,方法获取不当则抛出ConstraintViolationException异常,通常是因为方法签名与给定的目标对象不匹配。
其它诸如分组校验等操作,在方法约束校验上不再赘述。
方法参数脚本断言(hibernate)
同样Hibernate Validator也提供了,对于方法参数的脚本断言@ParameterScriptAssert
。
1 2 3 4 5 6
| public class SomeService { @ParameterScriptAssert(lang = "javascript", script = "a + b == 10") public void validateBasic(Integer a, Integer b) { assert Objects.requireNonNull(a) + Objects.requireNonNull(b) == 10; } }
|
值得注意的是,在javascript中 null与数字运算时被当作0,即 null + 5 = 5。所以上述脚本,a = null, b = 10时,验证也会通过。故java方法内需要留意空指针问题
约束扩展
基本扩展
扩展Bean Validation的约束需要通过注解+约束验证器(javax.validation.ConstraintValidator
)的形式。
声明一个约束注解,必须使用@Constraint
修饰,约束注解必须至少包含message(String)、groups(Class数组)、payload(Class数组)属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Retention(RetentionPolicy.RUNTIME)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Constraint(validatedBy = {ColorConstraintValidator.class}) public @interface Color {
String BLUE = "blue"; String RED = "red"; String YELLOW = "yellow"; String GREEN = "green"; String message() default "{ws.spring.validate.annotation.Color.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
|
实现约束验证器,值得注意的是,验证器在每次验证操作时都会被实例化,也就是说可设计成有状态的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class ColorConstraintValidator implements ConstraintValidator<Color, String> {
private static final Set<String> COLORS = Stream.of(Color.BLUE, Color.GREEN, Color.RED, Color.YELLOW).collect(Collectors.toSet()); @Override public void initialize(Color constraintAnnotation) { }
@Override public boolean isValid(String value, ConstraintValidatorContext context) { return COLORS.contains(value); } }
|
在上述代码中,@Color注解使用的消息我们需要支持国际化,需要按照官方“套路”,在classpath下提供ValidationMessages.properties。

到此扩展完成
组合扩展
如果扩展约束时,只是想把现有的约束整合到一起,可不提供约束验证器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Retention(RetentionPolicy.RUNTIME) @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Constraint(validatedBy = {}) @Size @Pattern(regexp = "[a-zA-Z]*") @ReportAsSingleViolation public @interface LetterText {
@OverridesAttribute(constraint = Size.class, name = "min") int min() default 0;
@OverridesAttribute(constraint = Size.class, name = "max") int max() default 5;
String message() default "{ws.spring.validate.annotation.LetterText.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
|
上述代码中,@LetterText约束由@Size约束与@Pattern约束组合而成,限制了字符串内容必须为字母,且长度默认为0到5。
使用@OverridesAttribute覆盖(或传递)注解的属性值。
使用@ReportAsSingleViolation注解则表明,校验未通过时,使用的消息以@LetterText的为准,@Size和@Pattern的消息将被忽略,即无论数据违反了@Size还是@Pattern的约束,都使用@LetterText的消息
特殊扩展
在方法参数脚本断言@ParameterScriptAssert中,并不像@Email这样单一校验某个参数,而是涉及了多个参数。
这是Bean Validation比较特殊的一种验证器:跨参数验证器
即跨多个方法参数的验证器
如我们自定义一个@AnyNotNull约束注解,来约束方法参数至少有一个不为null
1 2 3 4 5 6 7 8 9
| @Retention(RetentionPolicy.RUNTIME)
@Target({METHOD, CONSTRUCTOR}) @Constraint(validatedBy = AnyNotNullConstraintValidator.class) public @interface AnyNotNull { String message() default "{ws.spring.validate.annotation.AnyNotNull.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
|
@AnyNotNull的验证器,使用@SupportedValidationTarget
+ ValidationTarget.PARAMETERS
注明此为跨参数验证器(即指定验证目标为方法参数),跨参数验证器要验证的目标是方法参数,所以必须以Object[]接收(Object也可,但本质还是Object[])。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @SupportedValidationTarget({ValidationTarget.PARAMETERS}) public class AnyNotNullConstraintValidator implements ConstraintValidator<AnyNotNull, Object[]> {
@Override public boolean isValid(Object[] value, ConstraintValidatorContext context) { for (Object o : value) { if (o != null) { return true; } } return false; } }
|
需要注意的是,跨参数验证器约束注解不能作用在无参方法上
注意到ValidationTarget
枚举有两种类型(版本:jakarta.validation-api: 2.0.2)
1 2 3 4
| public enum ValidationTarget { ANNOTATED_ELEMENT, PARAMETERS }
|
当@SupportedValidationTarget 注解内部指定的验证目标为PARAMETERS,说明验证器为跨参数验证器,若为ANNOTATED_ELEMENT,说明验证器是普通的验证器,也就是说此时约束注解验证时是对方法的返回值进行验证,即避免了歧义。
下面讨论更复杂的场景,若我们的验证器想支持上述两种ValidationTarget,消除歧义是必须的。
以@AnyNotNull 为例,再增加对返回值的校验支持,即返回值不可为null。
根据Bean Validation的约定,@AnyNotNull 需要设定额外的属性validationAppliesTo
以标识要的验证目标,以消除上述歧义
1 2 3 4 5 6 7 8 9 10
| @Retention(RetentionPolicy.RUNTIME) @Target({METHOD, CONSTRUCTOR}) @Constraint(validatedBy = AnyNotNullConstraintValidator.class) public @interface AnyNotNull { String message() default "{ws.spring.validate.annotation.AnyNotNull.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT; }
|
验证器需做如下改造,其中通过target保存此次验证的目标,验证目标对象使用Object接收以兼容验证返回值的场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT, ValidationTarget.PARAMETERS}) public class AnyNotNullConstraintValidator implements ConstraintValidator<AnyNotNull, Object> { private ConstraintTarget target;
@Override public void initialize(AnyNotNull constraintAnnotation) { target = constraintAnnotation.validationAppliesTo(); }
@Override public boolean isValid(Object value, ConstraintValidatorContext context) { if (target == ConstraintTarget.RETURN_VALUE) { return value != null; } else if (target == ConstraintTarget.PARAMETERS) { Object[] arr = (Object[])value; for (Object o : arr) { if (o != null) { return true; } } return false; } else { throw new IllegalStateException("Unsupported ConstraintTarget"); } } }
|
使用@AnyNotNull约束注解时,则需要指明validationAppliesTo
属性为(RETURN_VALUE或PARAMETERS)
1 2 3 4
| @AnyNotNull(validationAppliesTo = ConstraintTarget.RETURN_VALUE) public String someMethod(String str, Integer size) { }
|
同样的,如果约束注解存在多个验证器,且这些验证器既有普通验证器又有跨参数验证器时,也需要设定validationAppliesTo
属性来消除歧义。
到此,Bean Validation介绍完毕,如果想更详细的了解Bean Validation可参考此篇文章:https://blog.csdn.net/qq_43341842/article/details/127560164