在《深度工做》中做者提出這麼一個公式:高質量產出=時間*專一度。因此高質量的產出不是靠時間熬出來的,而是效率爲王
【小家Java】深刻了解數據校驗:Java Bean Validation 2.0(JSR30三、JSR34九、JSR380)Hibernate-Validation 6.x使用案例
【小家Java】深刻了解數據校驗(Bean Validation):基礎類打點(ValidationProvider、ConstraintDescriptor、ConstraintValidator)
【小家Spring】詳述Spring對Bean Validation支持的核心API:Validator、SmartValidator、LocalValidatorFactoryBean...java
<center>對Spring感興趣可掃碼加入wx羣:Java高工、架構師3羣
(文末有二維碼)</center>spring
你在書寫業務邏輯的時候,是否會常常書寫大量的判空校驗。好比Service
層或者Dao
層的方法入參、入參對象、出參中你是否都有本身的一套校驗規則?好比有些字段必傳,有的非必傳;返回值中有些字段必須有值,有的非必須等等~編程
如上描述的校驗邏輯,窺探一下你的代碼,估摸裏面有大量的if else
吧。此部分邏輯簡單(由於和業務關係不大)卻看起來眼花繚亂(趕忙偷偷去喵一下你本身的代碼吧,哈哈)。在攻城主鍵變大的時候,你會發現會有大量的重複代碼出現,這部分就是你入職一個新公司的吐槽點之一:垃圾代碼。架構
若你追求乾淨的代碼,甚至有代碼潔癖
,如上衆多if else
的重複無心義勞動無疑是你的痛點,那麼本文應該可以幫到你。Bean Validation
校驗實際上是基於DDD
思想設計的,咱們雖然能夠不徹底的聽從這種思考方式編程,可是其優雅的優勢仍是可取的,本文將介紹Spring
爲此提供的解決方案~app
在講解以前,首先就來體驗一把吧~異步
@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; } }
向容器裏註冊一個處理器:maven
@Configuration public class RootConfig { @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); } }
測試:ide
@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中加入編譯插件而且配置好此編譯參數)
若須要校驗方法返回值,改寫以下:post
@NotNull Object hello(Integer id); // 此種寫法效果同上 //@NotNull Object hello(Integer id);
運行:
javax.validation.ConstraintViolationException: hello.<return value>: 不能爲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
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<ConstraintViolation<Object>> 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
,無論從使用上仍是原理上,都是很是簡單和簡約的,建議你們在企業應用中多多使用。
通常狀況下,咱們對於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
註解它寫在實現類/接口上都可~
最後你應該本身領悟到:若入參校驗失敗了,方法體是不會執行的。但假若是返回值校驗執行了(即便是失敗了),方法體也確定被執行了~~
提出這個細節的目的是:約束註解並非能用在全部類型上的。好比若你把@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.<return value>'
須要強調的是:若標註在方法上是驗證返回值的,這個時候
方法體是已經執行了的,這個和
ConstraintDeclarationException
不同~
對這兩個註解依照官方文檔作以下簡要說明。@NotEmpty
只能標註在以下類型
注意:""它是空的,可是" "就不是了
@NotBlank
只能使用在CharSequence
上,它是Bean Validation 2.0新增的註解~
這個問題有個隱含條件:只有校驗方法返回值時纔有這種可能性。
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.<return value>: 不能爲空 ...
到這裏,可能有小夥伴就會早早下結論:當同時存在時,以接口的約束爲準。
那麼,我只把返回值稍稍修改,你再看一下呢???
@Override public @NotNull String hello(Integer id, String name) { return null; // 返回值改成null }
再運行:
javax.validation.ConstraintViolationException: hello.<return value>: 不能爲空, hello.<return value>: 不能爲null ...
透過打印的信息,結論就天然沒必要我多。可是有個道理此處可說明:大膽猜想,當心求證
級聯屬性
?在實際開發中,其實大多數狀況下咱們方法入參是個對象(甚至對象裏面有對象),而不是單單平鋪的參數,所以就介紹一個級聯屬性校驗的例子:
@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
的相關屬性以及子屬性爲什麼全都沒有校驗呢?
上面說了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入羣"
字樣,會手動邀請入羣**