每篇一句

要致富,先修路。要使用,先...基础是需要垒砌的,做技术切勿空中楼阁

相关阅读

【小家Java】深入了解数据校验:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例

【小家Java】深入了解数据校验(Bean Validation):基础类打点(ValidationProvider、ConstraintDescriptor、ConstraintValidator)


对Spring感兴趣可扫码加入wx群:`Java高工、架构师3群`(文末有二维码)


前言

浩浩荡荡的把一般程序员都不太关注的Bean Validation话题讲了这么久,期间小伙伴wx我说一直还没看到他最想看到的内容,我问最想看到啥?他说显然是数据校验在Spring中的使用啊。我想若不出意外,这应该是众多小伙伴的共同心声吧,但路漫漫其修远兮,也得上下求索,本文将切入到最关心的Spring中来~

要想深入了解SpringBean Validation的支持,org.springframework.validation.beanvalidation这个包里面的这几个关键API必须搞明白喽,这样再使用起@Valid结合Spring时时才能更加的收放自如~

说明:这个包所在的jar是spring-context,属于Spring上下文的核心功能模块

我把这个包内的类图截图如下,供以参考:

Spring虽然没有直接实现Bean校验这块的JSR规范,但是从Spring3.0开始,Spring就提供了对Bean Validation的支持。

  1. 3.0提供了Bean级别的校验
  2. 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这个接口的子类。

其实这是断句问题,正确断句方式是:Local ValidatorFactory 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整体上用来处理国际化资源:MessageSourcejava.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入群" 字样,会手动邀请入群

详述Spring对数据校验支持的核心API:SmartValidator的更多相关文章

  1. spring mvc 数据校验(bean实体注解实现)

    spring mvc 数据校验 1.添加个jar (jar与一版本会冲突) <dependency> <groupId>com.fasterxml</groupId> ...

  2. Spring MVC 数据校验@Valid

    先看看几个关键词 @Valid @Pattern @NotNull @NotBlank @Size BindingResult 这些就是Spring MVC的数据校验的几个注解. 那怎么用呢?往下看 ...

  3. JSR教程2——Spring MVC数据校验与国际化

    SpringMVC数据校验采用JSR-303校验. • Spring4.0拥有自己独立的数据校验框架,同时支持JSR303标准的校验框架. • Spring在进行数据绑定时,可同时调用校验框架完成数据 ...

  4. Spring MVC数据校验

    在web应用程序中,为了防止客户端传来的数据引发程序异常,常常需要对 数据进行验证.输入验证分为客户端验证与服务器端验证.客户端验证主要通过JavaScript脚本进行,而服务器端验证则主要通过Jav ...

  5. spring mvc 数据校验

    1.需要导入的jar包: slf4j-api-1.7.21.jar validation-api-1.0.0.GA.jar hibernate-validator-4.0.1.GA.jar 2.访问页 ...

  6. 【代码总结】Spring MVC数据校验

    1.实验介绍 --------------------------------------------------------------------------------------------- ...

  7. Spring方法级别数据校验:@Validated + MethodValidationPostProcessor

    每篇一句 在<深度工作>中作者提出这么一个公式:高质量产出=时间*专注度.所以高质量的产出不是靠时间熬出来的,而是效率为王 相关阅读 [小家Java]深入了解数据校验:Java Bean ...

  8. Spring官网阅读(十七)Spring中的数据校验

    文章目录 Java中的数据校验 Bean Validation(JSR 380) 使用示例 Spring对Bean Validation的支持 Spring中的Validator 接口定义 UML类图 ...

  9. SpringMVC 数据转换 & 数据格式化 & 数据校验

    数据绑定流程 1. Spring MVC 主框架将 ServletRequest 对象及目标方法的入参实例传递给 WebDataBinderFactory 实例,以创建 DataBinder 实例对象 ...

随机推荐

  1. Python连载8-datetime包函数介绍

    一.datetime包(上接连载7内容) 1.函数:datetime (1)用法:输入一个日期,来返回一个datetime类​ (2)格式:datetime.datetime(年,月,日,hour=, ...

  2. You can't specify target table 'tbl_students' for update in FROM clause错误

    此问题只出现在mysql中 oracle中无此问题 在同一语句中,当你在select某表的数据后,不能update这个表,如: DELETE FROM tbl_students WHERE id NO ...

  3. Centos7 fstab盘符挂载硬盘导致重启系统失败解决办法

    服务器拥有多个硬盘插槽,在进行维护或重启时,这些硬盘的相对位置可能发生变化.利用盘符(dev/vda)方式挂载磁盘,可能由于磁盘顺序变化导致重启时读取fstab文件发生错误,从而无法正常重启服务器. ...

  4. 音视频技术“塔尖”之争,网易云信如何C位出道?

    音视频技术“塔尖”之争,网易云信如何C位出道? 社交+美颜.抖音短视频.在线狼人杀.直播竞答.子弹短信……,过往两三年间,互联网新产品和新玩法层出不穷,风口不断切换.这些爆红的网络应用背后,都有一些共 ...

  5. java源码解析之String类(四)

    /* * 返回指定字符第一次出现的字符串内的索引 */ public int indexOf(int ch) { return indexOf(ch, 0); } /* * 返回指定字符第一次出现的字 ...

  6. Vue SSR初探

    因为之前用nuxt开发过应用程序,但是nuxt早就达到了开箱即用的目的,所以一直对vue ssr的具体实现存在好奇. 构建步骤 我们通过上图可以看到,vue ssr 也是离不开 webpack 的打包 ...

  7. 学习体验centos7 下根目录扩容操作(步骤详细!!!)

    转自  苗尼玛乔 感谢你!

  8. 总结关于CPU的一些基本知识

    关于CPU和程序的执行 CPU是计算机的大脑. 程序的运行过程,实际上是程序涉及到的.未涉及到的一大堆的指令的执行过程. 当程序要执行的部分被装载到内存后,CPU要从内存中取出指令,然后指令解码(以便 ...

  9. Linux搭建基于BIND的DNS服务器

    Linux搭建基于BIND的DNS服务器   实验目标: 通过本实验掌握基于Linux的DNS服务器搭建. 实验步骤: 1.安装BIND 2.防火墙放通DNS服务 3.编辑BIND的主配置文件 4.编 ...

  10. IPv6 优于 IPv4 的十大功能

    现在是 9102 年,有一个严重的问题,困扰着资深宅男二狗子.那就是偶像团体没新名了.今年开始,偶像团体 XKB48 已经在无法取更多的新名字了,排列组合的所有方式都已经经过了历史长河的洗礼,除非偶像 ...