Hibernate Validator 总结大全

article/2025/10/6 21:30:07

背景

代码开发过程中,参数的有效性校验是一项很繁琐的工作, 如果参数简单,就那么几个参数,直接通过ifelse可以搞定,如果参数太多,比如一个大对象有100多个字段作为入参,你如何校验呢? 仍使用ifelse就是体力活了, Hibernate Validator 是很好的选择。

官方文档入口: https://hibernate.org/validator/

文章示例基于6.0版本,可以参考6.0的官方文档:https://docs.jboss.org/hibernate/validator/6.0/reference/en-US/html_single/#validator-gettingstarted

扫码查看原文:
薛定谔的雄猫

maven依赖

Hibernate validator 依赖

<!-- hibernate validator -->
<dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>6.0.13.Final</version>
</dependency>
<dependency><groupId>javax.el</groupId><artifactId>javax.el-api</artifactId><version>3.0.1-b06</version>
</dependency>
<dependency><groupId>org.glassfish.web</groupId><artifactId>javax.el</artifactId><version>2.2.6</version>
</dependency>

为了能让示例代码跑起来的一些必要依赖

<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.8</version><scope>provided</scope>
</dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13</version>
</dependency>

支持的校验注解

javax.validation.constraints 包下面的校验注解都支持,如下面这些注解,基本上见名知意, 就不一一解释了

Max       最大值校验  
Min       最小值校验  
Range     范围校验,Min和Max的组合  
NotBlank  不为空白字符的校验  
NotEmpty  数组、集合等不为空的校验  
NotNull   空指针校验  
Email     邮箱格式校验  
.... 

下面通过示例代码来说明校验器常用的几种使用方式: 简单对象校验、分组校验、

简单对象校验

建一个需要检验的参数类:

@Data
public class SimpleBean {@NotBlank(message = "姓名不能为空")private String name;@NotNull(message = "年龄不能为空")@Range(min = 0, max = 100, message = "年龄必须在{min}和{max}之间")private Integer age;@NotNull(message = "是否已婚不能为空")private Boolean isMarried;@NotEmpty(message = "集合不能为空")private Collection collection;@NotEmpty(message = "数组不能为空")private String[] array;@Emailprivate String email;/*真实场景下面可能还有几十个字段省略 ... ...*/}

校验测试

public class ValidateTest {//初始化一个校验器工厂  private static ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure()//校验失败是否立即返回: true-遇到一个错误立即返回不在往下校验,false-校验完所有字段才返回.failFast(false).buildValidatorFactory();Validator validator = validatorFactory.getValidator();/*** 简单对象校验*/@Testpublic void testSimple() {SimpleBean s=new SimpleBean();s.setAge(5);s.setName(" ");s.setEmail("email");Set<ConstraintViolation<SimpleBean>> result=validator.validate(s);System.out.println("遍历输出错误信息:");//getPropertyPath() 获取属性全路径名//getMessage() 获取校验后的错误提示信息result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage()));}
}

测试结果

遍历输出错误信息:
email:不是一个合法的电子邮件地址
collection:集合不能为空
array:数组不能为空
name:姓名不能为空
isMarried:是否已婚不能为空

嵌套对象校验

嵌套对象

上面是简单对象的校验,我们来尝试嵌套对象的校验,类结构如下:

|--OrgBean  
|----EmployeeBean  
|------List<PersonBean>  

OrgBean.java代码,对于嵌套对象校验要注意, 需要在内部引用的对象上用到@Valid注解,否则不会校验被引用对象的内部字段

@Data
public class OrgBean {@NotNullprivate Integer id;@Valid  //如果此处不用Valid注解,则不会去校验EmployeeBean对象的内部字段  @NotNull(message = "employee不能为空")private EmployeeBean Employee;
}

EmployeeBean.java代码

@Data
public class EmployeeBean {@Valid@NotNull(message = "person不能为空")/*** 此处用到容器元素级别的约束: List<@Valid @NotNull PersonBean>  * 会校验容器内部元素是否为null,否则为null时会跳过校验* NotNull注解的target包含ElementType.TYPE_USE,因此NotNull可以给泛型注解*/private List<@Valid @NotNull PersonBean> people;
}

PersonBean.java

@Data
public class PersonBean {@NotBlank(message = "姓名不能为空")private String name;@NotNull(message = "年龄不能为空")@Range(min = 0, max = 100, message = "年龄必须在{min}和{max}之间")private Integer age;@NotNull(message = "是否已婚不能为空")private Boolean isMarried;@NotNull(message = "是否有小孩不能为空")private Boolean hasChild;@NotNull(message = "小孩个数不能为空")private Integer childCount;@NotNull(message = "是否单身不能为空")private Boolean isSingle;}

校验测试代码

@Test
public void testNested() {PersonBean p=new PersonBean();p.setAge(30);p.setName("zhangsan");//p.setIsMarried(true);PersonBean p2=new PersonBean();p2.setAge(30);//p2.setName("zhangsan2");p2.setIsMarried(false);//p2.setHasChild(true);OrgBean org=new OrgBean();//org.setId(1);List<PersonBean> list=new ArrayList<>();list.add(p);list.add(p2);//增加一个null,测试是否会校验元素为nulllist.add(null);EmployeeBean e=new EmployeeBean();e.setPeople(list);org.setEmployee(e);Set<ConstraintViolation<OrgBean>> result=validator.validate(org);System.out.println("遍历输出错误信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage()));}

测试结果

id:不能为null
Employee.people[0].childCount:小孩个数不能为空
Employee.people[0].isSingle:是否单身不能为空
Employee.people[1].hasChild:是否有小孩不能为空
Employee.people[0].isMarried:是否已婚不能为空
Employee.people[1].name:姓名不能为空
Employee.people[1].childCount:小孩个数不能为空
Employee.people[2].<list element>:不能为null
Employee.people[0].hasChild:是否有小孩不能为空
Employee.people[1].isSingle:是否单身不能为空

结果分析:
(1)可以看到打印结果中校验的属性名有一长串: Employee.people[0].childCount
这是由于ConstraintViolation.getPropertyPath()函数返回的是属性的全路径名称。
(2)还有List元素中的值为null也进行了校验:Employee.people[2].:不能为null
这是因为使用了容器元素级别的校验,这种校验器可以使用在泛型参数里面,如注解在List元素的泛型里面增加@NotNull注解: private List<@Valid @NotNull PersonBean> people;
如果没有该注解,则list.dd(null)添加的空指针元素不会被校验。

/*** 此处用到容器元素级别的约束 List<@Valid @NotNull PersonBean> 会校验容器内部元素是否为null,否则为null时会跳过校验* NotNull注解的target包含ElementType.TYPE_USE,因此NotNull可以给泛型注解*/
private List<@Valid @NotNull PersonBean> people;

Hibernate Validator 约束级别

(1)字段级别: 在字段上面添加校验注解
本质上就是可以添加在字段上的注解,@Target({ElementType.FIELD})。

(2)属性级别: 在方法上面添加注解,如注解在getName()方法上
本质上就是可以添加在方法上的注解,@Target({ElementType.METHOD}) 。

(3)容器级别:在容器里面添加注解
本质上就是可以添加在泛型上的注解,这个是java8新增的特性,@Target({ElementType.TYPE_USE})。
如这些类都可以支持容器级别的校验:java.util.Iterable实现类,java.util.Map的key和values,java.util.Optional,java.util.OptionalInt,java.util.OptionalDouble,java.util.OptionalLong 等, 如:
List<@Valid @NotNull PersonBean> people;
private Map<@Valid Part, List<@Valid Manufacturer>> partManufacturers;

(4)类级别:添加在类上面的校验注解
需要@Target({ElementType.TYPE})标注,当然如果有@Target({ElementType.TYPE_USE})也行,因为TYPE_USE包含TYPE。

分组校验

有这样一个需求:当People对象为已婚时(isMarried字段为true),需要校验”配偶姓名“、”是否有小孩“等字段不能为空,当People对象为未婚时,需要校验“是否单身”等其他字段不能为空, 这种需求可以通过分组检验来实现,将校验逻辑分为两个组,然后每次调用校验接口时指定分组即可实现不同的校验。 如果不管“是否已婚”都需要校验的字段(如姓名、年龄这些字段等),则可以同时指定两个分组。

静态分组

静态分组主要在类上面是使用GroupSequence注解指定一个或者多个分组,用于处理不同的校验逻辑,我觉得这个基本上是写死的不能更改,用不用分组区别不大,因此没什么好说的,可以跳过直接看后面的动态分组。

@GroupSequence({ Group.UnMarried.class, Group.Married.class })
public class RentalCar extends PeopleBean {... ... 
}

动态分组

“未婚”和“已婚”两个分组的代码如下,由于分组必须是一个Class,而且有没有任何实现只是一个标记而已,因此我可以用接口。

public interface Group {//已婚情况的分组校验interface Married {}//未婚情况的分组校验interface UnMarried {}}

校验对象:People2Bean.java

@Data
public class People2Bean {//不管是否已婚,都需要校验的字段,groups里面指定两个分组@NotBlank(message = "姓名不能为空",groups = {Group.UnMarried.class, Group.Married.class})private String name;@NotNull(message = "年龄不能为空",groups = {Group.UnMarried.class, Group.Married.class})@Range(min = 0, max = 100, message = "年龄必须在{min}和{max}之间",groups = {Group.UnMarried.class, Group.Married.class})private Integer age;@NotNull(message = "是否已婚不能为空",groups = {Group.UnMarried.class, Group.Married.class})private Boolean isMarried;//已婚需要校验的字段@NotNull(message = "配偶姓名不能为空",groups = {Group.Married.class})private String spouseName;//已婚需要校验的字段@NotNull(message = "是否有小孩不能为空",groups = {Group.Married.class})private Boolean hasChild;//未婚需要校验的字段@NotNull(message = "是否单身不能为空",groups = {Group.UnMarried.class})private Boolean isSingle;
}

测试代码:通过isMarried的值来动态指定分组校验

@Test
public void testGroup() {PeopleBean p=new PeopleBean();p.setAge(30);p.setName(" ");p.setIsMarried(false);Set<ConstraintViolation<PeopleBean>> result;//通过isMarried的值来动态指定分组校验if(p.getIsMarried()){//如果已婚,则按照已婚的分组字段result=validator.validate(p, Group.Married.class);}else{//如果未婚,则只校验未婚的分组字段result=validator.validate(p, Group.UnMarried.class);}System.out.println("遍历输出错误信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage()));
}

测试结果,可以发现,未婚校验了isSingle字段,符合预期

遍历输出错误信息:
name:姓名不能为空
isSingle:是否单身不能为空

将上述代码中的isMarried设置为true:p.setIsMarried(false) 再次执行结果如下,也是符合预期的

遍历输出错误信息:
name:姓名不能为空
hasChild:是否有小孩不能为空
spouseName:配偶姓名

动态分组优化

有没有发现上面的分组校验代码实现不够好?本来校验我是要完全交给validator框架的,但是我还得在校验框架之外面额外判断isMarried再来决定校验方式(如下代码),这样校验代码从校验框架外泄了,不太优雅,有没有优化的空间呢?

if(p.getIsMarried()){//如果已婚,则按照已婚的分组字段result=validator.validate(p, Group.Married.class);
}else{//如果未婚,则只校验未婚的分组字段result=validator.validate(p, Group.UnMarried.class);
}

其实通过DefaultGroupSequenceProvider接口可以优化,这才是真正的动态分组校验,在该接口实现中判断isMarried值,来实现动态设置分组,也就是将校验的额外判断逻辑从校验框架外层转移到了校验框架中,外层业务代码只需要调用校验接口即可,而无需关注具体的校验逻辑,这样的框架才是优秀的。

如下PeopleGroupSequenceProvider.java类实现了DefaultGroupSequenceProvider接口

public class PeopleGroupSequenceProvider implements DefaultGroupSequenceProvider<People2Bean> {@Overridepublic List<Class<?>> getValidationGroups(People2Bean bean) {List<Class<?>> defaultGroupSequence = new ArrayList<>();// 这里必须将校验对象的类加进来,否则没有Default分组会抛异常,这个地方还没太弄明白,后面有时间再研究一下  defaultGroupSequence.add(People2Bean.class);if (bean != null) {Boolean isMarried=bean.getIsMarried();///System.err.println("是否已婚:" + isMarried + ",执行对应校验逻辑");if(isMarried!=null){if(isMarried){System.err.println("是否已婚:" + isMarried + ",groups: "+Group.Married.class);defaultGroupSequence.add(Group.Married.class);}else{System.err.println("是否已婚:" + isMarried + ",groups: "+Group.UnMarried.class);defaultGroupSequence.add(Group.UnMarried.class);}}else {System.err.println("isMarried is null");defaultGroupSequence.add(Group.Married.class);defaultGroupSequence.add(Group.UnMarried.class);}}else{System.err.println("bean is null");}return defaultGroupSequence;}
}

People2Bean.java类上要用到@GroupSequenceProvider注解指定一个GroupSequenceProvider

@GroupSequenceProvider(PeopleGroupSequenceProvider.class)
public class People2Bean {//字段同上   //... ...
}

测试代码

@Test
public void testGroupSequence(){People2Bean p=new People2Bean();p.setAge(30);p.setName(" ");System.out.println("----已婚情况:");p.setIsMarried(true);Set<ConstraintViolation<People2Bean>> result=validator.validate(p);System.out.println("遍历输出错误信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage()));System.out.println("----未婚情况:");p.setIsMarried(false);result=validator.validate(p);System.out.println("遍历输出错误信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage()));}

测试结果符合预期

----已婚情况:
遍历输出错误信息:
name:姓名不能为空
spouseName:配偶姓名不能为空
hasChild:是否有小孩不能为空
----未婚情况:
遍历输出错误信息:
name:姓名不能为空
isSingle:是否单身不能为空

自定义校验器

Hibernate中有不少约束校验器,但是不一定能满足你的业务,因此它还支持自定义约束校验器,一般是一个约束注解配合一个校验器使用,校验器需要实现ConstraintValidator接口,然后约束注解中通过`@Constraint(validatedBy = {ByteLengthValidator.class})绑定校验器即可。 这里我写三个示例来说明:

自定义枚举校验

在开发过程中,有很多参数类型限制只能使用某些枚举值,我们可以通过自定义的校验器来做约束,以最简单的性别举例,在我国性别只有男和女,校验注解定义如下: EnumRange.java

@Documented
@Constraint(//这个配置用于绑定校验器:EnumRangeValidatorvalidatedBy = {EnumRangeValidator.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(EnumRange.List.class)
public @interface EnumRange {//自定义默认的消息模板String message() default "枚举值不正确,范围如下:{}";//枚举类,用于在校验器中限定值的范围Class<? extends Enum> enumType();//分组 Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)@Documented//支持数组校验public @interface List {EnumRange[] value();}
}

校验器类:EnumRangeValidator.java 实现 ConstraintValidator 接口, ConstraintValidator<EnumRange,String> 接口的第一个泛型参数绑定EnumRange注解,第二个参数绑定要校验的值类型,这里是String。

public class EnumRangeValidator implements ConstraintValidator<EnumRange,String> {private Set<String> enumNames;private String enumNameStr;@Overridepublic void initialize(EnumRange constraintAnnotation) {Class<? extends Enum> enumType=constraintAnnotation.enumType();if(enumType==null){throw new IllegalArgumentException("EnumRange.enumType 不能为空");}try {//初始化:将枚举值放到Set中,用于校验Method valuesMethod = enumType.getMethod("values");Enum[] enums = (Enum[]) valuesMethod.invoke(null);enumNames = Stream.of(enums).map(Enum::name).collect(Collectors.toSet());enumNameStr = enumNames.stream().collect(Collectors.joining(","));} catch (Exception e) {throw new RuntimeException("EnumRangeValidator 初始化异常",e);}}@Overridepublic boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {if(value==null){return true;}boolean result = enumNames.contains(value);if(!result){//拿到枚举中的message,并替换变量,这个变量是我自己约定的,//你在使用注解的message中有花括号,这里会被替换为用逗号隔开展示的枚举值列表String message = constraintValidatorContext.getDefaultConstraintMessageTemplate().replace("{}",enumNameStr);//禁用默认值,否则会有两条messageconstraintValidatorContext.disableDefaultConstraintViolation();//添加新的messageconstraintValidatorContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();}return result;}
}

我们来定义一个性别的枚举:当然,你还可以用其他自定义枚举,只要是枚举值这个校验就就能生效

public enum SexEnum {F("女"),M("男");String desc;SexEnum(String desc){this.desc=desc;}}

被校验的类:Person2Bean.java

@Data
public class Person2Bean {@NotBlank(message = "姓名不能为空")private String name;@Range(min = 0, max = 100, message = "年龄必须在{min}和{max}之间")private Integer age;//性别用到上面的自定义注解,并指定枚举类SexEnum,message模板里面约定变量绑定“{}”  @EnumRange(enumType = SexEnum.class, message = "性别只能是如下值:{}")private String sex;}

校验测试代码

@Test
public void testSelfDef() {Person2Bean s=new Person2Bean();//性别设置为“A",校验应该不通过  s.setSex("A");//s.setFriendNames(Stream.of("zhangsan","李四思").collect(Collectors.toList()));Set<ConstraintViolation<Person2Bean>> result=validator.validate(s);System.out.println("遍历输出错误信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage()));
}

校验结果如下:性别设置为“A",校验应该不通过不是枚举值中的F和M,因此符合预期

遍历输出错误信息:
sex:性别只能是如下值:F,M
name:姓名不能为空

自定义字节数校验器

参数的字段值要存入数据库,比如某个字段用的 Oracle 的 Varchar(4) 类型,那么该字段值的不能超过4个字节,一般可能会想到应用 @Length 来校验,但是该校验器校验的是字符字符串长度,即用 String.length() 来校验的,英文字母占用的字节数与String.length()一致没有问题,但是中文不行,根据不同的字符编码占用的字节数不一样,比如一个中文字符用UTF8占用3个字节,用GBK占用两个字节,而一个英文字符不管用的什么编码始终只占用一个字节,因此我们来创建一个字节数校验器。

校验注解类:ByteMaxLength.java

@Documented
//绑定校验器:ByteMaxLengthValidator
@Constraint(validatedBy = {ByteMaxLengthValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(ByteMaxLength.List.class)
public @interface ByteMaxLength {//注意这里的max是指最大字节长度,而非字符个数,对应数据库字段类型varchar(n)中的nint max() default Integer.MAX_VALUE;String charset() default "UTF-8";Class<?>[] groups() default {};String message() default "【${validatedValue}】的字节数已经超过最大值{max}";Class<? extends Payload>[] payload() default {};@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface List {ByteMaxLength[] value();}
}

校验最大字节数的校验器:ByteMaxLengthValidator.java ,注意里面约定了两个绑定变量:chMax 和 enMax,分别对应中、英文的最大字符数,用于message模板中使得错误提示更加友好

public class ByteMaxLengthValidator implements ConstraintValidator<ByteMaxLength,String> {private int max;private Charset charset;@Overridepublic void initialize(ByteMaxLength constraintAnnotation) {max=constraintAnnotation.max();charset=Charset.forName(constraintAnnotation.charset());}@Overridepublic boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {if(value==null){return true;}int byteLength = value.getBytes(charset).length;//System.out.println("byteLength="+byteLength);boolean result = byteLength<=max;if(!result){//这里随便用一个汉字取巧获取每个中文字符占用该字符集的字节数int chBytes = "中".getBytes(charset).length;System.out.println("chBytes="+chBytes);//计算出最大中文字数int chMax = max/chBytes;//拿到枚举中的message,并替换变量,这个变量是我自己约定的,//约定了两个绑定变量:chMax 和 enMaxString message = constraintValidatorContext.getDefaultConstraintMessageTemplate().replace("{chMax}",String.valueOf(chMax)).replace("{enMax}",String.valueOf(max));//禁用默认值,否则会有两条messageconstraintValidatorContext.disableDefaultConstraintViolation();//添加新的messageconstraintValidatorContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();}return result;}
}

校验类

@Data
public class Person2Bean {/*** message里面用到了前面约定的两个变量:chMax和enMax,* 至于${validatedValue}是框架内置的变量,用于获取当前被校验对象的值*/@ByteMaxLength(max=4,charset = "UTF-8", message = "姓名【${validatedValue}】全中文字符不能超过{chMax}个字,全英文字符不能超过{enMax}个字母")private String name;/*** 该注解可以用于泛型参数:List<String> ,* 这样可以校验List中每一个String元素的字节数是否符合要求*/private List<@ByteMaxLength(max=4,charset = "UTF-8",message = "朋友姓名【${validatedValue}】的字节数不能超过{max}")String> friendNames;@Range(min = 0, max = 100, message = "年龄必须在{min}和{max}之间")private Integer age;//@EnumRange(enumType = SexEnum.class, message = "性别只能是如下值:{}")private String sex;}

校验测试代码

@Test
public void testSelfDef() {Person2Bean s=new Person2Bean();s.setName("张三");//s.setSex("M");s.setFriendNames(Stream.of("zhangsan","李四思","张").collect(Collectors.toList()));Set<ConstraintViolation<Person2Bean>> result=validator.validate(s);System.out.println("遍历输出错误信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage()));
}

运行结果,可以发现List中的元素也可以校验

遍历输出错误信息:
name:姓名【张三】全中文字符不能超过1个字,全英文字符不能超过4个字母
friendNames[0].<list element>:朋友姓名【zhangsan】的字节数不能超过4
friendNames[1].<list element>:朋友姓名【李四思】的字节数不能超过4

由于上面用的UTF-8编码,max=4,中文占三个字节,因此只能一个中文字符,换成GBK试一下

@ByteMaxLength(max=4,charset = "GBK", message = "姓名【${validatedValue}】全中文字符不能超过{chMax}个字,全英文字符不能超过{enMax}个字母")
private String name;//可以用于校验数组元素:List<String>
private List<@ByteMaxLength(max=4,charset = "GBK",message = "朋友姓名【${validatedValue}】的字节数不能超过{max}")String> friendNames;

同样的测试代码发现校验结果不一样了:name="张三"校验通过了,由于GBK中文值占2个字节而不是3个字节

friendNames[1].<list element>:朋友姓名【李四思】的字节数不能超过4
friendNames[0].<list element>:朋友姓名【zhangsan】的字节数不能超过4

自定义类级别的校验器

类级别的校验器没什么特别的,无非是其可以注解到类上面,即由@Target({ElementType.TYPE})标注的注解。但是某些特殊场景非常有用,字段上的校验器只能用于校验单个字段,如果我们需要对多个字段进行特定逻辑的组合校验就非常有用了。

下面的示例用于校验:订单价格==商品数量*商品价格

@OrderPrice注解:OrderPrice.java

@Documented
//绑定校验器
@Constraint(validatedBy = {OrderPriceValidator.class})
//可以发现没有 ElementType.TYPE 该注解也能用到类上面,这是因为ElementType.TYPE_USE包含ElementType.TYPE
@Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(OrderPrice.List.class)
public @interface OrderPrice {Class<?>[] groups() default {};String message() default "订单价格不符合校验规则";Class<? extends Payload>[] payload() default {};@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface List {OrderPrice[] value();}
}

校验器: OrderPriceValidator.java,注意ConstraintValidator<OrderPrice, OrderBean>第二个泛型参数为被校验的类OrderBean

public class OrderPriceValidator implements ConstraintValidator<OrderPrice, OrderBean> {@Overridepublic void initialize(OrderPrice constraintAnnotation) {}@Overridepublic boolean isValid(OrderBean order, ConstraintValidatorContext constraintValidatorContext) {if(order==null){return true;}return order.getPrice()==order.getGoodsPrice()*order.getGoodsCount();}}

被校验类:OrderBean.java

@Data
//类上面用到自定义的校验注解
@OrderPrice
public class OrderBean {@NotBlank(message = "商品名称不能为空")private String goodsName;@NotNull(message = "商品价格不能为空")private Double goodsPrice;@NotNull(message = "商品数量不能为空")private Integer goodsCount;@NotNull(message = "订单价格不能为空")private Double price;@NotBlank(message = "订单备注不能为空")private String remark;}

校验测试代码

@Test
public void testSelfDef2() {OrderBean o=new OrderBean();o.setGoodsName("辣条");o.setGoodsCount(5);o.setGoodsPrice(1.5);o.setPrice(20.5);Set<ConstraintViolation<OrderBean>> result=validator.validate(o);System.out.println("遍历输出错误信息:");result.forEach(r-> System.out.println(r.getPropertyPath()+":"+r.getMessage()));
}

测试执行结果如下:符合预期

遍历输出错误信息:
:订单价格不符合校验规则
remark:订单备注不能为空

EL表达式

其实在上面的示例中,可以看到在message中已经使用到了EL表达式:

@ByteMaxLength(max=4,charset = "GBK", message = "姓名【${validatedValue}】全中文字符不能超过{chMax}个字,全英文字符不能超过{enMax}个字母")private String name;

包含在${}之间的就是EL表达式,比如这里的${validatedValue} , validatedValue是内置的变量,用于存储当前被校验对象的值,更复杂的用法不仅仅是取值,还可以做各种逻辑运算、内置函数调用等,如下面这些用法:

@Size(min = 2,max = 14,message = "The license plate '${validatedValue}' must be between {min} and {max} characters long"
)@Min(value = 2,message = "There must be at least {value} seat${value > 1 ? 's' : ''}"
)DecimalMax(value = "350",message = "The top speed ${formatter.format('%1$.2f', validatedValue)} is higher than {value}"
)@DecimalMax(value = "100000", message = "Price must not be higher than ${value}")

上面有一种不包含$符号,只包含在花括号{}的表达式,这种表达式只能用于简单的变量替换,如果没有该变量也不会报错,只是会被原样输出,而${validatedValue}这个里面的表达式如果错了则会抛异常。

比如@Length注解有两个变量min和max,其实像groups、payload都可以获取到其值,也就是在message中可以获取当前注解的所有成员变量值(除了message本身)。

public @interface Length {int min() default 0;int max() default 2147483647;String message() default "{org.hibernate.validator.constraints.Length.message}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};... ...
}

如:

@Length(min=1,max=10,message = "字符长度请控制在{min}到{max}之间,分组校验:{groups},消息:{message}")
private String name; 

上述代码的message中{min}、{max}、{groups}最终在错误消息输出时hi可以被对应的变量值替换的,但是{message}就会被原样输出,因为不可能在message里面获取它自己的值。

校验框架对EL表达式的支持对于自定义消息模板非常有用,可以使错误消息提示更加友好。

SpringMVC中如何使用

上面的示例代码都是在单元测试中使用,validator类也是自己手动创建的,在spring中validator需要通过容器来创建,除了上面的maven依赖,还需在spring.xml中为校验器配置工厂bean

<mvc:annotation-driven validator="validator"/>
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"><property name="providerClass" value="org.hibernate.validator.HibernateValidator"/><property name="validationMessageSource" ref="messageSource"/>
</bean><bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">

然后在Controller类中方法的参数增加@Valid注解即可

@RequestMapping("/update")
public String update(@Valid  PersonBean person) {//TODO  ... 
}

总结

写到这里,上面提到的validator框架用法基本能满足我们大多数业务场景了,我是最近在为公司写业务代码过程中对各种繁琐的校验头痛不已,前期都是直接用ifelse搞定,后面觉得干体力活没意思,因此通过validator框架把公司代码现有校验逻辑重构了一遍,非常受用,重构时比较痛苦,但是后面再使用就非常轻松了,上面这些场景都是我真实用到的,因此在这里总结一下做个笔记。

所有代码都在如下仓库: github-validator












http://chatgpt.dhexx.cn/article/A5J2vG8K.shtml

相关文章

java使用validator进行校验

不管是html页面表单提交的对象数据还是和第三方公司进行接口对接&#xff0c;都需要对接收到的数据进行校验&#xff08;非空、长度、格式等等&#xff09;。如果使用if一个个进行校验&#xff08;字段非常多&#xff09;&#xff0c;这是让人崩溃的过程。幸好jdk或hibernate都…

java validator_Spring中校验器(Validator)的深入讲解

前言 Spring框架的 validator 组件,是个辅助组件,在进行数据的完整性和有效性非常有用,通过定义一个某个验证器,即可在其它需要的地方,使用即可,非常通用。 应用在执行业务逻辑之前,必须通过校验保证接受到的输入数据是合法正确的,但很多时候同样的校验出现了多次,在不…

springboot使用hibernate validator校验

目录 一、参数校验二、hibernate validator校验demo三、hibernate的校验模式 1、普通模式&#xff08;默认是这个模式&#xff09;2、快速失败返回模式四、hibernate的两种校验 1、请求参数校验2、GET参数校验(RequestParam参数校验)3、model校验4、对象级联校验5、分组校验五…

Validator 使用总结

介绍 首先说下大家常用的hibernate-validator&#xff0c;它是对JSR-303/JSR-349标准的实现&#xff0c;然后spring为了给开发者提供便捷集成了hibernate-validator&#xff0c;默认在springmvc模块。 依赖 本文所介绍皆在springboot应用的基础上&#xff0c;首先加上web模块…

浅谈 Android Tombstone(墓碑日志)分析步骤

最近项目产品刚刚出货&#xff0c;客户退机、死机事件频发。日常解决bug中&#xff0c;少不了和墓碑日志打交道&#xff0c;截止今天之前&#xff0c;见到墓碑日志都是一脸懵逼&#xff0c;不知道怎么分析。最近又有了两个日志&#xff0c;硬着头皮看吧。之所以称之为浅谈&…

Android tombstone文件是如何生成的

本节内容我们聚焦到androidQ上&#xff0c;分析android中一个用于debug的功能&#xff0c;那就是tombstone&#xff0c;俗称“墓碑”。现实生活中墓碑一般是给死人准备的&#xff0c;而在android系统中“墓碑”则是给进程准备的。 为何Android要设计出这样一个东西呢&#xff…

【Android NDK 开发】NDK C/C++ 代码崩溃调试 - Tombstone 报错信息日志文件分析 ( 获取 tombstone_0X 崩溃日志信息 )

文章目录 一、崩溃信息描述二、手机命令行操作三、电脑命令行操作四、Tombstone 内容 Tombstone 报错信息日志文件被保存在了 /data/tombstones/ 目录下 , 先 ROOT 再说 , 没有 ROOT 权限无法访问该目录中的信息 ; 使用 Pixel 2 手机进行调试 , 其它 ROOT 后的手机也可以使用 …

Android tombstone 分析案例

Android tombstone 分析案例 tombstone文件内容1. 体系结构2. 发生Crash线程3. 原因4. 寄存器状态4.1 处理器工作模式下的寄存器4.2 未分组寄存器r0 – r74.3 分组寄存器r8 – r144.4 程序计数器pc(r15)4.5 程序状态寄存器4.6 ARM参数规则 5. 回溯栈6. 程序栈7. 寄存器地址附近…

RocksDB Tombstone 详解

目录 为什么会有墓碑&#xff1f; 使用场景 原理 描述 分段 查询 优化点 总结 为什么会有墓碑&#xff1f; 我们知道 TP 数据库一般选择 KV 引擎作为存储引擎&#xff0c;数据库的元数据和数据通过一定的编码规则变成 KV 对存储在存储引擎中&#xff0c;比如 CockroachD…

Tombstone 文件分析

Tombstone 文件分析 /* * 下面信息是dropbox负责添加的 **/ isPrevious: true Build: Rock/odin/odin:7.1.1/NMF26F/1500868195:user/dev-keys Hardware: msm8953 Revision: 0 Bootloader: unknown Radio: unknown Kernel: Linux version 3.18.31-perf-g34cb3d1 (smartcmhardc…

android Tombstone 流程

一 总述 下面是一份dump 的log&#xff1a; 810 876 I system_server: libdebuggerd_client: started dumping process 678 740 740 I /system/bin/tombstoned: registered intercept for pid 678 and type kDebuggerdNativeBacktrace 678 678 I libc : Requested du…

android tombstone log分析

今天和大家一起聊聊android 中出现的 Tombstone问题&#xff0c;近期在定制pad 上分析设备概率性重启&#xff0c;导出bugreport日志后&#xff0c;除了看到anr log外&#xff0c;同级目录下还看到了tombstones 并且对比以往日志&#xff0c;发现都生产了大量tombstone...,于是…

深入学习tombstone和signal

三驾马车&#xff08;CPU&#xff0c;内存和存储设备&#xff09;中&#xff0c;跑得最慢的就是存储设备了 电脑上&#xff0c;从HDD 到SSD&#xff0c;从SATA SSD到PCIe SSD&#xff0c;硬盘是越来越快&#xff1b; 手机上&#xff0c;从SD卡&#xff0c;到eMMC卡&#xff0…

tombstone

1.什么是tombstone 当一个动态库&#xff08;native 程序&#xff09;开始执行时&#xff0c;系统会注册一些连接到 debuggerd 的 signal handlers&#xff0c;当系统 crash 的时候&#xff0c;会保存一个 tombstone 文件到/data/tombstones目录下&#xff08;Logcat中也会有相…

Tombstone原理分析

本文主要围绕三个问题对tombstone进行分析和介绍&#xff0c;debuggerd是如何监控进程并生成tombstone的&#xff1f;tombstone文件中的信息都是什么&#xff0c;是怎么获取的&#xff1f;tombstone文件应该怎么分析&#xff1f; 一、Tombstone简介 当一个native程序开始执行时…

【date】Linux date命令修改时间的问题

Linux date命令修改时间的问题 问题路径找原因解决方法 问题 Android10&#xff1b;高通平台 使用下面date命令修改时间日期&#xff0c;时分秒生效&#xff0c;年月日不生效 > date -D YYYY-MM-DD hh:mm:ss 路径 \android\external\toybox\toys\posix\date.c \android\e…

i2ctools工具移植到android(使用NDK方式 在某android平台测试)

前提条件 主板i2c已在设备树配置status和引脚复用正常&#xff0c;即设备的i2c总线达到正常使用条件I2C device interface假设内核已配置进去 编译工具链NDK环境搭建 下载NDK 下载地址点我解压 ~/workspace/ndk$ ls android-ndk-r22b android-ndk-r22b-linux-x86_64.zip …

高通平台 Android9 adb shell “hwclock -w“ 报错

hwclock -w 报错 文章目录 hwclock -w 报错问题现象分析1. hwclock命令分析2. /dev/rtc0驱动节点分析 修改设备树后hwclock -w报错没有了&#xff0c;但是系统会重启&#xff0c;原因未知 问题现象 sdm660_64:/ # hwclock -w hwclock: ioctl 4024700a: Invalid argument分析 …

Android top命令、ps命令、busybox命令

top命令 usage: top [-Hbq] [-k FIELD,] [-o FIELD,] [-s SORT] [-n NUMBER] [-m LINES] [-d SECONDS] [-p PID,] [-u USER,]Show process activity in real time.-H Show threads -k Fallback sort FIELDS (default -S,-%CPU,-ETIME,-PID) -o Show FIELDS (def PID,USER,PR,N…

OpenHarmony啃论文俱乐部—盘点开源鸿蒙引用的三方开源软件[1]

目录这里写自定义目录标题 OpenHarmony third_party三方库&#xff1a;学术研究和参与开源的结合third_party_openh264third_party_ninjathird_party_gnthird_party_markupsafethird_party_toyboxthird_party_gstreamerthird_party_ffmpegthird_party_mtdevthird_party_flutter…