摘要:就這樣借助相關約束注解,就非常簡單明了,語義清晰的優雅的完成了方法級別入參校驗返回值校驗的校驗。但倘若是返回值校驗執行了即使是失敗了,方法體也肯定被執行了只能哪些類型上提出這個細節的目的是約束注解并不是能用在所有類型上的。
每篇一句
在《深度工作》中作者提出這么一個公式:高質量產出=時間*專注度。所以高質量的產出不是靠時間熬出來的,而是效率為王相關閱讀
【小家Java】深入了解數據校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例
【小家Java】深入了解數據校驗(Bean Validation):基礎類打點(ValidationProvider、ConstraintDescriptor、ConstraintValidator)
【小家Spring】詳述Spring對Bean Validation支持的核心API:Validator、SmartValidator、LocalValidatorFactoryBean...
你在書寫業務邏輯的時候,是否會經常書寫大量的判空校驗。比如Service層或者Dao層的方法入參、入參對象、出參中你是否都有自己的一套校驗規則?比如有些字段必傳,有的非必傳;返回值中有些字段必須有值,有的非必須等等~
如上描述的校驗邏輯,窺探一下你的代碼,估摸里面有大量的if else吧。此部分邏輯簡單(因為和業務關系不大)卻看起來眼花繚亂(趕緊偷偷去喵一下你自己的代碼吧,哈哈)。在攻城主鍵變大的時候,你會發現會有大量的重復代碼出現,這部分就是你入職一個新公司的吐槽點之一:垃圾代碼。
若你追求干凈的代碼,甚至有代碼潔癖,如上眾多if else的重復無意義勞動無疑是你的痛點,那么本文應該能夠幫到你。
Bean Validation校驗其實是基于DDD思想設計的,我們雖然可以不完全的遵從這種思考方式編程,但是其優雅的優點還是可取的,本文將介紹Spring為此提供的解決方案~
在講解之前,首先就來體驗一把吧~
@Validated(Default.class) public interface HelloService { Object hello(@NotNull @Min(10) Integer id, @NotNull String name); } // 實現類如下 @Slf4j @Service public class HelloServiceImpl implements HelloService { @Override public Object hello(Integer id, String name) { return null; } }
向容器里注冊一個處理器:
@Configuration public class RootConfig { @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); } }
測試:
@Slf4j @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {RootConfig.class}) public class TestSpringBean { @Autowired private HelloService helloService; @Test public void test1() { System.out.println(helloService.getClass()); helloService.hello(1, null); } }
結果如圖:
完美的校驗住了方法入參。
注意此處的一個小細節:若你自己運行這個案例你得到的參數名稱可能是hello.args0等,而我此處是形參名。是因為我使用Java8的編譯參數:-parameters(此處說一點:若你的邏輯中強依賴于此參數,務必在你的maven中加入編譯插件并且配置好此編譯參數)
若需要校驗方法返回值,改寫如下:
@NotNull Object hello(Integer id); // 此種寫法效果同上 //@NotNull Object hello(Integer id);
運行:
javax.validation.ConstraintViolationException: hello.: 不能為null ...
校驗完成。就這樣借助Spring+JSR相關約束注解,就非常簡單明了,語義清晰的優雅的完成了方法級別(入參校驗、返回值校驗)的校驗。
校驗不通過的錯誤信息,再來個全局統一的異常處理,就能讓整個工程都能盡顯完美之勢。(錯誤消息可以從異常ConstraintViolationException的getConstraintViolations()方法里獲得的~)
它是Spring提供的來實現基于方法Method的JSR校驗的核心處理器~它能讓約束作用在方法入參、返回值上,如:
public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)
官方說明:方法里寫有JSR校驗注解要想其生效的話,要求類型級別上必須使用@Validated標注(還能指定驗證的Group)
另外提示一點:這個處理器同處理@Async的處理器AsyncAnnotationBeanPostProcessor非常相似,都是繼承自AbstractBeanFactoryAwareAdvisingPostProcessor 的,所以若有興趣再次也推薦@Async的分析博文,可以對比著觀看和記憶:【小家Spring】Spring異步處理@Async的使用以及原理、源碼分析(@EnableAsync)
// @since 3.1 public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean { // 備注:此處你標注@Valid是無用的~~~Spring可不提供識別 // 當然你也可以自定義注解(下面提供了set方法~~~) // 但是注意:若自定義注解的話,此注解只決定了是否要代理,并不能指定分組哦 so,沒啥事別給自己找麻煩吧 private Class extends Annotation> validatedAnnotationType = Validated.class; // 這個是javax.validation.Validator @Nullable private Validator validator; // 可以自定義生效的注解 public void setValidatedAnnotationType(Class extends Annotation> validatedAnnotationType) { Assert.notNull(validatedAnnotationType, ""validatedAnnotationType" must not be null"); this.validatedAnnotationType = validatedAnnotationType; } // 這個方法注意了:你可以自己傳入一個Validator,并且可以是定制化的LocalValidatorFactoryBean哦~(推薦) public void setValidator(Validator validator) { // 建議傳入LocalValidatorFactoryBean功能強大,從它里面生成一個驗證器出來靠譜 if (validator instanceof LocalValidatorFactoryBean) { this.validator = ((LocalValidatorFactoryBean) validator).getValidator(); } else if (validator instanceof SpringValidatorAdapter) { this.validator = validator.unwrap(Validator.class); } else { this.validator = validator; } } // 當然,你也可以簡單粗暴的直接提供一個ValidatorFactory即可~ public void setValidatorFactory(ValidatorFactory validatorFactory) { this.validator = validatorFactory.getValidator(); } // 毫無疑問,Pointcut使用AnnotationMatchingPointcut,并且支持內部類哦~ // 說明@Aysnc使用的也是AnnotationMatchingPointcut,只不過因為它支持標注在類上和方法上,所以最終是組合的ComposablePointcut // 至于Advice通知,此處一樣的是個`MethodValidationInterceptor`~~~~ @Override public void afterPropertiesSet() { Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); } // 這個advice就是給@Validation的類進行增強的~ 說明:子類可以覆蓋哦~ // @since 4.2 protected Advice createMethodValidationAdvice(@Nullable Validator validator) { return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); } }
它是個普通的BeanPostProcessor,為Bean創建的代理的時機是postProcessAfterInitialization(),也就是在Bean完成初始化后有必要的話用一個代理對象返回進而交給Spring容器管理~(同@Aysnc)
容易想到,關于校驗方面的邏輯不在于它,而在于切面的通知:MethodValidationInterceptor
它是AOP聯盟類型的通知,此處專門用于處理方法級別的數據校驗。
注意理解方法級別:方法級別的入參有可能是各種平鋪的參數、也可能是一個或者多個對象
// @since 3.1 因為它校驗Method 所以它使用的是javax.validation.executable.ExecutableValidator public class MethodValidationInterceptor implements MethodInterceptor { // javax.validation.Validator private final Validator validator; // 如果沒有指定校驗器,那使用的就是默認的校驗器 public MethodValidationInterceptor() { this(Validation.buildDefaultValidatorFactory()); } public MethodValidationInterceptor(ValidatorFactory validatorFactory) { this(validatorFactory.getValidator()); } public MethodValidationInterceptor(Validator validator) { this.validator = validator; } @Override @SuppressWarnings("unchecked") public Object invoke(MethodInvocation invocation) throws Throwable { // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton // 如果是FactoryBean.getObject() 方法 就不要去校驗了~ if (isFactoryBeanMetadataMethod(invocation.getMethod())) { return invocation.proceed(); } Class>[] groups = determineValidationGroups(invocation); // Standard Bean Validation 1.1 API ExecutableValidator是1.1提供的 ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set> result; // 錯誤消息result 若存在最終都會ConstraintViolationException異常形式拋出 try { // 先校驗方法入參 result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { // 此處回退了異步:找到bridged method方法再來一次 methodToValidate = BridgeMethodResolver.findBridgedMethod(ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass())); result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } if (!result.isEmpty()) { // 有錯誤就拋異常拋出去 throw new ConstraintViolationException(result); } // 執行目標方法 拿到返回值后 再去校驗這個返回值 Object returnValue = invocation.proceed(); result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups); if (!result.isEmpty()) { throw new ConstraintViolationException(result); } return returnValue; } // 找到這個方法上面是否有標注@Validated注解 從里面拿到分組信息 // 備注:雖然代理只能標注在類上,但是分組可以標注在類上和方法上哦~~~~ protected Class>[] determineValidationGroups(MethodInvocation invocation) { Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class); if (validatedAnn == null) { validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class); } return (validatedAnn != null ? validatedAnn.value() : new Class>[0]); } }
這個Advice的實現,簡單到不能再簡單了,稍微有點基礎的應該都能很容易看懂吧(據我不完全估計這個應該是最簡單的)。
==使用細節==(重要)文首雖然已經給了一個使用示例,但是那畢竟只是局部。在實際生產使用中,比如上面理論更重要的是一些使用細節(細節往往是區分你是不是高手的地方),這里從我使用的經驗中,總結如下幾點供給大家參考(基本算是分享我躺過的坑):
使用@Validated去校驗方法Method,不管從使用上還是原理上,都是非常簡單和簡約的,建議大家在企業應用中多多使用。1、約束注解(如@NotNull)不能放在實體類上
一般情況下,我們對于Service層驗證(Controller層一般都不給接口),大都是面向接口編程和使用,那么這種@NotNull放置的位置應該怎么放置呢?
看這個例子:
public interface HelloService { Object hello(@NotNull @Min(10) Integer id, @NotNull String name); } @Validated(Default.class) @Slf4j @Service public class HelloServiceImpl implements HelloService { @Override public Object hello(Integer id, String name) { return null; } }
約束條件都寫在實現類上,按照我們所謂的經驗,應該是不成問題的。但運行:
javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method HelloServiceImpl#hello(Integer) redefines the configuration of HelloService#hello(Integer). at org.hibernate.validator.internal.metadata.aggregated.rule.OverridingMethodMustNotAlterParameterConstraints.apply(OverridingMethodMustNotAlterParameterConstraints.java:24) ...
重說三:請務必注意請務必注意請務必注意這個異常是javax.validation.ConstraintDeclarationException,而不是錯誤校驗錯誤異常javax.validation.ConstraintViolationException。請在做全局異常捕獲的時候一定要區分開來~
異常信息是說parameter constraint configuration在校驗方法入參的約束時,若是@Override父類/接口的方法,那么這個入參約束只能寫在父類/接口上面~~~
至于為什么只能寫在接口處,這個具體原因其實是和Bean Validation的實現產品有關的,比如使用的Hibernate校驗,原因可參考它的此類:OverridingMethodMustNotAlterParameterConstraints
還需注意一點:若實現類寫的約束和接口一模一樣,那也是沒問題的。比如上面若實現類這么寫是沒有問題能夠完成正常校驗的:
@Override public Object hello(@NotNull @Min(10) Integer id, @NotNull String name) { return null; }
雖然能正常work完成校驗,但需要深刻理解一模一樣這四個字。簡單的說把10改成9都會報ConstraintDeclarationException異常,更別談移除某個注解了(不管多少字段多少注解,但凡只要寫了一個就必須保證一模一樣)。
關于@Override方法校驗返回值方面:即使寫在實現類里也不會拋ConstraintDeclarationException
另外@Validated注解它寫在實現類/接口上均可~
最后你應該自己領悟到:若入參校驗失敗了,方法體是不會執行的。但倘若是返回值校驗執行了(即使是失敗了),方法體也肯定被執行了~~2、@NotEmpty/@NotBlank只能哪些類型上?
提出這個細節的目的是:約束注解并不是能用在所有類型上的。比如若你把@NotEmpty讓它去驗證Object類型,它會報錯如下:
javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint "javax.validation.constraints.NotEmpty" validating type "java.lang.Object". Check configuration for "hello."
需要強調的是:若標注在方法上是驗證返回值的,這個時候方法體是已經執行了的,這個和ConstraintDeclarationException不一樣~
對這兩個注解依照官方文檔做如下簡要說明。@NotEmpty只能標注在如下類型
CharSequence
Collection
Map
Array
注意:""它是空的,但是" "就不是了
@NotBlank只能使用在CharSequence上,它是Bean Validation 2.0新增的注解~
3、接口和實現類上都有注解,以誰為準?這個問題有個隱含條件:只有校驗方法返回值時才有這種可能性。
public interface HelloService { @NotEmpty String hello(@NotNull @Min(10) Integer id, @NotNull String name); } @Slf4j @Service @Validated(Default.class) public class HelloServiceImpl implements HelloService { @Override public @NotNull String hello(Integer id, String name) { return ""; } }
運行案例,helloService.hello(18, "fsx");打印如下:
javax.validation.ConstraintViolationException: hello.: 不能為空 ...
到這里,可能有小伙伴就會早早下結論:當同時存在時,以接口的約束為準。
那么,我只把返回值稍稍修改,你再看一下呢???
@Override public @NotNull String hello(Integer id, String name) { return null; // 返回值改為null }
再運行:
javax.validation.ConstraintViolationException: hello.: 不能為空, hello. : 不能為null ...
透過打印的信息,結論就自然不必我多。但是有個道理此處可說明:大膽猜測,小心求證
4、如何校驗級聯屬性?在實際開發中,其實大多數情況下我們方法入參是個對象(甚至對象里面有對象),而不是單單平鋪的參數,因此就介紹一個級聯屬性校驗的例子:
@Getter @Setter @ToString public class Person { @NotNull private String name; @NotNull @Positive private Integer age; @Valid // 讓InnerChild的屬性也參與校驗 @NotNull private InnerChild child; @Getter @Setter @ToString public static class InnerChild { @NotNull private String name; @NotNull @Positive private Integer age; } } public interface HelloService { String cascade(@NotNull @Valid Person father, @NotNull Person mother); } @Slf4j @Service @Validated(Default.class) public class HelloServiceImpl implements HelloService { @Override public String cascade(Person father, Person mother) { return "hello cascade..."; } }
運行測試用例:
@Test public void test1() { helloService.cascade(null, null); }
輸出如下:
cascade.father: 不能為null, cascade.mother: 不能為null
此處說明一點:若你father前面沒加@NotNull,那打印的消息只有:cascade.mother: 不能為null
我把測試用例改造如下,你繼續感受一把:
@Test public void test1() { Person father = new Person(); father.setName("fsx"); Person.InnerChild innerChild = new Person.InnerChild(); innerChild.setAge(-1); father.setChild(innerChild); helloService.cascade(father, new Person()); }
錯誤消息如下(請小伙伴仔細觀察和分析緣由):
cascade.father.age: 不能為null, cascade.father.child.name: 不能為null, cascade.father.child.age: 必須是正數
思考:為何mother的相關屬性以及子屬性為何全都沒有校驗呢?
5、循環依賴問題上面說了Spring對@Validated的處理和對@Aysnc的代理邏輯是差不多的,有了之前的經驗,很容易想到它也存在著如題的問題:比如HelloService的A方法想調用本類的B方法,但是很顯然我是希望B方法的方法校驗是能生效的,因此其中一個做法就是注入自己,使用自己的代理對象來調用:
public interface HelloService { Object hello(@NotNull @Min(10) Integer id, @NotNull String name); String cascade(@NotNull @Valid Person father, @NotNull Person mother); } @Slf4j @Service @Validated(Default.class) public class HelloServiceImpl implements HelloService { @Autowired private HelloService helloService; @Override public Object hello(@NotNull @Min(10) Integer id, @NotNull String name) { helloService.cascade(null, null); // 調用本類方法 return null; } @Override public String cascade(Person father, Person mother) { return "hello cascade..."; } }
運行測試用例:
@Test public void test1() { helloService.hello(18, "fsx"); // 入口方法校驗通過,內部調用cascade方法希望繼續得到校驗 }
運行報錯:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name "helloServiceImpl": Bean with name "helloServiceImpl" has been injected into other beans [helloServiceImpl] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using "getBeanNamesOfType" with the "allowEagerInit" flag turned off, for example. at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean ...
這個報錯消息不可為不熟悉。關于此現象,之前做過非常非常詳細的說明并且提供了多種解決方案,所以此處略過。
若關于此問的原因和解決方案不明白的,請移步此處:【小家Spring】使用@Async異步注解導致該Bean在循環依賴時啟動報BeanCurrentlyInCreationException異常的根本原因分析,以及提供解決方案
雖然我此處不說解決方案,但我提供問題解決后運行的打印輸出情況,供給小伙伴調試參考,此舉很暖心有木有:
javax.validation.ConstraintViolationException: cascade.mother: 不能為null, cascade.father: 不能為null ...總結
本文介紹了Spring提供給我們方法級別校驗的能力,在企業應用中使用此種方式完成絕大部分的基本校驗工作,能夠讓我們的代碼更加簡潔、可控并且可擴展,因此我是推薦使用和擴散的~
在文末有必要強調一點:關于上面級聯屬性的校驗時使用的@Valid注解你使用@Validated可替代不了,不會有效果的。
至于有小伙伴私信我疑問的問題:為何他Controller方法中使用@Valid和@Validated均可,并且網上同意給的答案都是都可用,差不多???還是那句話:這是下篇文章的重點,請持續關注~
稍稍說一下它的弊端:因為校驗失敗它最終采用的是拋異常方式來中斷,因此效率上有那么一丟丟的損耗。but,你的應用真的需要考慮這種極致性能問題嗎?這才是你該思考的~知識交流
若文章格式混亂,可點擊:原文鏈接-原文鏈接-原文鏈接-原文鏈接-原文鏈接
==The last:如果覺得本文對你有幫助,不妨點個贊唄。當然分享到你的朋友圈讓更多小伙伴看到也是被作者本人許可的~==
**若對技術內容感興趣可以加入wx群交流:Java高工、架構師3群。
若群二維碼失效,請加wx號:fsx641385712(或者掃描下方wx二維碼)。并且備注:"java入群" 字樣,會手動邀請入群**
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/75584.html
摘要:畢竟永遠相信本文能給你帶來意想不到的收獲使用示例關于數據校驗這一塊在中的使用案例,我相信但凡有點經驗的程序員應該沒有不會使用的,并且還不乏熟練的選手。 每篇一句 NBA里有兩大笑話:一是科比沒天賦,二是詹姆斯沒技術 相關閱讀 【小家Java】深入了解數據校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validati...
摘要:方案一借助對方法級別數據校驗的能力首先必須明確一點此能力屬于框架的,而部分框架。 每篇一句 在金字塔塔尖的是實踐,學而不思則罔,思而不學則殆(現在很多編程框架都只是教你碎片化的實踐) 相關閱讀 【小家Java】深入了解數據校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例【小家Spr...
摘要:本文主要介紹在中自動校驗的機制。引入依賴我們使用構建應用來進行演示。在中校驗數據值得注意的地方參數前需要加上注解,表明需要對其進行校驗,而校驗的信息會存放到其后的中。層改寫方法限定需要進行校驗,而方法則不做限制。 簡介 JSR303/JSR-349,hibernate validation,spring validation之間的關系。JSR303是一項標準,JSR-349是其的升級版...
摘要:初步使用主要使用注解的方式對進行校驗,第一個例子在需要校驗的字段上指定約束條件然后在中可以這樣調用,加上注解即可。如果校驗失敗,默認會返回框架的出錯信息。指定到的分組名會全部進行校驗,不指定的不校驗。 Spring Boot - 表單校驗(JSR303&Hibernate Validator) 回顧 Spring Boot - 初識 Hello World Spring Boot -...
摘要:如果說要使用數據校驗,我十分相信小伙伴們都能夠使用,但估計大都是有個前提的環境。具體使用可參考小家讓支持對平鋪參數執行數據校驗默認使用只能對進行校驗級聯校驗什么叫級聯校驗,其實就是帶校驗的成員里存在級聯對象時,也要對它完成校驗。 每篇一句 NBA里有兩大笑話:一是科比沒天賦,二是詹姆斯沒技術 相關閱讀 【小家Java】深入了解數據校驗:Java Bean Validation 2.0(...
閱讀 1837·2021-11-11 16:54
閱讀 2061·2019-08-30 15:56
閱讀 2372·2019-08-30 15:44
閱讀 1297·2019-08-30 15:43
閱讀 1864·2019-08-30 11:07
閱讀 821·2019-08-29 17:11
閱讀 1470·2019-08-29 15:23
閱讀 3011·2019-08-29 13:01