Hibernate Validator 总结大全

背景

代码开发过程中,参数的有效性校验是一项很繁琐的工作, 如果参数简单,就那么几个参数,直接通过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;

    @Email
    private String email;
  
    /*
    真实场景下面可能还有几十个字段
    省略 ... ...
    */
  
}

校验测试

public class ValidateTest {
    //初始化一个校验器工厂  
    private static ValidatorFactory validatorFactory = Validation
            .byProvider(HibernateValidator.class)
            .configure()
            //校验失败是否立即返回: true-遇到一个错误立即返回不在往下校验,false-校验完所有字段才返回
            .failFast(false)
            .buildValidatorFactory();
    Validator validator = validatorFactory.getValidator();

    /**
     * 简单对象校验
     */
    @Test
    public 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 {
    @NotNull
    private 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,测试是否会校验元素为null
    list.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> {
    @Override
    public 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(
    //这个配置用于绑定校验器:EnumRangeValidator
    validatedBy = {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;
    
    @Override
    public 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);
        }
    }

    @Override
    public 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);

            //禁用默认值,否则会有两条message
            constraintValidatorContext.disableDefaultConstraintViolation();

            //添加新的message
            constraintValidatorContext.
                    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)中的n
    int 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)
    @Documented
    public @interface List {
        ByteMaxLength[] value();
    }
}

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

public class ByteMaxLengthValidator implements ConstraintValidator<ByteMaxLength,String> {
    private int max;
    private Charset charset;

    @Override
    public void initialize(ByteMaxLength constraintAnnotation) {
        max=constraintAnnotation.max();
        charset=Charset.forName(constraintAnnotation.charset());
    }

    @Override
    public 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 和 enMax
            String message = constraintValidatorContext
                    .getDefaultConstraintMessageTemplate()
                    .replace("{chMax}",String.valueOf(chMax))
                    .replace("{enMax}",String.valueOf(max));

            //禁用默认值,否则会有两条message
            constraintValidatorContext.disableDefaultConstraintViolation();

            //添加新的message
            constraintValidatorContext.
                    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)
    @Documented
    public @interface List {
        OrderPrice[] value();
    }
}

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

public class OrderPriceValidator implements ConstraintValidator<OrderPrice, OrderBean> {

    @Override
    public void initialize(OrderPrice constraintAnnotation) {

    }

    @Override
    public 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











  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值