Hibernate Validator实战篇

在写程序的时候经常需要进行数据校验,比如服务端对http请求参数校验,数据入库时对字段长度进行校验,接口参数校验,可以说数据校验遍布应用程序代码中,就像下图所示:


Hibernate Validator实战篇_第1张图片
application-layers.png

为了减少代码重复率,开发人员将数据校验的逻辑直接加载数据模型中。JSR380定义了一套bean校验的元数据模型,将数据的约束定义在数据模型中。

Hibernate Validator实战篇_第2张图片
Paste_Image.png

而hibernate validator则实现了这样一套规范。
下面我们来看看如何使用hibernate进行bean校验已经方法参数校验。

准备

下载hibernate validator的依赖包,这里全部使用maven管理依赖。


    org.hibernate.validator
    hibernate-validator
    6.0.1.Final


    org.glassfish
    javax.el
    3.0.1-b08

校验bean的属性

  1. 先定义一个需要校验的类,这里采用hibernate validator官方的例子
public class Car {
   @NotNull @Size(min = 2, max = 14)
   private String manufacturer;
   public Car(String manufacturer) {
      this.manufacturer = manufacturer;
   }
   public void drive(@Max(50) int speedInMph) {
     // ...
   }
  // getter setter
}
  1. 获取Validator
  ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
                           .configure()
                           .buildValidatorFactory();
   Validator validator = factory.getValidator();
  1. 使用Validator校验Car
  Car car = new Car(null);
  Set> violations = validator.validate(car);
  assertEquals(1, violations.size());

对于bean的校验首先需要确定要校验哪些类,在这些类的属性添加各种约束(比如@NotNull),通过java.validation.Validation获取Validator,通过validator校验对象,获取校验的结果,其校验结果都是返回一个包含ConstraintViolation对象的集合。

校验方法中的参数

比如要校验Car中drive方法中的参数。

  1. 首先获取ExecutableValidator
ExecutableValidator executableValidator = validator.forExecutables();

通过之前获取的Validator得到ExecutableValidater.

  1. 通过ExecutableValidator校验drive方法中的参数
Car object = new Car("Morris");
Method method = RacingCar.class.getMethod( "drive", int.class );
Object[] parameterValues = { 90 };
Set violations = executableValidator.validateParameters(
                object,
                method,
                parameterValues
        );
// assertEquals(1, violations.size() );

通过这个例子能够最bean的属性和方法进行简单的校验,但是我们经常遇到需要校验的场景有:

  1. 对象的级联校验;
    比如Car中引用一个对象Driver,在校验Car的同时也需要校验Driver中的属性。
class Car {
  @NotNull
   private String manufacturer;
   private Driver driver;
}
  1. 对象中关联参数联合校验;
    比如Car有两个属性,座位数和乘客数,要求乘客数量不能大于座位数。
class Car {
  @Max(20)
  private int seatCount;
  // passengers.size() <= seatCount
  private List passengers;
}
  1. 方法中参数关联校验;
    比如Car对象的方法buildCar的签名如下:
public Car buildCar(int seatCount, List passengers) {
  // ...
  return null;
}

要求乘客数量小于座位数。

  1. 默认情况下,validator会对被校验对象的所有属性进行校验,能否只校验一部分?
  2. 如何自定义约束呢?
  3. 如何自定义校验结果中的message呢?
    下面一起来回答前面提到的几个问题。

级联校验

通过在属性上添加@Valid注解就可以进行级联校验了。如下:

class Car {
  @NotNull
  private String manufacturer;
  @Valid
  private Driver driver;

  // getter setter
}

这样在校验Car的时候,也同时会校验Driver中的属性。

自定义约束

1.首先定义一个约束,约束是一个注解形式,相当于定义个注解,我们需要确定这个注解是用于属性、类还是方法上等,其次约束注解需要提供几个固定的方法,最后确定这个约束需要的自定义方法。例如要验证汽车的载客人数不能超过座位数的约束。

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
//必须添加下面这个注解,实际校验的时候将使用指定的Validator进行校验
@Constraint(validateBy = {ValidPassengerCount.Validator.class}) 
public @interface ValidPassengerCount {
// 固定需要添加的方法
  String message() default 'validatePassengerCount.message';
// 固定需要添加的方法
Class[] groups() default {};
// 固定需要添加的方法
Class payload() default {};

class Validator implements ConstraintValidator {

        @Override
        public void initialize(ValidPassengerCount constraintAnnotation) {

        }

        @Override
        public boolean isValid(Car value, ConstraintValidatorContext context) {
            if (value.getPassengers().size() > value.getPassengers().size()) {
                return false;
            }
            return true;
        }
    }

上面自定义的约束没有添加自定义的业务属性,但可以添加任何自定义的方法,然后在Validator的initialize方法中,通过ValidatorPassengerCount获取自定义方法放回的结果保存在Validator中,然后在isValid方法使用;
对于isValid方法的返回类型是boolean型,校验通过返回true,校验失败返回false。
2.在需要约束的类定义中添加自定义的约束,已Car为例,如下:

@ValidPassengerCount
class Car {
  @Min(4)
  private int seatCount;
  @NotNull
  private List passengers;
  // getter setter
}

约束分组(GROUP)

约束分组用来实现部分校验的功能,例如我们在Car的fields上添加了较多约束,但是在有些场景中我们只需要验证car的部分属性,虽然这种场景的使用应不多,但我们如何实现这种功能呢?
通过前面的例子我们可以看到,在每一个约束中都包含一个groups的属性,返回class数组,Validator的validate方法也提供一个输入groups的参数,我想大家都明白groups是怎么用的了,对,我们就是可以使用groups实现之校验该跟分组的约束。示例如下:

public class Driver  {

@NotNull
public String name;

    @Min(
            value = 18,
            message = "You have to be 18 to drive a car",
            groups = DriverChecks.class
    )
    public int age;

    @AssertTrue(
            message = "You first have to pass the driving test",
            groups = DriverChecks.class
    )
    public boolean hasDrivingLicense;

    public Driver(String name) {
       this.name = name;
    }

    public void passedDrivingTest(boolean b) {
        hasDrivingLicense = b;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
public interface DriverChecks {
}

上面的示例代码中使用了两种group,DEFAULT和DriverChecks,Driver.name属于DEFAULT 组,Driver.age和Driver.hasDrivingLicense属于DriverChecks分组,如果只想校验Driver.name,只需要参照如下示例:

validator.validate(driver, DEFAULT.class);

如果只想校验Driver.age和Driver.hasDrivingLicense,参考如下示例:

validator.validate(driver, DriverChecks.class);

如果想同时Driver.name、Driver.age和Driver.hasDrivingLicense,参考如下示例:

validator.validate(driver, DEFAULT.class, DriverChecks.class);

校验的顺序与group的先后顺序一致。

自定义message

自定义message可以通过多种方式来实现。

  1. 在属性或者方法参数上添加约束注解时,可以在约束的message属性上设置自定义的message。如下:
class Car {
  @NotNull("car.menufacturer can't be null")
  private String manufacturer;
}
  1. 通过在构建Validator实例的过程中,配置ValidatorFactory时,设置一个MessageInterpolater,如下:
validator = Validation.byProvider(HibernateValidator.class)
                .configure()
                .messageInterpolator(new ResourceBundleMessageInterpolator(new PlatformResourceBundleLocator("message/validate_message")))
                .buildValidatorFactory()
                .getValidator();

然后在classpath的message目录下添加validate_message.properties文件,将要翻译的信息添加到该文件中:

image.png

这个是hibernate-validator jar中ValidationMessages.properties文件中的内容,默认key的规则都是以定义的约束的class的全限量名加".message"组成。

  1. 自定义约束设置message
    自定义约束时,在约束的message属性上设置default值,如下:
String message() default "{javax.validation.constraints.NotNull.message}";
  1. 在指定约束的校验器中,覆盖默认message,如下:
constraintContext.disableDefaultConstraintViolation();           
constraintContext.buildConstraintViolationWithTemplate( "{com.mycompany.constraints.CheckCase.message}"  ).addConstraintViolation();       
 }

hibernate-validator还包含一些其他的特性,就不细说了。下面我们看看如何将hibernate-validator应用到实际的开发工作中去。

应用例子

对于使用springMvc框架的,要在Controller中校验方法参数只需要在参数上注解@Valid和一些约束注解就可以了。
在使用一些rpc通讯框架时,一般这些rpc框架都不会集成一些参数校验的组件,需要我们自己写,这个时候我们就可以采用hibernate-validator组件了,这个相比自己去写校验组件真是快多了。
一般采用rpc做服务实现时,在服务实现的第一层,我们通过配置aop的方式对服务实现类进行代理,在代理中添加校验的逻辑。如下:

  1. 写一个通过hibernate-validator进行校验的类,如下:
public class HibernateValidateService implements ValidateService {

    private static HibernateValidateService INSTANCE;

    private static ExecutableValidator validator;

    static {
        validator = Validation.byProvider(HibernateValidator.class)
                .configure()
                .failFast(true)
                .ignoreXmlConfiguration()
                .parameterNameProvider(new ParanamerParameterNameProvider())
                .messageInterpolator(new ResourceBundleMessageInterpolator(new PlatformResourceBundleLocator("message/validate_message")))
                .buildValidatorFactory()
                .getValidator()
                .forExecutables();
    }


    public  void validate(T obj, Method method, Object[] parameters) throws OspException {
        Set> violations = validator.validateParameters(obj, method, parameters);
        if (!violations.isEmpty()) {
            ConstraintViolation violation = violations.iterator().next();
            throw new IllegalArgumentException(buildErrorMsg(violation));
        }
    }

    private  String buildErrorMsg(ConstraintViolation violation) {
        Iterator propertyNodes = violation.getPropertyPath().iterator();
        // skip method name
        propertyNodes.next();

        StringBuilder sb = new StringBuilder();
        while (propertyNodes.hasNext()) {
            sb.append(propertyNodes.next().getName());
            if (propertyNodes.hasNext()) {
                sb.append(".");
            } else {
                sb.append(" ");
            }
        }
        return sb.append(violation.getMessage()).toString();
    }

    public static ValidateService getInstance() {
        if (INSTANCE == null) {
            synchronized (HibernateValidateService.class) {
                if (INSTANCE == null) {
                    INSTANCE = new HibernateValidateService();
                }
            }
        }
        return INSTANCE;
    }

}
  1. 编写aop advice
public class ValidateBeforeAdvice implements MethodBeforeAdvice {
    private ValidateService validateService = HibernateValidateService.getInstance();
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        validateService.validate(target, method, args);
    }
}
  1. 配置服务实现层(api层)代理
 

        
        
    

这里有个注意点就是api层抛出了什么类型的异常,在写校验的时候也应该抛出什么类型的异常。

你可能感兴趣的:(Hibernate Validator实战篇)