摘要:畢竟永遠相信本文能給你帶來意想不到的收獲使用示例關于數據校驗這一塊在中的使用案例,我相信但凡有點經驗的程序員應該沒有不會使用的,并且還不乏熟練的選手。
每篇一句
NBA里有兩大笑話:一是科比沒天賦,二是詹姆斯沒技術相關閱讀
【小家Java】深入了解數據校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例
【小家Spring】讓Controller支持對平鋪參數執行數據校驗(默認Spring MVC使用@Valid只能對JavaBean進行校驗)
【小家Spring】Spring方法級別數據校驗:@Validated + MethodValidationPostProcessor優雅的完成數據校驗動作
上篇文章 介紹了Spring環境下實現優雅的方法級別的數據校驗,并且埋下一個伏筆:它在Spring MVC(Controller層)里怎么應用呢?本文為此繼續展開講解Spring MVC中的數據校驗~
可能小伙伴能立馬想到:這不一樣嗎?我們使用Controller就是方法級別的,所以它就是直接應用了方法級別的校驗而已嘛~對于此疑問我先不解答,而是順勢再拋出兩個問題你自己應該就能想明白了:
上文有說過,基于方法級別的校驗Spring默認是并未開啟的,但是為什么你在Spring MVC卻可以直接使用@Valid完成校驗呢?
1. 可能有的小伙伴說他用的是`SpringBoot`可能默認給開啟了,其實不然。哪怕你用的傳統`Spring MVC`你會發現也是直接可用的,不信你就試試
類比一下:Spring MVC的HandlerInterceptor是AOP思想的實現,但你有沒有發現即使你沒有啟動@EnableAspectJAutoProxy的支持,它依舊好使~
若你能想明白我提出的這兩個問題,下文就非常不難理解了。當然即使你知道了這兩個問題的答案,還是建議你讀下去。畢竟:永遠相信本文能給你帶來意想不到的收獲~
使用示例關于數據校驗這一塊在Spring MVC中的使用案例,我相信但凡有點經驗的Java程序員應該沒有不會使用的,并且還不乏熟練的選手。在此之前我簡單“采訪”過,絕大多數程序員甚至一度認為Spring中的數據校驗就是指的在Controller中使用@Validated校驗入參JavaBean這一塊~
因此下面這個例子,你應該一點都不陌生:
@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; } } @RestController @RequestMapping public class HelloController { @PostMapping("/hello") public Object helloPost(@Valid @RequestBody Person person, BindingResult result) { System.out.println(result.getErrorCount()); System.out.println(result.getAllErrors()); return person; } }
發送post請求:/hello Content-Type=application/json,傳入的json串如下:
{ "name" : "fsx", "age" : "-1", "child" : { "age" : 1 } }
控制臺有如下打印:
2 [Field error in object "person" on field "child.name": rejected value [null]; codes [NotNull.person.child.name,NotNull.child.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.child.name,child.name]; arguments []; default message [child.name]]; default message [不能為null], Field error in object "person" on field "age": rejected value [-1]; codes [Positive.person.age,Positive.age,Positive.java.lang.Integer,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.age,age]; arguments []; default message [age]]; default message [必須是正數]]
從打印上看:校驗生效(拿著錯誤消息就可以返回前端展示,或者定位到錯誤頁面都行)。
此例兩個小細節務必注意:
@RequestBody注解不能省略,否則傳入的json無法完成數據綁定(即使不綁定,校驗也是生效的哦)~
若方法入參不寫BindingResult result這個參數,請求得到的直接是400錯誤,因為若有校驗失敗的服務端會拋出異常org.springframework.web.bind.MethodArgumentNotValidException。若寫了,那就調用者自己處理嘍~
據我不完全和不成熟的統計,就這個案例就覆蓋了小伙伴們實際使用中的90%以上的真實使用場景,使用起來確實非常的簡單、優雅、高效~
但是作為一個有豐富經驗的程序員的你,雖然你使用了@Valid優雅的完成了數據校驗,但回頭是你是否還會發現你的代碼里還是存在了大量的if else的基礎的校驗?什么原因?其實根本原因只有一個:很多case使用@Valid并不能覆蓋,因為它只能校驗JavaBean
我相信你是有這樣那樣的使用痛點的,本文先從原理層面分析,進而給出你所遇到的痛點問題的參考解決參考方案~
Controller提供的使用@Valid便捷校驗JavaBean的原理,和Spring方法級別的校驗支持的原理是有很大差異的(可類比Spring MVC攔截器和Spring AOP的差異區別~),那么現在就看看這塊吧
請不要忽視優雅代碼的力量,它會成倍提升你的編碼效率、成倍降低后期維護成本,甚至成倍提升你的擴展性和成倍降低你寫bug的可能性~回憶DataBinder/WebDataBinder
若對Spring數據綁定模塊不是很熟悉的(有閱讀過我之前文章的可忽略),建議先補:
【小家Spring】聊聊Spring中的數據綁定 --- DataBinder本尊(源碼分析)
【小家Spring】聊聊Spring中的數據綁定 --- WebDataBinder、ServletRequestDataBinder、WebBindingInitializer...
DataBinder類名叫數據綁定,但它在org.springframework.validation這個包,可見Spring它把數據綁定和數據校驗牢牢的放在了一起,并且內部弱化了數據校驗的概念以及邏輯(Spring想讓調用者無需關心數據校驗的細節,全由它來自動完成,減少使用的成本)。
我們知道DataBinder它主要對外提供了bind(PropertyValues pvs)和validate()方法,當然還有處理綁定/校驗失敗的相關(配置)組件:
public class DataBinder implements PropertyEditorRegistry, TypeConverter { ... @Nullable private AbstractPropertyBindingResult bindingResult; // 它是個BindingResult @Nullable private MessageCodesResolver messageCodesResolver; private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor(); // 最重要是它:它是org.springframework.validation.Validator // 一個DataBinder 可以持有對個驗證器。也就是說對于一個Bean,是可以交給多個驗證器去驗證的(當然一般都只有一個即可而已~~~) private final Listvalidators = new ArrayList<>(); public void bind(PropertyValues pvs) { MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues ? (MutablePropertyValues) pvs : new MutablePropertyValues(pvs)); doBind(mpvs); } ... public void validate() { Object target = getTarget(); Assert.state(target != null, "No target to validate"); BindingResult bindingResult = getBindingResult(); // 拿到所有的驗證器 一個個的對此target進行驗證~~~ // Call each validator with the same binding result for (Validator validator : getValidators()) { validator.validate(target, bindingResult); } } }
DataBinder提供了這兩個非常獨立的原子方法:綁定 + 校驗。他倆結合完成了我們的數據綁定+數據校驗,完全的和業務無關~
網上有很多文章說DataBinder完成數據綁定后繼續校驗,這種說法是不準確的呀,因為它并不處理這部分的組合邏輯,它只提供原始能力~Spring MVC處理入參的時機
Spring MVC處理入參的邏輯是非常復雜的,前面花大篇幅講了Spring MVC對返回值的處理器:HandlerMethodReturnValueHandler,詳見:
【小家Spring】Spring MVC容器的web九大組件之---HandlerAdapter源碼詳解---一篇文章帶你讀懂返回值處理器HandlerMethodReturnValueHandler
同樣的,本文只關注它對@RequestBody這種類型的入參進行講解~
處理入參的處理器:HandlerMethodArgumentResolver,處理@RequestBody最終使用的實現類是:RequestResponseBodyMethodProcessor,Spring借助此處理器完成一系列的消息轉換器、數據綁定、數據校驗等工作~
這個類應該是陌生的,在上面推薦的處理MVC返回值的文章中有提到過它:它能夠處理@ResponseBody注解返回值(請參考它的supportsReturnType()方法~)
它還有另一個能力是:它能夠處理請求參數(當然也是標注了@RequestBody它的~)
所以它既是個處理返回值的HandlerMethodReturnValueHandler,有是一個處理入參的HandlerMethodArgumentResolver。所以它命名為Processor而不是Resolver/Handler嘛,這就是命名的藝術~
// @since 3.1 public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestBody.class); } // 類上或者方法上標注了@ResponseBody注解都行 @Override public boolean supportsReturnType(MethodParameter returnType) { return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class)); } // 這是處理入參封裝校驗的入口,也是本文關注的焦點 @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 它是支持`Optional`容器的 parameter = parameter.nestedIfOptional(); // 使用消息轉換器HttpInputMessage把request請求轉換出來 // 此處注意:比如本例入參是Person類,所以經過這里處理會生成一個空的Person對象出來(反射) Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); // 獲取到入參的名稱 // 請注意:這里的名稱是類名首字母小寫,并不是你方法里寫的名字。比如本利若形參名寫為personAAA,但是name的值還是person String name = Conventions.getVariableNameForParameter(parameter); // 只有存在binderFactory才會去完成自動的綁定、校驗~ // 此處web環境為:ServletRequestDataBinderFactory if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); // 顯然傳了參數才需要去綁定校驗嘛 if (arg != null) { // 這里完成數據綁定+數據校驗~~~~~(綁定的錯誤和校驗的錯誤都會放進Errors里) // Applicable:適合 validateIfApplicable(binder, parameter); // 若有錯誤消息hasErrors(),并且僅跟著的一個參數不是Errors類型,Spring MVC會主動給你拋出MethodArgumentNotValidException異常 // 否則,調用者自行處理 if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } // 把錯誤消息放進去 證明已經校驗出錯誤了~~~ // 后續邏輯會判斷MODEL_KEY_PREFIX這個key的~~~~ if (mavContainer != null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } } return adaptArgumentIfNecessary(arg, parameter); } // 校驗,如果合適的話。使用WebDataBinder,失敗信息最終也都是放在它身上~ 本方法是本文關注的焦點 // 入參:MethodParameter parameter protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { // 拿到標注在此參數上的所有注解們(比如此處有@Valid和@RequestBody兩個注解) Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { // 先看看有木有@Validated Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); // 這個里的判斷是關鍵:可以看到標注了@Validated注解 或者注解名是以Valid打頭的 都會有效哦 //注意:這里可沒說必須是@Valid注解。實際上你自定義注解,名稱只要一Valid開頭都成~~~~~ if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { // 拿到分組group后,調用binder的validate()進行校驗~~~~ // 可以看到:拿到一個合適的注解后,立馬就break了~~~ // 所以若你兩個主機都標注@Validated和@Valid,效果是一樣滴~ Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); binder.validate(validationHints); break; } } } ... }
本文我們只著眼于關注@Valid的數據校驗這塊,有幾個相關的使用小細節,總結如下:
形參(@RequestBody標注的入參)的name(形參名)和你寫什么無關,若是實體類名字就是類名首字母小寫。(若是數組、集合等,都會有自己特定的名稱)
@Validated和@Valid都能使校驗生效,但卻不僅僅是它哥倆才能行:任何名稱是"Valid"打頭的注解都能使得數據校驗生效
1. 自定義注解名稱以`Valid`開頭,并且給個`value`屬性同樣能夠指定`Group`分組 2. 個人直接建議使用`@Validated`即可,而去使用`@Valid`了,更不用自己給自己找麻煩去自定義注解啥的了~
只有當Errors(BindingResult)入參是是僅跟著@Valid注解的實體,Spring MVC才會把錯誤消息放權交給調用者處理,否則(沒有或者不是緊挨著)它會拋出MethodArgumentNotValidException異常~
這是使用@RequestBody結合@Valid完成數據校驗的基本原理。其實當Spring MVC在處理@RequestPart注解入參數據時,也會執行綁定、校驗的相關邏輯。對應處理器是RequestPartMethodArgumentResolver,原理大體上和這相似,它主要處理Multipart相關,本文忽略~
==此處提示一個點,此文發出去后有一個好奇的小寶寶問我入參能使用多個對象并且都用@RequestBody標注嗎?==
關于這個問題姑且先不考慮合理與否,我們這樣做試試:
@PostMapping("/hello") public Object helloPost(@Valid @RequestBody Person personAAA, BindingResult result, @Valid @RequestBody Person personBBB) { ... }
請求卻報錯了:
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is java.io.IOException: Stream closed]
錯誤消息很好理解:請求域的Body體是只能被讀取一次的(流只能被讀取一次嘛)。
若你是好奇的,你可能還會問:URL參數呢?請求鏈接?后面的參數呢,如何封裝???因為本部分內容不是本文的關注點,若有興趣請出門左拐~
說明:關于使用Map、List、數組等接受@RequestBody參數的情況類似,區別在于綁定器上,對Map、List的校驗前面文章有過講解,此處就不展開了。希望讀者能掌握這部分內容,因為它和面向使用者比較重要的@InitBinder強關聯~~~
實際使用中一般使用@Validated分組校驗(若需要),然后結合全局異常的處理方式來友好的對調用者展示錯誤消息~
全局異常處理示例當校驗失敗時,Spring會拋出MethodArgumentNotValidException異常,該異常會持有校驗結果對象BindingResult,從而獲得校驗失敗信息。本處只給示例,僅供參考:
@RestControllerAdvice public class MethodArgumentNotValidExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public Result handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { BindingResult bindingResult = ex.getBindingResult(); StringBuilder stringBuilder = new StringBuilder(); for (FieldError error : bindingResult.getFieldErrors()) { String field = error.getField(); Object value = error.getRejectedValue(); String msg = error.getDefaultMessage(); String message = String.format("錯誤字段:%s,錯誤值:%s,原因:%s;", field, value, msg); stringBuilder.append(message).append(" "); } return Result.error(MsgDefinition.ILLEGAL_ARGUMENTS.codeOf(), stringBuilder.toString()); } }遺留痛點
你是否發現,雖然Spring MVC給我們提供了極其方便的數據校驗方式,但是它還是有比較大的局限性的:它要求待校驗的入參是JavaBean
請注意:并不一樣要求是請求Body體哦,比如get請求的入參若用JavaBean接收的話,依舊能啟用校驗
但在實際應用中,其實我們非常多的Controller方法的方法入參是平鋪的,也就是所謂的平鋪參數,形如這樣:
@PutMapping("/hello/id/{id}/status/{status}") public Object helloGet(@PathVariable Integer id, @PathVariable Integer status) { ... return "hello world"; }
其實,特別是get請求的case,@RequestParam入參一般是非常多的(比如分頁查詢),難道對于這種平鋪參數的case,我們真的是能通過人肉if else的去校驗嗎?
興許你對此問題有興趣,那就參閱本文吧,它能給你提供解決方案:【小家Spring】讓Controller支持對平鋪參數執行數據校驗(默認Spring MVC使用@Valid只能對JavaBean進行校驗)
如題的問題,我相信是很多小伙伴都很關心的一個對比,若你把這個系列都有喵過,那么這個問題的答案就浮出水面了:
@Valid:標準JSR-303規范的標記型注解,用來標記驗證屬性和方法返回值,進行級聯和遞歸校驗
@Validated:Spring的注解,是標準JSR-303的一個變種(補充),提供了一個分組功能,可以在入參驗證時,根據不同的分組采用不同的驗證機制
在Controller中校驗方法參數時,使用@Valid和@Validated并無特殊差異(若不需要分組校驗的話)
@Validated注解可以用于類級別,用于支持Spring進行方法級別的參數校驗。@Valid可以用在屬性級別約束,用來表示級聯校驗。
@Validated只能用在類、方法和參數上,而@Valid可用于方法、字段、構造器和參數上
最后提示一點:Spring Boot的Web Starter已經加入了Bean Validation以及實現的依賴,可以直接使用。但若是純Spring MVC環境,請自行導入~總結
本文介紹的是我們平時使用得最多的數據校驗場景:使用@Validated完成Controller的入參校驗,實現優雅的處理數據校驗。同時希望通過本文能讓你徹底弄懂@Validated和@Valid使用上的區別以及聯系,在實際生產使用中能夠做到更加的得心應手~
知識交流若文章格式混亂,可點擊:原文鏈接-原文鏈接-原文鏈接-原文鏈接-原文鏈接
==The last:如果覺得本文對你有幫助,不妨點個贊唄。當然分享到你的朋友圈讓更多小伙伴看到也是被作者本人許可的~==
**若對技術內容感興趣可以加入wx群交流:Java高工、架構師3群。
若群二維碼失效,請加wx號:fsx641385712(或者掃描下方wx二維碼)。并且備注:"java入群" 字樣,會手動邀請入群**
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/75669.html
摘要:方案一借助對方法級別數據校驗的能力首先必須明確一點此能力屬于框架的,而部分框架。 每篇一句 在金字塔塔尖的是實踐,學而不思則罔,思而不學則殆(現在很多編程框架都只是教你碎片化的實踐) 相關閱讀 【小家Java】深入了解數據校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例【小家Spr...
摘要:就這樣借助相關約束注解,就非常簡單明了,語義清晰的優雅的完成了方法級別入參校驗返回值校驗的校驗。但倘若是返回值校驗執行了即使是失敗了,方法體也肯定被執行了只能哪些類型上提出這個細節的目的是約束注解并不是能用在所有類型上的。 每篇一句 在《深度工作》中作者提出這么一個公式:高質量產出=時間*專注度。所以高質量的產出不是靠時間熬出來的,而是效率為王 相關閱讀 【小家Java】深入了解數據校...
摘要:如果說要使用數據校驗,我十分相信小伙伴們都能夠使用,但估計大都是有個前提的環境。具體使用可參考小家讓支持對平鋪參數執行數據校驗默認使用只能對進行校驗級聯校驗什么叫級聯校驗,其實就是帶校驗的成員里存在級聯對象時,也要對它完成校驗。 每篇一句 NBA里有兩大笑話:一是科比沒天賦,二是詹姆斯沒技術 相關閱讀 【小家Java】深入了解數據校驗:Java Bean Validation 2.0(...
摘要:和上標注的約束都會被執行注意如果子類覆蓋了父類的方法,那么子類和父類的約束都會被校驗。 每篇一句 沒有任何技術方案會是一種銀彈,任何東西都是有利弊的 相關閱讀 【小家Java】深入了解數據校驗:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例【小家Spring】Spring方法級別數據校...
摘要:也就是說如果傳的中有的為空或者是負數,入參驗證不會檢測出來。為了能夠進行嵌套驗證,必須手動在實體的字段上明確指出這個字段里面的實體也要進行驗證。用在方法入參上無法單獨提供嵌套驗證功能。 來源:blog.csdn.net/qq_27680317/article/details/79970590整編:Java技術棧(公眾號ID:javastack) Spring Validation驗證框...
閱讀 1336·2023-04-25 23:47
閱讀 912·2021-11-23 09:51
閱讀 4432·2021-09-26 10:17
閱讀 3706·2021-09-10 11:19
閱讀 3254·2021-09-06 15:10
閱讀 3546·2019-08-30 12:49
閱讀 2421·2019-08-29 13:20
閱讀 1730·2019-08-28 18:14