Java中的数据校验
在学习Java中的数据校验前,我们需要先了解一个概念,即什么是JSR?
JSR:全称Java Specification Requests,意思是Java 规范提案。我们可以将其理解为Java为一些功能指定的一系列统一的规范。跟数据校验相关的最新的JSR为JSR 380。
Bean Validation 2.0 是JSR第380号标准。该标准连接如下:https://www.jcp.org/en/egc/view?id=380
Bean Validation的主页:http://beanvalidation.org
Bean Validation的参考实现:https://github.com/hibernate/hibernate-validator
Bean Validation(JSR 380)
从官网中的截图我们可以看到,Bean Validation 2.0的唯一实现就是Hibernate Validator,对应版本为6.0.1.Final,同时在2.0版本之前还有1.1(JSR 349)及1.0(JSR 303)两个版本,不过版本间的差异并不是我们关注的重点,而且Bean Validation 2.0本身也向下做了兼容。
在上面的图中,可以看到Bean Validation2.0的全称为Jakarta Bean Validation2.0,关于Jakarta,感兴趣的可以参考这个链接:https://www.oschina.net/news/94055/jakarta-ee-new-logo,就是Java换了个名字。
使用示例
导入依赖:
<!--除了导入hibernate-validator外,还需要导入一个tomcat-embed-el包,用于提供EL表达式的功能 因为错误message是支持EL表达式计算的,所以需要导入此包 --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> <version>9.0.16</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.14.Final</version> <scope>compile</scope> </dependency> <!-- 如果你用的是一个SpringBoot项目只需要导入下面这个依赖即可 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> -->
测试Demo:
@Data public class Person { @NotEmpty private String name; @Positive @Max(value = 100) private int age; } public class SpringValidation { public static void main(String[] args) { Person person = new Person(); person.setAge(-1); Set<ConstraintViolation<Person>> result = Validation.buildDefaultValidatorFactory().getValidator().validate(person); // 对结果进行遍历输出 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()) .forEach(System.out::println); } // 运行结果: // name 不能为空: null // age 必须是正数: -1 }
对于其中涉及的细节目前来说我不打算过多的探讨,我们现在只需要知道Java提供了数据校验的规范,同时Hibernate对其有一套实现就可以了,并且我们也验证了使用其进行校验是可行的。那么接下来我们的问题就变成了Spring对Java的这套数据校验的规范做了什么支持呢?或者它又做了什么扩展呢?
Spring对Bean Validation的支持
我们先从官网入手,看看Spring中如何使用数据校验,我这里就直接取官网中的Demo了
@Data public class Person { private String name; private int age; } public class PersonValidator implements Validator { @Override public boolean supports(Class clazz) { return Person.class.equals(clazz); } @Override public void validate(Object obj, Errors e) { ValidationUtils.rejectIfEmpty(e, "name", "name.empty"); Person p = (Person) obj; if (p.getAge() < 0) { e.rejectValue("age", "negativevalue"); } else if (p.getAge() > 110) { e.rejectValue("age", "too.darn.old"); } } } public class Main { public static void main(String[] args) { Person person = new Person(); person.setAge(-1); DirectFieldBindingResult errorResult = new DirectFieldBindingResult(person, "dmz"); PersonValidator personValidator = new PersonValidator(); personValidator.validate(person, errorResult); System.out.println(errorResult); // 程序打印: //Field error in object 'dmz' on field 'name': rejected value [null]; codes //[name.empty.dmz.name,name.empty.name,name.empty.java.lang.String,name.empty]; arguments //[]; default message [null] //Field error in object 'dmz' on field 'age': rejected value [-1]; codes //[negativevalue.dmz.age,negativevalue.age,negativevalue.int,negativevalue]; arguments //[]; default message [null] } }
在上面的例子中,PersonValidator实现了一个Validator接口,这个接口是Spring自己提供的,全称:org.springframework.validation.Validator,我们看看这个接口的定义
Spring中的Validator
org.springframework.validation.Validator是专门用于应用相关的对象的校验器。
这个接口完全从基础设施或者上下文中脱离的,这意味着它没有跟web层或者数据访问层或者其余任何的某一个层次发生耦合。所以它能用于应用中的任意一个层次,能对应用中的任意一个对象进行校验。,
接口定义
public interface Validator { // 此clazz是否可以被validate boolean supports(Class<?> clazz); // 执行校验,错误消息放在Errors中 // 如果能执行校验,通常也意味着supports方法返回true // 可以参考ValidationUtils这个工具类 void validate(Object target, Errors errors); }
UML类图
SmartValidator
对Validator接口进行了增强,能进行分组校验
public interface SmartValidator extends Validator { // validationHints:就是启动的校验组 // target:需要校验的结果 // errors:封装校验 void validate(Object target, Errors errors, Object... validationHints); // 假设value将被绑定到指定对象中的指定字段上,并进行校验 // @since 5.1 这个方法子类需要复写 否则不能使用 default void validateValue(Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) { throw new IllegalArgumentException("Cannot validate individual value for " + targetType); } }
SpringValidatorAdapter
在之前的接口我们会发现,到目前为止Spring中的校验跟Bean Validation还没有产生任何交集,而SpringValidatorAdapter就完成了到Bean Validation的对接
// 可以看到,这个接口同时实现了Spring中的SmartValidator接口跟JSR中的Validator接口 public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator { //@NotEmpty,@NotNull等注解都会有这三个属性 private static final Set<String> internalAnnotationAttributes = new HashSet<>(4); static { internalAnnotationAttributes.add("message"); internalAnnotationAttributes.add("groups"); internalAnnotationAttributes.add("payload"); } // targetValidator就是实际完成校验的对象 @Nullable private javax.validation.Validator targetValidator; public SpringValidatorAdapter(javax.validation.Validator targetValidator) { Assert.notNull(targetValidator, "Target Validator must not be null"); this.targetValidator = targetValidator; } SpringValidatorAdapter() { } void setTargetValidator(javax.validation.Validator targetValidator) { this.targetValidator = targetValidator; } // 支持对所有类型的Bean的校验 @Override public boolean supports(Class<?> clazz) { return (this.targetValidator != null); } // 调用targetValidator完成校验,并通过processConstraintViolations方法封装校验后的结果到Errors中 @Override public void validate(Object target, Errors errors) { if (this.targetValidator != null) { processConstraintViolations(this.targetValidator.validate(target), errors); } } // 完成分组校验 @Override public void validate(Object target, Errors errors, Object... validationHints) { if (this.targetValidator != null) { processConstraintViolations( this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); } } // 完成对对象上某一个字段及给定值的校验 @SuppressWarnings("unchecked") @Override public void validateValue( Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) { if (this.targetValidator != null) { processConstraintViolations(this.targetValidator.validateValue( (Class) targetType, fieldName, value, asValidationGroups(validationHints)), errors); } } // @since 5.1 // 将validationHints转换成JSR中的分组 private Class<?>[] asValidationGroups(Object... validationHints) { Set<Class<?>> groups = new LinkedHashSet<>(4); for (Object hint : validationHints) { if (hint instanceof Class) { groups.add((Class<?>) hint); } } return ClassUtils.toClassArray(groups); } // 省略对校验错误的封装 // ..... // 省略对JSR中validator接口的实现,都是委托给targetValidator完成的 // ...... }
ValidatorAdapter
跟SpringValidatorAdapter同一级别的类,但是不同的是他没有实现JSR中的Validator接口。一般不会使用这个类
CustomValidatorBean
public class CustomValidatorBean extends SpringValidatorAdapter implements Validator, InitializingBean { // JSR中的接口,校验器工厂 @Nullable private ValidatorFactory validatorFactory; // JSR中的接口,用于封装校验信息 @Nullable private MessageInterpolator messageInterpolator; // JSR中的接口,用于判断属性能否被ValidatorProvider访问 @Nullable private TraversableResolver traversableResolver; // 忽略setter方法 // 在SpringValidatorAdapter的基础上实现了InitializingBean,在Bean初始化时调用,用于给上面三个属性进行配置 @Override public void afterPropertiesSet() { if (this.validatorFactory == null) { this.validatorFactory = Validation.buildDefaultValidatorFactory(); } ValidatorContext validatorContext = this.validatorFactory.usingContext(); MessageInterpolator targetInterpolator = this.messageInterpolator; if (targetInterpolator == null) { targetInterpolator = this.validatorFactory.getMessageInterpolator(); } validatorContext.messageInterpolator(new LocaleContextMessageInterpolator(targetInterpolator)); if (this.traversableResolver != null) { validatorContext.traversableResolver(this.traversableResolver); } setTargetValidator(validatorContext.getValidator()); } }
LocalValidatorFactoryBean
public class LocalValidatorFactoryBean extends SpringValidatorAdapter implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean { //...... }
可以看到,这个类额外实现了ValidatorFactory接口,所以通过它不仅能完成校验,还能获取一个校验器validator。
OptionalValidatorFactoryBean
public class OptionalValidatorFactoryBean extends LocalValidatorFactoryBean { @Override public void afterPropertiesSet() { try { super.afterPropertiesSet(); } catch (ValidationException ex) { LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex); } } }
继承了LocalValidatorFactoryBean,区别在于让校验器的初始化成为可选的,即使校验器没有初始化成功也不会报错。
使用示例
在对整个体系有一定了解之后,我们通过一个例子来体会下Spring中数据校验
public class Main { public static void main(String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); // 将CustomValidatorBean注册到容器中,主要是为了让它经过初始化阶段完成对校验器的配置 ac.register(CustomValidatorBean.class); // 刷新启动容器 ac.refresh(); // 获取到容器中的校验器 CustomValidatorBean cb = ac.getBean(CustomValidatorBean.class); // 校验simple组的校验 Person person = new Person(); DirectFieldBindingResult simpleDbr = new DirectFieldBindingResult(person, "person"); cb.validate(person, simpleDbr, Person.Simple.class); // 校验Complex组的校验 DirectFieldBindingResult complexDbr = new DirectFieldBindingResult(person, "person"); person.setStart(new Date()); cb.validate(person, complexDbr, Person.Complex.class); System.out.println(complexDbr); } }
运行结果我这里就不贴出来了,大家可以自行测试
到目前为止,我们所接触到的校验的内容跟实际使用还是有很大区别,我相信在绝大多数情况下大家都不会采用前文所采用的这种方式去完成校验,而是通过@Validated或者@Valid来完成校验。