要致富,先修路。要使用,先...基础是须要垒砌的,作技术切勿空中楼阁
【小家Java】深刻了解数据校验:Java Bean Validation 2.0(JSR30三、JSR34九、JSR380)Hibernate-Validation 6.x使用案例
【小家Java】深刻了解数据校验(Bean Validation):基础类打点(ValidationProvider、ConstraintDescriptor、ConstraintValidator)java
<center>对Spring感兴趣可扫码加入wx群:Java高工、架构师3群
(文末有二维码)</center>程序员
浩浩荡荡的把通常程序员都不太关注的Bean Validation
话题讲了这么久,期间小伙伴wx我说一直还没看到他最想看到的内容,我问最想看到啥?他说显然是数据校验在Spring
中的使用啊。我想若不出意外,这应该是众多小伙伴的共同心声吧,但路漫漫其修远兮,也得上下求索,本文将切入到最关心的Spring中来~spring
要想深刻了解Spring
对Bean Validation
的支持,org.springframework.validation.beanvalidation
这个包里面的这几个关键API必须搞明白喽,这样再使用起@Valid
结合Spring
时时才能更加的收放自如~编程
说明:这个包所在的jar是
spring-context
,属于Spring上下文的核心功能模块
我把这个包内的类图截图以下,供以参考:安全
Spring
虽然没有直接实现Bean校验这块的JSR
规范,可是从Spring3.0
开始,Spring就提供了对Bean Validation
的支持。架构
方法级别
的校验它就是个普通的BeanPostProcessor
。它可以去校验Spring容器中的Bean,从而决定允不容许它初始化完成。app
好比咱们有些Bean某些字段是不容许为空的,好比数据的连接,用户名密码等等,这个时候用上它处理就很是的优雅和高级了~
若校验不经过,在违反约束的状况下就会抛出异常,阻止容器的正常启动~ide
public class BeanValidationPostProcessor implements BeanPostProcessor, InitializingBean { // 这就是咱们熟悉的校验器 // 请注意这里是javax.validation.Validator,而不是org.springframework.validation.Validator @Nullable private Validator validator; // true:表示在Bean初始化以后完成校验 // false:表示在Bean初始化以前就校验 private boolean afterInitialization = false; ... // 省略get/set // 因而可知使用的是默认的校验器(固然仍是Hibernate的) @Override public void afterPropertiesSet() { if (this.validator == null) { this.validator = Validation.buildDefaultValidatorFactory().getValidator(); } } // 这个实现太简单了~~~ @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (!this.afterInitialization) { doValidate(bean); } return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (this.afterInitialization) { doValidate(bean); } return bean; } protected void doValidate(Object bean) { Assert.state(this.validator != null, "No Validator set"); Object objectToValidate = AopProxyUtils.getSingletonTarget(bean); if (objectToValidate == null) { objectToValidate = bean; } Set<ConstraintViolation<Object>> result = this.validator.validate(objectToValidate); // 拼接错误消息最终抛出 if (!result.isEmpty()) { StringBuilder sb = new StringBuilder("Bean state is invalid: "); for (Iterator<ConstraintViolation<Object>> it = result.iterator(); it.hasNext();) { ConstraintViolation<Object> violation = it.next(); sb.append(violation.getPropertyPath()).append(" - ").append(violation.getMessage()); if (it.hasNext()) { sb.append("; "); } } throw new BeanInitializationException(sb.toString()); } } }
这个BeanValidationPostProcessor
实现的功能确实很是的简单,无非就是对全部的Bean在初始化前/后
进行校验。
咱们如果对Spring Bean
想作约束的话(好比对属性、构造器等等),使用它就很是的方便~工具
备注:
BeanValidationPostProcessor
默承认是没有被装配进容器的~
应用程序特定对象的验证器,这是Spring本身的抽象,注意区别于javax.validation.Validator
。这个接口彻底脱离了任何基础设施或上下文
,也就是说,它没有耦合到只验证Web层、数据访问层或任何层中的对象。它支持应用于程序内的任何层post
// 注意:它可不是Spring3后才推出的 最初就有 public interface Validator { // 此clazz是否能够被validate boolean supports(Class<?> clazz); // 执行校验,错误消息放在Errors 装着 // 能够参考ValidationUtils这个工具类,它能帮助你不少 void validate(Object target, Errors errors); }
它的继承树以下:
这个子接口它扩展增长了校验分组:hints。
// @since 3.1 这个出现得比较晚 public interface SmartValidator extends Validator { // 注意:这里的Hints最终都会被转化到JSR的分组里去~~ // 因此这个可变参数,传接口Class对象便可~ void validate(Object target, Errors errors, Object... validationHints); // @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
:校验适配器(重要)这个实现类Class是很是重要的,它是javax.validation.Validator
到Spring的Validator
的适配,经过它就能够对接到JSR的校验器来完成校验工做了~
在Spring5.0后,此实现类已完美支持到
Bean Validation 2.0
// @since 3.0 public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator { // 通用的三个约束注解都须要有的属性 private static final Set<String> internalAnnotationAttributes = new HashSet<>(4); static { internalAnnotationAttributes.add("message"); internalAnnotationAttributes.add("groups"); internalAnnotationAttributes.add("payload"); } // 最终都是委托给它来完成校验的~~~ @Nullable private javax.validation.Validator targetValidator; public SpringValidatorAdapter(javax.validation.Validator targetValidator) { Assert.notNull(targetValidator, "Target Validator must not be null"); this.targetValidator = targetValidator; } // 简单的说:默认支持校验全部的Bean类型~~~ @Override public boolean supports(Class<?> clazz) { return (this.targetValidator != null); } // processConstraintViolations作的事一句话解释: // 把ConstraintViolations错误消息,全都适配放在Errors(BindingResult)里面存储着 @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); } } // 把validationHints都转换为group (支识别Class类型哦) 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); } // 关于Implementation of JSR-303 Validator interface 省略... }
这个适配器它把全部的Spring接口的校验方法,最终都委托给了org.springframework.validation.Validator
,这样就能够完美的和JSR结合起来使用了,功能更加的强大~
虽然本类它是个Class实体类,可是通常来讲不建议直接使用它
可配置(Custom)的Bean类,也一样的实现了双接口
。它能够配置ValidatorFactory
验证器工厂、MessageInterpolator
插值器等...
public class CustomValidatorBean extends SpringValidatorAdapter implements Validator, InitializingBean { // javax.validation.ValidatorFactory @Nullable private ValidatorFactory validatorFactory; @Nullable private MessageInterpolator messageInterpolator; @Nullable private TraversableResolver traversableResolver; ... // 省略全部set方法(木有get方法) // 默认设置~~~~初始化 @Override public void afterPropertiesSet() { if (this.validatorFactory == null) { this.validatorFactory = Validation.buildDefaultValidatorFactory(); } // 这一句就是new ValidatorContextImpl( this ) 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); } // 把已经配置好的这个Validator设置进去~ setTargetValidator(validatorContext.getValidator()); } }
命名中就能能够看出,它是一个Bean,因此能够配合Spring容器一块儿使用。Spring
内部虽然没有直接使用到它,但咱们本身有需求的话本身可使用它(其实更多的仍是使用更强的子类)~
它和CustomValidatorBean
平级,都是继承自SpringValidatorAdapter
,可是它提供的能力更加的强大,好比Spring
处理校验这块最重要的处理器MethodValidationPostProcessor
就是依赖于它来给提供验证器~
它是Spring
上下文中javax.validation
的中心配置类。
// @since 3.0 这个类很是的丰富 实现了接口javax.validation.ValidatorFactory // 实现了ApplicationContextAware拿到Spring上下文... // 但其实,它的实际工做都是委托式,本身只提供了各式各样的配置~~~(主要是配置JSR) public class LocalValidatorFactoryBean extends SpringValidatorAdapter implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean { ... // 省略全部的配置属性 ... // 省略全部的get/set ... // 省略afterPropertiesSet()进行的默认配置初始化 最终调用setTargetValidator(this.validatorFactory.getValidator()); // 备注:还记得上文吗?上文的validator校验器是从上下文拿的,这里是从工厂拿的 // 省略全部对ValidatorFactory接口的方法实现~ }
这个类是很是重要的,虽然它也不被Spring直接使用,可是它是基石。
备注:虽然命名后缀是FactoryBean
,但它并非org.springframework.beans.factory.FactoryBean
这个接口的子类。
其实这是断句问题,正确断句方式是:LocalValidatorFactory
Bean~
@since 4.0.1
提供的,它作的惟一一件事:让org.springframework.validation.Validator
成为可选(即便没有初始化成功,也不会报错,至关于把异常吃了嘛~)
// @since 4.0.1 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); } } }
综上,若你想使用org.springframework.validation.SmartValidator
来完成对Bean的校验,那就手动定义一个这样的Bean,而后自行调用API校验完成校验~
若你想这一切能面向注解编程,自动完成校验,那就听下文分解吧(也是最为关心,最为重要的内容)~
ConstraintValidatorFactory
整个API前问有讲过,本类就是Spring
对它的扩展,从而和Spring容器整合了~
public class SpringConstraintValidatorFactory implements ConstraintValidatorFactory { private final AutowireCapableBeanFactory beanFactory; public SpringConstraintValidatorFactory(AutowireCapableBeanFactory beanFactory) { Assert.notNull(beanFactory, "BeanFactory must not be null"); this.beanFactory = beanFactory; } // 注意:此处是直接调用了create方法,放进容器 @Override public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) { return this.beanFactory.createBean(key); } // Bean Validation 1.1 releaseInstance method public void releaseInstance(ConstraintValidator<?, ?> instance) { this.beanFactory.destroyBean(instance); } }
MessageSourceResourceBundleLocator
这个类也很是有意思,它扩展了Hibernate
包的ResourceBundleLocator
国际化,而使用
Spring本身的国际化资源:org.springframework.context.MessageSource
说明:ResourceBundleLocator
是它Hibernate
的一个SPI,Hibernate
内部本身对它但是也有实现的哦~(Bean Validation
内部大量的用到了SPI技术,有兴趣的能够了解)
public class MessageSourceResourceBundleLocator implements ResourceBundleLocator { private final MessageSource messageSource; public MessageSourceResourceBundleLocator(MessageSource messageSource) { Assert.notNull(messageSource, "MessageSource must not be null"); this.messageSource = messageSource; } @Override public ResourceBundle getResourceBundle(Locale locale) { return new MessageSourceResourceBundle(this.messageSource, locale); } }
关于MessageSourceResourceBundle
它,就相对比较熟悉点了,它不是校验专用的,是Spring总体上用来处理国际化资源:MessageSource
,java.util.ResourceBundl
的帮助类~
//@since 27.02.2003 java.util.ResourceBundle 它是JDK提供来读取国际化的属性配置文件的 是个抽象类 public class MessageSourceResourceBundle extends ResourceBundle { private final MessageSource messageSource; private final Locale locale; public MessageSourceResourceBundle(MessageSource source, Locale locale) { Assert.notNull(source, "MessageSource must not be null"); this.messageSource = source; this.locale = locale; } public MessageSourceResourceBundle(MessageSource source, Locale locale, ResourceBundle parent) { this(source, locale); setParent(parent); } @Override @Nullable protected Object handleGetObject(String key) { try { return this.messageSource.getMessage(key, null, this.locale); } catch (NoSuchMessageException ex) { return null; } } // @since 1.6 @Override public boolean containsKey(String key) { try { this.messageSource.getMessage(key, null, this.locale); return true; } catch (NoSuchMessageException ex) { return false; } } @Override public Enumeration<String> getKeys() { throw new UnsupportedOperationException("MessageSourceResourceBundle does not support enumerating its keys"); } @Override public Locale getLocale() { return this.locale; } }
Spring
环境下不只可使用Hibernate
的国际化文件,也能够借助MessageSourceResourceBundleLocator
搞本身的。
它是个javax.validation.MessageInterpolator
插值器,Spring把它和本身的LocaleContext
结合起来了~
// @since 3.0 // org.springframework.context.i18n.LocaleContextHolder#getLocale() public class LocaleContextMessageInterpolator implements MessageInterpolator { private final MessageInterpolator targetInterpolator; public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator) { Assert.notNull(targetInterpolator, "Target MessageInterpolator must not be null"); this.targetInterpolator = targetInterpolator; } @Override public String interpolate(String message, Context context) { return this.targetInterpolator.interpolate(message, context, LocaleContextHolder.getLocale()); } @Override public String interpolate(String message, Context context, Locale locale) { return this.targetInterpolator.interpolate(message, context, locale); } }
想来想去,仍是给个Demo很是简单的操做一把吧,此处我以CustomValidatorBean
为例对Bean进行校验:
@Getter @Setter @ToString public class Person { // 错误消息message是能够自定义的 @NotNull(message = "{message} -> 名字不能为null", groups = Simple.class) public String name; @Max(value = 10, groups = Simple.class) @Positive(groups = Default.class) // 内置的分组:default public Integer age; @NotNull(groups = Complex.class) @NotEmpty(groups = Complex.class) private List<@Email String> emails; @Future(groups = Complex.class) private Date start; // 定义两个组 Simple组和Complex组 public interface Simple { } public interface Complex { } }
想容器放入一个校验器:
@Configuration public class RootConfig { @Bean public CustomValidatorBean customValidatorBean() { return new CustomValidatorBean(); } }
使用此校验器校验Person对象(本文为了简单就直接new了哈,固然你也能够是容器内的Bean对象)
@Slf4j @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class}) public class TestSpringBean { @Autowired private SmartValidator smartValidator; @Test public void test1() { Person person = new Person(); person.setAge(-1); person.setStart(new Date()); Errors errors = new DirectFieldBindingResult(person, "person"); ValidationUtils.invokeValidator(smartValidator, person, errors, Person.Complex.class); System.out.println(errors); } }
打印输出:
org.springframework.validation.DirectFieldBindingResult: 3 errors Field error in object 'person' on field 'emails': rejected value [null]; codes [NotEmpty.person.emails,NotEmpty.emails,NotEmpty.java.util.List,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.emails,emails]; arguments []; default message [emails]]; default message [不能为空] Field error in object 'person' on field 'start': rejected value [Fri Jul 26 11:12:21 CST 2019]; codes [Future.person.start,Future.start,Future.java.util.Date,Future]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.start,start]; arguments []; default message [start]]; default message [须要是一个未来的时间] Field error in object 'person' on field 'emails': rejected value [null]; codes [NotNull.person.emails,NotNull.emails,NotNull.java.util.List,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.emails,emails]; arguments []; default message [emails]]; default message [不能为null]
符合预期。
说明:由于前面说了Bean Validation
内的校验类大都是线程安全的,包括校验器javax.validation.Validator
也是线程安全的~
从这篇文章开始,关于Bean Validation
这块就切入进Spring的应用里了。本文主要描述的是一些支持类,咱们了解了它能够经过手动完成对Spring Bean的校验,可是在实际应用中显然不会这么去作,毕竟一切都须要崇尚自动化嘛~
==下一篇,也就是整个Bean Validation
的主菜,也就是真正在企业级·Spring·应用中使用的校验方式分析,也就是你们熟悉的@Valid,@Validated
以及级联属性的校验问题,欢迎点赞关注~==
若文章格式混乱,可点击
:
原文连接-原文连接-原文连接-原文连接-原文连接
==The last:若是以为本文对你有帮助,不妨点个赞呗。固然分享到你的朋友圈让更多小伙伴看到也是被做者本人许可的~
==
**若对技术内容感兴趣能够加入wx群交流:Java高工、架构师3群
。
若群二维码失效,请加wx号:fsx641385712
(或者扫描下方wx二维码)。而且备注:"java入群"
字样,会手动邀请入群**