详述Spring对数据校验支持的核心API:SmartValidator

每篇一句

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

相关阅读

【小家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中来~

要想深入了解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入群" 字样,会手动邀请入群

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容