Java Hibernate validator 校验框架


#Java 笔记


官方网址:http://hibernate.org/validator/

基础示例

若使用 gradle 构建 Java 项目,请在 build.gradle 中增加以下依赖:

compile group: 'org.hibernate.validator', name: 'hibernate-validator', version: '6.0.11.Final'
compile group: 'org.glassfish', name: 'javax.el', version: '3.0.1-b09'
compile group: 'org.projectlombok', name: 'lombok', version: '1.18.0'  // 不是必须,用来减少 Java Bean 的模板代码
compile group: 'junit', name: 'junit', version: '4.12'  // 正常应该用 testCompile,这里用 compile 编写示例更方便

示例1

import lombok.Data;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

@Data
public class Person {

    @NotNull
    private String name;

    @Min(1)
    private int age;
}

如果name是null,hibernate validator 校验器会识别到,并给出错误信息。 如果age小于1,hibernate validator 校验器会识别到,并给出错误信息。

测试:

import org.junit.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

public class PersonTest {

    @Test
    public void test01() {
        // 生成校验器
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator(); 

        Person person = new Person();

        // 校验 person
        Set<ConstraintViolation<Person>> constraintViolations = validator.validate( person );

        // 输出校验结果
        System.out.println("错误数量: " + constraintViolations.size());
        for (ConstraintViolation<Person> v: constraintViolations) {
            System.out.println(v.getMessage());
        }
    }

    @Test
    public void test02() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Person person = new Person();
        person.setName("letian");
        person.setAge(18);

        Set<ConstraintViolation<Person>> constraintViolations = validator.validate( person );

        System.out.println("错误数量: " + constraintViolations.size());

        for (ConstraintViolation<Person> v: constraintViolations) {
            System.out.println(v.getMessage());
        }
    }

}

执行 test01,输出:

错误数量: 2
最小不能小于1
不能为null

执行 test02,输出:

错误数量: 0

示例2:自定义错误消息

import lombok.Data;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

@Data
public class Person {

    @NotNull(message = "name不能为null")
    private String name;

    @Min(value = 1, message = "age不能小于1")
    private int age;
}
import org.junit.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

public class PersonTest {

    @Test
    public void test01() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        Person person = new Person();
        Set<ConstraintViolation<Person>> constraintViolations = validator.validate( person );

        System.out.println("错误数量: " + constraintViolations.size());
        for (ConstraintViolation<Person> v: constraintViolations) {
            System.out.println(v.getMessage());
        }
    }

}

执行结果如下:

错误数量: 2
name不能为null
age不能小于1

示例3:不要使用原始类型,使用包装类型

原始类型的默认值(比如int的0)可能有业务含义,而包装类型的默认值(例如Integer的null)可以避免这个问题。

我们将上面示例中的 int 改成 Integer:

import lombok.Data;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class Person {

    @NotBlank(message = "name不能为空")
    private String name;

    @Min(value = 1, message = "age不能小于1")
    @NotNull(message = "age不能为null")
    private Integer age;
}
import org.junit.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

public class PersonTest {

    @Test
    public void test01() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Person person = new Person();
        person.setName("  ");
        person.setAge(0);

        Set<ConstraintViolation<Person>> constraintViolations = validator.validate( person );

        System.out.println("错误数量: " + constraintViolations.size());

        for (ConstraintViolation<Person> v: constraintViolations) {
            System.out.println(v.getMessage());
        }
    }

}

执行结果:

错误数量: 2
age不能小于1
name不能为空

示例4:级联校验

如果 Java Bean 的一个属性是另外一个 Java Bean,如果要同时校验该 Java Bean,就是级联校验。

import lombok.Data;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;

@Data
public class Address implements Serializable {

    @NotBlank(message = "city 不能为空")
    private String city;

}
import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;

@Data
public class Person implements Serializable {

    @NotBlank(message = "name 不能为空")
    private String name;

    @Valid // 这个注解很重要
    @NotNull(message = "address 不能为空")
    private Address address;
}
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

public class PersonTest {

    @Test
    public void test01() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Person person = new Person();
        person.setName("letian");

        Set<ConstraintViolation<Person>> constraintViolations = validator.validate( person );

        System.out.println("错误数量: " + constraintViolations.size());

        for (ConstraintViolation<Person> v: constraintViolations) {
            System.out.println(v.getMessage());
        }
    }

    @Test
    public void test02() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Person person = new Person();
        person.setName("letian");
        person.setAddress(new Address());

        Set<ConstraintViolation<Person>> constraintViolations = validator.validate( person );

        System.out.println("错误数量: " + constraintViolations.size());

        for (ConstraintViolation<Person> v: constraintViolations) {
            System.out.println(v.getMessage());
        }
    }

}

不管有没有@Vaild注解 Person 的 address 属性,test01 的结果都是:

错误数量: 1
address 不能为空

如果没有@Valid注解 Person 的 address 属性,test02 的结果是:

错误数量: 0

如果有@Valid注解 Person 的 address 属性,test02 的结果是:

错误数量: 1
city 不能为空

示例5:校验分组

@NotNull@NotBlank 的默认分组是是javax.validation.groups.Default接口。

validator.validate( person ); 校验 person 对象中属于 Default 分组的@NotNull@NotBlank等注解的属性。

我们也可以自定义分组:

  1. 定义接口,代表分组
  2. @NotNull等注解加上分组
  3. 校验时指定使用哪些分组。
public interface NameCheckGroup {
}
import lombok.Data;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;

@Data
public class Person {

    @NotBlank(message = "name不能为空", groups = { NameCheckGroup.class })
    private String name;

    @Min(value = 1, message = "age不能小于1")
    private int age;
}
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.groups.Default;
import java.util.Set;

public class PersonTest {

    // 校验默认分组的数据
    @Test
    public void test01() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        Person person = new Person();
        person.setName("  ");
        person.setAge(0);

        Set<ConstraintViolation<Person>> constraintViolations = validator.validate( person );

        System.out.println("错误数量: " + constraintViolations.size());

        for (ConstraintViolation<Person> v: constraintViolations) {
            System.out.println(v.getMessage());
        }
    }

    // 校验 NameCheckGroup 分组的数据
    @Test
    public void test02() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        Person person = new Person();
        person.setName("  ");
        person.setAge(0);

        Set<ConstraintViolation<Person>> constraintViolations = validator.validate( person, NameCheckGroup.class);

        System.out.println("错误数量: " + constraintViolations.size());

        for (ConstraintViolation<Person> v: constraintViolations) {
            System.out.println(v.getMessage());
        }
    }

    // 校验 NameCheckGroup 分组和默认分组的数据
    @Test
    public void test03() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        Person person = new Person();
        person.setName("  ");
        person.setAge(0);

        Set<ConstraintViolation<Person>> constraintViolations = validator.validate( person, NameCheckGroup.class, Default.class);

        System.out.println("错误数量: " + constraintViolations.size());

        for (ConstraintViolation<Person> v: constraintViolations) {
            System.out.println(v.getMessage());
        }
    }

}

这三个测试中测试的 person 对象内容都是相同的,但校验时使用的分组不同。

test01 执行结果:

错误数量: 1
age不能小于1

test02 执行结果:

错误数量: 1
name不能为空

test03 执行结果:

错误数量: 2
name不能为空
age不能小于1

注解说明

@NotNull

注解的元素不能为 null。

@NotBlank

注解的属性是 CharSequence 类型(String 继承自 CharSequence,也可以),必须包含非空白字符。 这意味着,也不是 null 。

@NotEmpty

注解的元素不能是 null,也不能是空。

元素类型是 CharSequence 、Array,则长度不能是 0。 元素类型是 Collection、Map,则size不能是0。

@Min

注解的元素的值必须大于等于指定的最小值。

支持的类型: BigDecimal、BigInteger、byte、Byte、short、Short、long、Long、int、Integer。

@Max

注解的元素的值必须小于等于指定的最大值。

支持的类型: BigDecimal、BigInteger、byte、Byte、short、Short、long、Long、int、Integer。

@Size

注解的元素大小必须在指定的范围内。

支持的类型:

  • CharSequence : 使用 length 代表大小
  • Array : 使用 length 代表大小
  • Collection: 使用 size 代表大小
  • Map: 使用 size 代表大小

更多注解

还有很多注解,比如 @Negative、@Email、 @DecimalMax 等,这些都在一个 package 中定义,翻看代码即可。

解决 javax.validation.ValidationException: HV000183: Unable to load 'javax.el.ExpressionFactory'

在上面的示例中,校验器是用下面的方式初始化的:

// 生成校验器
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator(); 

若无 org.glassfish 依赖,那么会报错 javax.validation.ValidationException: HV000183: Unable to load 'javax.el.ExpressionFactory'。

如果要在无 org.glassfish 依赖的情况下不报错,可以用下面的方式生成校验器:

import javax.validation.Validation;
import javax.validation.Validator;
import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;

private static final Validator validator =
  Validation.byDefaultProvider()
    .configure()
    .messageInterpolator(new ParameterMessageInterpolator())
    .buildValidatorFactory()
    .getValidator();

具体讨论可参考 https://stackoverflow.com/questions/24386771/javax-validation-validationexception-hv000183-unable-to-load-javax-el-express

一个通用的工具类

import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;

public class ValidatorUtil {

     private final static Validator validator = Validation.byDefaultProvider()
             .configure()
             .messageInterpolator(new ParameterMessageInterpolator())
             .buildValidatorFactory()
             .getValidator();

    /**
     * 对象内部的字段校验
     *
     * @param obj
     * @param <T>
     */
     public static <T> void validate(T obj) {
         if (obj == null) {
             throw new RuntimeException("不能为null");
         }
         Set<ConstraintViolation<T>> constraintViolations = validator.validate(obj);
         for(ConstraintViolation<T> item : constraintViolations) {
             throw new RuntimeException(item.getMessage());
         }
     }

}



( 本文完 )