摘要:和上標注的約束都會被執行注意如果子類覆蓋了父類的方法,那么子類和父類的約束都會被校驗。
每篇一句
沒有任何技術方案會是一種銀彈,任何東西都是有利弊的相關閱讀
【小家Java】深入了解數據校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例
【小家Spring】Spring方法級別數據校驗:@Validated + MethodValidationPostProcessor優雅的完成數據校驗動作
【小家Java】深入了解數據校驗(Bean Validation):從深處去掌握@Valid的作用(級聯校驗)以及常用約束注解的解釋說明
一般來說,對于web項目我們都有必要對請求參數進行校驗,有的前端使用JavaScript校驗,但是為了安全起見后端的校驗都是必須的。因此數據校驗不僅僅是在web下,在方方面面都是一個重要的點。前端校驗有它的JS校驗框架(比如我之前用的jQuery Validation Plugin),后端自然也少不了。
前面洋洋灑灑已經把數據校驗Bean Validation講了很多了,如果你已經運用在你的項目中,勢必將大大提高生產力吧,本文作為完結篇(不是總結篇)就不用再系統性的介紹Bean Validation他了,而是旨在介紹你在使用過程中不得不關心的周邊、細節~
如果說前面是用機,那么本文就有點玩機的意思~BV(Bean Validation)的使用范圍
本次再次強調了這一點(設計思想是我認為特別重要的存在):使用范圍。
Bean Validation并不局限于應用程序的某一層或者哪種編程模型, 它可以被用在任何一層, 除了web程序,也可以是像Swing這樣的富客戶端程序中(GUI編程)。
我抄了一副業界著名的圖給大家:
Bean Validation的目標是簡化Bean校驗,將以往重復的校驗邏輯進行抽象和標準化,形成統一API規范;
說到抽象統一API,它可不是亂來的,只有當你能最大程度的得到公有,這個動作才有意義,至少它一般都是與業務無關的。抽象能力是對程序員分級的最重要標準之一約束繼承
如果子類繼承自他的父類,除了校驗子類,同時還會校驗父類,這就是約束繼承(同樣適用于接口)。
// child和person上標注的約束都會被執行 public class Child extends Person { ... }
注意:如果子類覆蓋了父類的方法,那么子類和父類的約束都會被校驗。
約束級聯(級聯校驗)如果要驗證屬性關聯的對象,那么需要在屬性上添加@Valid注解,如果一個對象被校驗,那么它的所有的標注了@Valid的關聯對象都會被校驗,這些對象也可以是數組、集合、Map等,這時會驗證他們持有的所有元素。
Demo:
@Getter @Setter @ToString public class Person { @NotNull private String name; @NotNull @Positive private Integer age; @Valid @NotNull private InnerChild child; @Valid // 讓它校驗List里面所有的屬性 private ListchildList; @Getter @Setter @ToString public static class InnerChild { @NotNull private String name; @NotNull @Positive private Integer age; } }
校驗程序:
public static void main(String[] args) { Person person = new Person(); person.setName("fsx"); Person.InnerChild child = new Person.InnerChild(); child.setName("fsx-age"); child.setAge(-1); person.setChild(child); // 設置childList person.setChildList(new ArrayList(){{ Person.InnerChild innerChild = new Person.InnerChild(); innerChild.setName("innerChild1"); innerChild.setAge(-11); add(innerChild); innerChild = new Person.InnerChild(); innerChild.setName("innerChild2"); innerChild.setAge(-12); add(innerChild); }}); Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false) .buildValidatorFactory().getValidator(); Set > result = validator.validate(person); // 輸出錯誤消息 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()) .forEach(System.out::println); }
打印校驗失敗的消息:
age 不能為null: null childList[0].age 必須是正數: -11 child.age 必須是正數: -1 childList[1].age 必須是正數: -12約束失敗消息message自定義
每個約束定義中都包含有一個用于提示驗證結果的消息模版message,并且在聲明一個約束條件的時候,你可以通過這個約束注解中的message屬性來重寫默認的消息模版(這是自定義message最簡單的一種方式)。
如果在校驗的時候,這個約束條件沒有通過,那么你配置的MessageInterpolator插值器會被用來當成解析器來解析這個約束中定義的消息模版, 從而得到最終的驗證失敗提示信息。
默認使用的插值器是org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator,它借助org.hibernate.validator.spi.resourceloading.ResourceBundleLocator來獲取到國際化資源屬性文件從而填充模版內容~
資源解析器默認使用的實現是PlatformResourceBundleLocator,在配置Configuration初始化的時候默認被賦值:
private ConfigurationImpl() { this.validationBootstrapParameters = new ValidationBootstrapParameters(); // 默認的國際化資源文件加載器USER_VALIDATION_MESSAGES值為:ValidationMessages // 這個值就是資源文件的文件名~~~~ this.defaultResourceBundleLocator = new PlatformResourceBundleLocator( ResourceBundleMessageInterpolator.USER_VALIDATION_MESSAGES ); this.defaultTraversableResolver = TraversableResolvers.getDefault(); this.defaultConstraintValidatorFactory = new ConstraintValidatorFactoryImpl(); this.defaultParameterNameProvider = new DefaultParameterNameProvider(); this.defaultClockProvider = DefaultClockProvider.INSTANCE; }
這個解析器會嘗試解析模版中的占位符( 大括號括起來的字符串,形如這樣{xxx})。
它解析message的核心代碼如下(比如此處message模版是{javax.validation.constraints.NotNull.message}為例):
public abstract class AbstractMessageInterpolator implements MessageInterpolator { ... private String interpolateMessage(String message, Context context, Locale locale) throws MessageDescriptorFormatException { // 如果message消息木有占位符,那就直接返回 不再處理了~ // 這里自定義的優先級是最高的~~~ if ( message.indexOf( "{" ) < 0 ) { return replaceEscapedLiterals( message ); } // 調用resolveMessage方法處理message中的占位符和el表達式 if ( cachingEnabled ) { resolvedMessage = resolvedMessages.computeIfAbsent( new LocalizedMessage( message, locale ), lm -> resolveMessage( message, locale ) ); } else { resolvedMessage = resolveMessage( message, locale ); } ... } private String resolveMessage(String message, Locale locale) { String resolvedMessage = message; // 獲取資源ResourceBundle三部曲 ResourceBundle userResourceBundle = userResourceBundleLocator.getResourceBundle( locale ); ResourceBundle constraintContributorResourceBundle = contributorResourceBundleLocator.getResourceBundle( locale ); ResourceBundle defaultResourceBundle = defaultResourceBundleLocator.getResourceBundle( locale ); ... } }
對如上message的處理步驟大致總結如下:
若沒占位符符號{需要處理,直接返回(比如我們自定義message屬性值全是文字,就直接返回了)~
有占位符或者EL,交給resolveMessage()方法從資源文件里拿內容來處理~
拿取資源文件,按照如下三個步驟尋找:
1. `userResourceBundleLocator`:去用戶自己的`classpath`里面去找資源文件(默認名字是`ValidationMessages.properties`,當然你也可以使用國際化名) 2. `contributorResourceBundleLocator`:加載貢獻的資源包 3. `defaultResourceBundle`:默認的策略。去這里`于/org/hibernate/validator`加載`ValidationMessages.properties`
需要注意的是,如上是加載資源的順序。無論怎么樣,這三處的資源文件都會加載進內存的(并無短路邏輯)。進行占位符匹配的時候,依舊遵守這規律:
1. 最先用自己當前項目`classpath`下的資源去匹配資源占位符,若沒匹配上再用下一級別的資源~~~ 2. 規律同上,依次類推,遞歸的匹配所有的占位符(若占位符沒匹配上,原樣輸出,并不是輸出`null`哦~)
需要注意的是,因為{在此處是特殊字符,若你就想輸出{,請轉義:{
了解了這些之后,想自定義失敗消息message,就簡直不要太簡單了好不好,例子如下:
@Min(value = 10, message = "{com.fsx.my.min.message}") private Integer age;
寫一個資源屬性文件,命名為ValidationMessages.properties放在類路徑下,文件內容如下:
// 此處可以使用占位符{value}讀取注解對應屬性上的值 com.fsx.my.min.message=[自定義消息]最小值必須是{value}
運行測試用例,打印輸出如下失敗消息:
age [自定義消息]最小值必須是10: -1
完美(自定義的生效了)
說明:因為我的平臺是中文的,因此文件命名為ValidationMessages_zh_CN.properties的效果也是一樣的,因為Hibernate Validation提供了Locale國際化的支持
上面使用的是Hibernate Validation內置的對國際化的支持,由于大部分情況下我們都是在Spring環境下使用數據校驗,因此有必要講講Spring加持情況下的國家化做法。我們知道Spring MVC是有專門做國際化的模塊的,因此國際化這個動作當然也是可以交給Spring自己來做的,此處我也給一個Demo吧:
說明:即使在Spring環境下,你照常使用Hibernate Validation的國際化方案,依舊是沒有問題的~
1、向容器內配置驗證器(含有自己的國際化資源文件):
@Configuration public class RootConfig { @Bean public LocalValidatorFactoryBean localValidatorFactoryBean() { LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean(); // 使用Spring加載國際化資源文件 //ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); //messageSource.setBasename("MyValidationMsg"); ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasename("MyValidationMsg"); // 注意此處名字就隨意啦,畢竟交給spring了`.properties`就不需要了哦 messageSource.setCacheSeconds(120); // 緩存時長 // messageSource.setFileEncodings(); // 設置編碼 UTF-8 localValidatorFactoryBean.setValidationMessageSource(messageSource); return localValidatorFactoryBean; } }
運行單測:
@Slf4j @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class}) public class TestSpringBean { @Autowired private LocalValidatorFactoryBean localValidatorFactoryBean; @Test public void test1() { Person person = new Person(); person.setAge(-5); Validator validator = localValidatorFactoryBean.getValidator(); Set> result = validator.validate(person); // 輸出錯誤消息 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()) .forEach(System.out::println); } }
打印校驗失敗消息如下(完美生效):
age [自定義消息]最小值必須是10: -5
說明:若是Spring應用,如果你還需要考慮國際化的話,我個人建議使用Spring來處理國際化,而不是Hibernate~(有種Spring的腦殘粉感覺有木有,當然這不是強制的)
Spring MVC中如何自定義全局校驗器ValidatorSpring MVC默認配置的(使用的)校驗器的執行代碼如下:
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware { ... @Bean public Validator mvcValidator() { Validator validator = getValidator(); if (validator == null) { if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) { Class> clazz; try { String className = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean"; clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader()); } catch (ClassNotFoundException | LinkageError ex) { throw new BeanInitializationException("Failed to resolve default validator class", ex); } validator = (Validator) BeanUtils.instantiateClass(clazz); } else { validator = new NoOpValidator(); } } return validator; } ... }
代碼很簡答,就不逐行解釋了。我歸納如下:
Spring MVC中校驗要想自動生效,必須導入了javax.validation.Validator才行,否則是new NoOpValidator()它木有校驗行為
Spring MVC最終默認使用的校驗器是OptionalValidatorFactoryBean(LocalValidatorFactoryBean的子類)~
顯然,要想校驗生效@EnableWebMvc也是必須的(SpringBoot環境另說)
那如何自定義一個全局的校驗器呢?最佳做法如下:
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { ... @Override public Validator getValidator() { // return "global" validator return new LocalValidatorFactoryBean(); } ... }
當然,你還可以使用@InitBinder來設置,甚至可以細粒度設置到只與當前Controller綁定的校驗器都是可行的(比如你可以使用自定校驗器實現各種私有的、比較復雜的邏輯判斷)
==自定義約束==JSR和Hibernate支持的約束條件已經足夠強大,應該是能滿足我們絕大部分情況下的基礎驗證的。如果還是不能滿足業務需求,我們還可以自定義約束,也很簡單一事。
JSR和Hibernate提供的約束注解解釋說明:【小家Java】深入了解數據校驗(Bean Validation):從深處去掌握@Valid的作用(級聯校驗)以及常用約束注解的解釋說明
自定義一個約束分如下三步(說是2步也成):
自定義一個約束注解
實現一個校驗器(實現接口:ConstraintValidator)
定義默認的校驗錯誤信息
給個Demo:此處以自定義一個約束注解來校驗集合的長度范圍:@CollectionRange
1、自定義注解(此處使用得比較高級)
@Documented @Constraint(validatedBy = {}) @SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT) @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Repeatable(value = CollectionRange.List.class) @Size // 校驗動作委托給Size去完成 所以它自己并不需要校驗器~~~ @ReportAsSingleViolation // 組合組件一般建議標注上 public @interface CollectionRange { // 三個必備的基本屬性 String message() default "{com.fsx.my.collection.message}"; Class>[] groups() default {}; Class extends Payload>[] payload() default {}; // 自定義屬性 @OverridesAttribute這里有點方法覆蓋的意思~~~~~~ 子類屬性覆蓋父類的默認值嘛 @OverridesAttribute(constraint = Size.class, name = "min") int min() default 0; @OverridesAttribute(constraint = Size.class, name = "max") int max() default Integer.MAX_VALUE; // 重復注解 @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Documented public @interface List { CollectionRange[] value(); } }
2、實現一個校驗器
此例用不著(下面會有)
3、自定義錯誤消息
當然,你可以寫死在message屬性上,但是本處使用配置的方式來~
com.fsx.my.collection.message=[自定義消息]你的集合的長度必須介于{min}和{max}之間(包含邊界值)
運行案例:
@Getter @Setter @ToString public class Person { @CollectionRange(min = 5, max = 10) private Listnumbers; } // 測試用例 public static void main(String[] args) { Person person = new Person(); person.setNumbers(Arrays.asList(1, 2, 3)); Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false) .buildValidatorFactory().getValidator(); Set > result = validator.validate(person); // 輸出錯誤消息 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()) .forEach(System.out::println); }
輸出校驗信息如下(校驗成功):
numbers [自定義消息]你的集合的長度必須介于5和10之間(包含邊界值): [1, 2, 3]組合約束
這塊比較簡單,很多情況下一個字段是需要有多個約束(不為空且大于0)的。這個時候我們有兩種做法:
就在該屬性上標注多個注解即可(推薦)
自定義一個注解,把這些注解封裝起來,形成一個新的約束注解(使用場景相對較少)
自定義message消息==可使用的變量==我們知道約束的失敗消息message里是可以使用{}占位符來動態取值的,默認情況下能夠取到約束注解里的所有屬性值,并且也只能取到那些屬性的值。
but,有的時候為了友好展示,我們需要自定義message里可取的值怎么辦呢?下面給個例子,讓大家知道怎么自定義可使用占位符的參數(備注:需要基于自定義注解):
自定義一個性別約束注解:
@Documented @Retention(RUNTIME) @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Constraint(validatedBy = {GenderConstraintValidator.class}) public @interface Gender { // 三個必備的基本屬性 String message() default "{com.fsx.my.gender.message}"; Class>[] groups() default {}; Class extends Payload>[] payload() default {}; int gender() default 0; //0:男生 1:女生 }
配置的消息資源是:
com.fsx.my.gender.message=[自定義消息]此處只能允許性別為[{zhGenderValue}]的
很顯然,此處我們需要讀取zhGenderValue這個自定義的屬性值,并且希望它是中文。所以看看下面我實現的這個校驗器吧:
public class GenderConstraintValidator implements ConstraintValidator{ int genderValue; @Override public void initialize(Gender constraintAnnotation) { genderValue = constraintAnnotation.gender(); } @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { //添加參數 校驗失敗的時候可用 HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class); hibernateContext.addMessageParameter("zhGenderValue", genderValue == 0 ? "男" : "女"); // 友好展示 //hibernateContext.buildConstraintViolationWithTemplate("{zhGenderValue}").addConstraintViolation(); if (value == null) { return false; // null is not valid } return value == genderValue; } }
運行單測:
@Getter @Setter @ToString public class Person { @Gender(gender = 0) private Integer personGender; } public static void main(String[] args) { Person person = new Person(); person.setPersonGender(1); Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false) .buildValidatorFactory().getValidator(); Set> result = validator.validate(person); // 輸出錯誤消息 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()) .forEach(System.out::println); }
打印如下:
personGender [自定義消息]此處只能允許性別為[男]的: 1
完美(效果達到)
總結如果說前面文章是用機,那這篇可以稱作是玩機了。Bean Validation是java官方定義的bean驗證標準,現在最新的版本為2.x,hibernate validator作為其標準實現,對其進行了擴展,增加了多種約束,如果仍然不能滿足業務需求,我們還可以自定義約束。
數據校驗Bean Validation這一大塊的內容到此就告一段落了,希望講解的所有內容能給你實際工作中帶來幫助,祝好~
若文章格式混亂,可點擊:原文鏈接-原文鏈接-原文鏈接-原文鏈接-原文鏈接
==The last:如果覺得本文對你有幫助,不妨點個贊唄。當然分享到你的朋友圈讓更多小伙伴看到也是被作者本人許可的~==
**若對技術內容感興趣可以加入wx群交流:Java高工、架構師3群。
若群二維碼失效,請加wx號:fsx641385712(或者掃描下方wx二維碼)。并且備注:"java入群" 字樣,會手動邀請入群**
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/75746.html
摘要:如果說要使用數據校驗,我十分相信小伙伴們都能夠使用,但估計大都是有個前提的環境。具體使用可參考小家讓支持對平鋪參數執行數據校驗默認使用只能對進行校驗級聯校驗什么叫級聯校驗,其實就是帶校驗的成員里存在級聯對象時,也要對它完成校驗。 每篇一句 NBA里有兩大笑話:一是科比沒天賦,二是詹姆斯沒技術 相關閱讀 【小家Java】深入了解數據校驗:Java Bean Validation 2.0(...
摘要:可能有人認為數據校驗模塊并不是那么的重要,因為硬編碼都可以做。我以數據綁定為引子引出了數據校驗這一塊,是想表明它的重要性。關于數據校驗這塊,最新的是,也就是我們常說的。 每篇一句 吾皇一日不退役,爾等都是臣子 對Spring感興趣可掃碼加入wx群:Java高工、架構師3群(文末有二維碼) 前言 前幾篇文章在講Spring的數據綁定的時候,多次提到過數據校驗。可能有人認為數據校驗模塊...
摘要:就這樣借助相關約束注解,就非常簡單明了,語義清晰的優雅的完成了方法級別入參校驗返回值校驗的校驗。但倘若是返回值校驗執行了即使是失敗了,方法體也肯定被執行了只能哪些類型上提出這個細節的目的是約束注解并不是能用在所有類型上的。 每篇一句 在《深度工作》中作者提出這么一個公式:高質量產出=時間*專注度。所以高質量的產出不是靠時間熬出來的,而是效率為王 相關閱讀 【小家Java】深入了解數據校...
摘要:方案一借助對方法級別數據校驗的能力首先必須明確一點此能力屬于框架的,而部分框架。 每篇一句 在金字塔塔尖的是實踐,學而不思則罔,思而不學則殆(現在很多編程框架都只是教你碎片化的實踐) 相關閱讀 【小家Java】深入了解數據校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例【小家Spr...
摘要:畢竟永遠相信本文能給你帶來意想不到的收獲使用示例關于數據校驗這一塊在中的使用案例,我相信但凡有點經驗的程序員應該沒有不會使用的,并且還不乏熟練的選手。 每篇一句 NBA里有兩大笑話:一是科比沒天賦,二是詹姆斯沒技術 相關閱讀 【小家Java】深入了解數據校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validati...
閱讀 2181·2021-11-19 09:55
閱讀 2637·2021-11-11 16:55
閱讀 3175·2021-09-28 09:36
閱讀 1945·2021-09-22 16:05
閱讀 3271·2019-08-30 15:53
閱讀 1806·2019-08-30 15:44
閱讀 2899·2019-08-29 13:10
閱讀 1339·2019-08-29 12:30