要致富,先修路。要使用,先...基礎是須要壘砌的,作技術切勿空中樓閣java
【小家Java】深刻了解數據校驗:Java Bean Validation 2.0(JSR30三、JSR34九、JSR380)Hibernate-Validation 6.x使用案例
【小家Java】深刻了解數據校驗(Bean Validation):基礎類打點(ValidationProvider、ConstraintDescriptor、ConstraintValidator)程序員
浩浩蕩蕩的把通常程序員都不太關注的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
的支持。app
方法級別
的校驗它就是個普通的BeanPostProcessor
。它可以去校驗Spring容器中的Bean,從而決定允不容許它初始化完成。ide
好比咱們有些Bean某些字段是不容許爲空的,好比數據的連接,用戶名密碼等等,這個時候用上它處理就很是的優雅和高級了~工具
若校驗不經過,在違反約束的狀況下就會拋出異常,阻止容器的正常啓動~post
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層、數據訪問層或任何層中的對象。它支持應用於程序內的任何層
// 注意:它可不是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入羣"
字樣,會手動邀請入羣