共计 14017 个字符,预计需要花费 36 分钟才能阅读完成。
每篇一句
要致富,先修路。要使用,先 … 基础是需要垒砌的,做技术切勿空中楼阁
相关阅读
【小家 Java】深入了解数据校验:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x 使用案例
【小家 Java】深入了解数据校验(Bean Validation):基础类打点(ValidationProvider、ConstraintDescriptor、ConstraintValidator)
<center> 对 Spring 感兴趣可扫码加入 wx 群:Java 高工、架构师 3 群
(文末有二维码)</center>
前言
浩浩荡荡的把一般程序员都不太关注的 Bean Validation
话题讲了这么久,期间小伙伴 wx 我说一直还没看到他 最想看到 的内容,我问最想看到啥?他说显然是 数据校验在 Spring
中的使用 啊。我想若不出意外,这应该是众多小伙伴的共同心声吧,但路漫漫其修远兮,也得上下求索,本文将切入到最关心的 Spring 中来~
要想深入了解 Spring
对Bean Validation
的支持,org.springframework.validation.beanvalidation
这个包里面的这几个关键 API 必须搞明白喽,这样再使用起 @Valid
结合 Spring
时时才能更加的收放自如~
说明:这个包所在的 jar 是
spring-context
,属于 Spring 上下文的核心功能模块
我把这个包内的类图截图如下,供以参考:
Spring
虽然没有直接实现 Bean 校验这块的 JSR
规范,但是从 Spring3.0
开始,Spring 就提供了对 Bean Validation
的支持。
- 3.0 提供了 Bean 级别的校验
- 3.1 提供了更加强大的
方法级别
的校验
BeanValidationPostProcessor
它就是个普通的BeanPostProcessor
。它能够去校验 Spring 容器中的 Bean,从而决定允不允许它初始化完成。
比如我们有些 Bean 某些字段是不允许为空的,比如数据的链接,用户名密码等等,这个时候用上它处理就非常的优雅和高级了~
若校验不通过,在违反约束的情况下就会抛出异常,阻止容器的正常启动~
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
默认可是没有被装配进容器的~
==org.springframework.validation.Validator==
应用程序特定对象的验证器,这是 Spring 自己的抽象,注意区别于 javax.validation.Validator
。 这个接口完全脱离了任何基础设施或上下文
,也就是说,它没有耦合到只验证 Web 层、数据访问层或任何层中的对象。 它支持应用于程序内的任何层
// 注意:它可不是 Spring3 后才推出的 最初就有
public interface Validator {
// 此 clazz 是否可以被 validate
boolean supports(Class<?> clazz);
// 执行校验,错误消息放在 Errors 装着
// 可以参考 ValidationUtils 这个工具类,它能帮助你很多
void validate(Object target, Errors errors);
}
它的继承树如下:
SmartValidator
这个子接口它扩展增加了校验 分组: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 实体类,但是一般来说不建议直接使用它
CustomValidatorBean
可配置(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
内部虽然没有直接使用到它,但我们自己有需求的话自己可以使用它(其实更多的还是使用更强的子类)~
==LocalValidatorFactoryBean==
它和 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~
OptionalValidatorFactoryBean
@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 校验完成校验~
若你想这一切能面向注解编程,自动完成校验,那就听下文分解吧(也是最为关心,最为重要的内容)~
SpringConstraintValidatorFactory
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
搞自己的。
LocaleContextMessageInterpolator
它是个 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 Show
想来想去,还是给个 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 入群"
字样,会手动邀请入群 **