在写程序的时候经常需要进行数据校验,比如服务端对http请求参数校验,数据入库时对字段长度进行校验,接口参数校验,可以说数据校验遍布应用程序代码中,就像下图所示:
application-layers.png
为了减少代码重复率,开发人员将数据校验的逻辑直接加载数据模型中。JSR380定义了一套bean校验的元数据模型,将数据的约束定义在数据模型中。
Paste_Image.png而hibernate validator则实现了这样一套规范。
下面我们来看看如何使用hibernate进行bean校验已经方法参数校验。
准备
下载hibernate validator的依赖包,这里全部使用maven管理依赖。
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.1-b08</version>
</dependency>
校验bean的属性
- 先定义一个需要校验的类,这里采用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
}
- 获取Validator
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
.buildValidatorFactory();
Validator validator = factory.getValidator();
- 使用Validator校验Car
Car car = new Car(null);
Set<ConstraintViolation<Car>> violations = validator.validate(car);
assertEquals(1, violations.size());
对于bean的校验首先需要确定要校验哪些类,在这些类的属性添加各种约束(比如@NotNull),通过java.validation.Validation获取Validator,通过validator校验对象,获取校验的结果,其校验结果都是返回一个包含ConstraintViolation对象的集合。
校验方法中的参数
比如要校验Car中drive方法中的参数。
- 首先获取ExecutableValidator
ExecutableValidator executableValidator = validator.forExecutables();
通过之前获取的Validator得到ExecutableValidater.
- 通过ExecutableValidator校验drive方法中的参数
Car object = new Car("Morris");
Method method = RacingCar.class.getMethod( "drive", int.class );
Object[] parameterValues = { 90 };
Set<ConstraintViolation> violations = executableValidator.validateParameters(
object,
method,
parameterValues
);
// assertEquals(1, violations.size() );
通过这个例子能够最bean的属性和方法进行简单的校验,但是我们经常遇到需要校验的场景有:
- 对象的级联校验;
比如Car中引用一个对象Driver,在校验Car的同时也需要校验Driver中的属性。
class Car {
@NotNull
private String manufacturer;
private Driver driver;
}
- 对象中关联参数联合校验;
比如Car有两个属性,座位数和乘客数,要求乘客数量不能大于座位数。
class Car {
@Max(20)
private int seatCount;
// passengers.size() <= seatCount
private List<Passenger> passengers;
}
- 方法中参数关联校验;
比如Car对象的方法buildCar的签名如下:
public Car buildCar(int seatCount, List<Passenger> passengers) {
// ...
return null;
}
要求乘客数量小于座位数。
- 默认情况下,validator会对被校验对象的所有属性进行校验,能否只校验一部分?
- 如何自定义约束呢?
- 如何自定义校验结果中的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<? extends Payload> payload() default {};
class Validator implements ConstraintValidator<ValidPassengerCount, Car> {
@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<Passenger> 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可以通过多种方式来实现。
- 在属性或者方法参数上添加约束注解时,可以在约束的message属性上设置自定义的message。如下:
class Car {
@NotNull("car.menufacturer can't be null")
private String manufacturer;
}
- 通过在构建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"组成。
- 自定义约束设置message
自定义约束时,在约束的message属性上设置default值,如下:
String message() default "{javax.validation.constraints.NotNull.message}";
- 在指定约束的校验器中,覆盖默认message,如下:
constraintContext.disableDefaultConstraintViolation();
constraintContext.buildConstraintViolationWithTemplate( "{com.mycompany.constraints.CheckCase.message}" ).addConstraintViolation();
}
hibernate-validator还包含一些其他的特性,就不细说了。下面我们看看如何将hibernate-validator应用到实际的开发工作中去。
应用例子
对于使用springMvc框架的,要在Controller中校验方法参数只需要在参数上注解@Valid和一些约束注解就可以了。
在使用一些rpc通讯框架时,一般这些rpc框架都不会集成一些参数校验的组件,需要我们自己写,这个时候我们就可以采用hibernate-validator组件了,这个相比自己去写校验组件真是快多了。
一般采用rpc做服务实现时,在服务实现的第一层,我们通过配置aop的方式对服务实现类进行代理,在代理中添加校验的逻辑。如下:
- 写一个通过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 <T> void validate(T obj, Method method, Object[] parameters) throws OspException {
Set<ConstraintViolation<T>> violations = validator.validateParameters(obj, method, parameters);
if (!violations.isEmpty()) {
ConstraintViolation<T> violation = violations.iterator().next();
throw new IllegalArgumentException(buildErrorMsg(violation));
}
}
private <T> String buildErrorMsg(ConstraintViolation<T> violation) {
Iterator<Path.Node> 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;
}
}
- 编写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);
}
}
- 配置服务实现层(api层)代理
<bean id="validateBeforeAdvice" class="advice.ValidateBeforeAdvice" />
<aop:config proxy-target-class="false">
<aop:pointcut id="pointcut" expression="execution(* api.validate..*(..))" />
<aop:advisor advice-ref="validateBeforeAdvice" pointcut-ref="pointcut" />
</aop:config>
这里有个注意点就是api层抛出了什么类型的异常,在写校验的时候也应该抛出什么类型的异常。
网友评论