每篇一句
NBA里有兩大笑話:一是科比沒天賦,二是詹姆斯沒技術
相關閱讀
【小家Java】深入了解數(shù)據(jù)校驗:Java Bean Validation 2.0(JSR303闲询、JSR349袍祖、JSR380)Hibernate-Validation 6.x使用案例
【小家Spring】讓Controller支持對平鋪參數(shù)執(zhí)行數(shù)據(jù)校驗(默認Spring MVC使用@Valid只能對JavaBean進行校驗)
【小家Spring】Spring方法級別數(shù)據(jù)校驗:@Validated + MethodValidationPostProcessor優(yōu)雅的完成數(shù)據(jù)校驗動作
<center>對Spring感興趣可掃碼加入wx群:Java高工底瓣、架構師3群
(文末有二維碼)</center>
前言
上篇文章 介紹了Spring
環(huán)境下實現(xiàn)優(yōu)雅的方法級別的數(shù)據(jù)校驗,并且埋下一個伏筆:它在Spring MVC
(Controller
層)里怎么應用呢蕉陋?本文為此繼續(xù)展開講解Spring MVC
中的數(shù)據(jù)校驗~
可能小伙伴能立馬想到:這不一樣嗎捐凭?我們使用Controller
就是方法級別的,所以它就是直接應用了方法級別的校驗而已嘛~對于此疑問我先不解答凳鬓,而是順勢再拋出兩個問題你自己應該就能想明白了:
- 上文有說過茁肠,基于方法級別的校驗
Spring
默認是并未開啟
的,但是為什么你在Spring MVC
卻可以直接使用@Valid
完成校驗呢村视?- 可能有的小伙伴說他用的是
SpringBoot
可能默認給開啟了官套,其實不然。哪怕你用的傳統(tǒng)Spring MVC
你會發(fā)現(xiàn)也是直接可用的,不信你就試試
- 可能有的小伙伴說他用的是
- 類比一下:
Spring MVC
的HandlerInterceptor
是AOP
思想的實現(xiàn)奶赔,但你有沒有發(fā)現(xiàn)即使你沒有啟動@EnableAspectJAutoProxy
的支持惋嚎,它依舊好使~
若你能想明白我提出的這兩個問題,下文就非常不難理解了站刑。當然即使你知道了這兩個問題的答案另伍,還是建議你讀下去。畢竟:永遠相信本文能給你帶來意想不到的收獲~
使用示例
關于數(shù)據(jù)校驗這一塊在Spring MVC
中的使用案例绞旅,我相信但凡有點經(jīng)驗的Java程序員應該沒有不會使用的摆尝,并且還不乏熟練的選手。在此之前我簡單“采訪”
過因悲,絕大多數(shù)程序員甚至一度認為Spring中的數(shù)據(jù)校驗就是指的在Controller
中使用@Validated
校驗入?yún)?code>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;
}
}
發(fā)送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 [必須是正數(shù)]]
從打印上看:校驗生效(拿著錯誤消息就可以返回前端展示讯检,或者定位到錯誤頁面都行)。
此例兩個小細節(jié)務必注意:
-
@RequestBody
注解不能省略卫旱,否則傳入的json無法完成數(shù)據(jù)綁定(即使不綁定人灼,校驗也是生效的哦)~ - 若方法入?yún)⒉粚?code>BindingResult result這個參數(shù),請求得到的直接是400錯誤顾翼,因為若有校驗失敗的服務端會拋出異常
org.springframework.web.bind.MethodArgumentNotValidException
投放。若寫了,那就調(diào)用者自己處理嘍~
據(jù)我不完全和不成熟的統(tǒng)計适贸,就這個案例就覆蓋了小伙伴們實際使用中的90%
以上的真實使用場景灸芳,使用起來確實非常的簡單、優(yōu)雅取逾、高效~
但是作為一個有豐富經(jīng)驗的程序員的你耗绿,雖然你使用了@Valid
優(yōu)雅的完成了數(shù)據(jù)校驗,但回頭是你是否還會發(fā)現(xiàn)你的代碼里還是存在了大量的if else
的基礎的校驗砾隅?什么原因误阻?其實根本原因只有一個:很多case使用@Valid并不能覆蓋,因為它只能校驗JavaBean
我相信你是有這樣那樣的使用痛點的晴埂,本文先從原理層面分析究反,進而給出你所遇到的痛點問題的參考解決參考方案~
原理分析
Controller
提供的使用@Valid
便捷校驗JavaBean
的原理,和Spring方法級別的校驗支持的原理是有很大差異的(可類比Spring MVC
攔截器和Spring AOP
的差異區(qū)別~)儒洛,那么現(xiàn)在就看看這塊吧
請不要忽視
優(yōu)雅
代碼的力量精耐,它會成倍提升你的編碼效率、成倍降低后期維護成本琅锻,甚至成倍提升你的擴展性和成倍降低你寫bug的可能性~
回憶DataBinder
/WebDataBinder
若對Spring
數(shù)據(jù)綁定模塊不是很熟悉的(有閱讀過我之前文章的可忽略)卦停,建議先補:
- 【小家Spring】聊聊Spring中的數(shù)據(jù)綁定 --- DataBinder本尊(源碼分析)
- 【小家Spring】聊聊Spring中的數(shù)據(jù)綁定 --- WebDataBinder向胡、ServletRequestDataBinder、WebBindingInitializer...
DataBinder
類名叫數(shù)據(jù)綁定惊完,但它在org.springframework.validation
這個包僵芹,可見Spring它把數(shù)據(jù)綁定和數(shù)據(jù)校驗牢牢的放在了一起,并且內(nèi)部弱化了數(shù)據(jù)校驗的概念以及邏輯(Spring
想讓調(diào)用者無需關心數(shù)據(jù)校驗的細節(jié)小槐,全由它來自動完成拇派,減少使用的成本)。
我們知道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 List<Validator> validators = 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
提供了這兩個非常獨立的原子方法:綁定 + 校驗控嗜。他倆結合完成了我們的數(shù)據(jù)綁定+數(shù)據(jù)校驗茧彤,完全的和業(yè)務無關~
網(wǎng)上有很多文章說
DataBinder
完成數(shù)據(jù)綁定后繼續(xù)校驗,這種說法是不準確的呀躬审,因為它并不處理這部分的組合邏輯棘街,它只提供原始能力~
Spring MVC
處理入?yún)⒌臅r機
Spring MVC
處理入?yún)⒌倪壿嬍欠浅碗s的蟆盐,前面花大篇幅講了Spring MVC
對返回值的處理器:HandlerMethodReturnValueHandler
承边,詳見:
【小家Spring】Spring MVC容器的web九大組件之---HandlerAdapter源碼詳解---一篇文章帶你讀懂返回值處理器HandlerMethodReturnValueHandler
同樣的,本文只關注它對@RequestBody
這種類型的入?yún)⑦M行講解~
處理入?yún)⒌奶幚砥鳎?code>HandlerMethodArgumentResolver石挂,處理@RequestBody
最終使用的實現(xiàn)類是:RequestResponseBodyMethodProcessor
博助,Spring
借助此處理器完成一系列的消息轉換器、數(shù)據(jù)綁定痹愚、數(shù)據(jù)校驗等工作~
RequestResponseBodyMethodProcessor
這個類應該是陌生的富岳,在上面推薦的處理MVC返回值的文章中有提到過它:它能夠處理@ResponseBody
注解返回值(請參考它的supportsReturnType()
方法~)
它還有另一個能力是:它能夠處理請求參數(shù)(當然也是標注了@RequestBody
它的~)
所以它既是個處理返回值的HandlerMethodReturnValueHandler
,有是一個處理入?yún)⒌?code>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));
}
// 這是處理入?yún)⒎庋b校驗的入口,也是本文關注的焦點
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 它是支持`Optional`容器的
parameter = parameter.nestedIfOptional();
// 使用消息轉換器HttpInputMessage把request請求轉換出來
// 此處注意:比如本例入?yún)⑹荘erson類动壤,所以經(jīng)過這里處理會生成一個空的Person對象出來(反射)
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
// 獲取到入?yún)⒌拿Q
// 請注意:這里的名稱是類名首字母小寫萝喘,并不是你方法里寫的名字。比如本利若形參名寫為personAAA琼懊,但是name的值還是person
String name = Conventions.getVariableNameForParameter(parameter);
// 只有存在binderFactory才會去完成自動的綁定阁簸、校驗~
// 此處web環(huán)境為:ServletRequestDataBinderFactory
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
// 顯然傳了參數(shù)才需要去綁定校驗嘛
if (arg != null) {
// 這里完成數(shù)據(jù)綁定+數(shù)據(jù)校驗~~~~~(綁定的錯誤和校驗的錯誤都會放進Errors里)
// Applicable:適合
validateIfApplicable(binder, parameter);
// 若有錯誤消息hasErrors(),并且僅跟著的一個參數(shù)不是Errors類型哼丈,Spring MVC會主動給你拋出MethodArgumentNotValidException異常
// 否則启妹,調(diào)用者自行處理
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
// 把錯誤消息放進去 證明已經(jīng)校驗出錯誤了~~~
// 后續(xù)邏輯會判斷MODEL_KEY_PREFIX這個key的~~~~
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
// 校驗,如果合適的話醉旦。使用WebDataBinder饶米,失敗信息最終也都是放在它身上~ 本方法是本文關注的焦點
// 入?yún)ⅲ篗ethodParameter parameter
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
// 拿到標注在此參數(shù)上的所有注解們(比如此處有@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后檬输,調(diào)用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
的數(shù)據(jù)校驗這塊褪猛,有幾個相關的使用小細節(jié)网杆,總結如下:
- 形參(
@RequestBody
標注的入?yún)ⅲ┑?code>name(形參名)和你寫什么無關,若是實體類名字就是類名首字母小寫伊滋。(若是數(shù)組碳却、集合等,都會有自己特定的名稱) -
@Validated
和@Valid
都能使校驗生效笑旺,但卻不僅僅是它哥倆才能行:任何名稱是"Valid"打頭的注解都能使得數(shù)據(jù)校驗生效
1. 自定義注解名稱以Valid
開頭昼浦,并且給個value
屬性同樣能夠指定Group
分組
2. 個人直接建議使用@Validated
即可,而去使用@Valid
了筒主,更不用自己給自己找麻煩去自定義注解啥的了~ - 只有當
Errors(BindingResult)
入?yún)⑹鞘莾H跟著@Valid
注解的實體关噪,Spring MVC
才會把錯誤消息放權交給調(diào)用者處理,否則(沒有或者不是緊挨著)它會拋出MethodArgumentNotValidException
異常~
這是使用@RequestBody
結合@Valid
完成數(shù)據(jù)校驗的基本原理乌妙。其實當Spring MVC
在處理@RequestPart
注解入?yún)?shù)據(jù)時使兔,也會執(zhí)行綁定、校驗的相關邏輯藤韵。對應處理器是RequestPartMethodArgumentResolver
虐沥,原理大體上和這相似,它主要處理Multipart
相關泽艘,本文忽略~
==此處提示一個點欲险,此文發(fā)出去后有一個好奇的小寶寶問我入?yún)⒛苁褂枚鄠€對象并且都用@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參數(shù)呢然低?請求鏈接?
后面的參數(shù)呢喜每,如何封裝?脚翘?灼卢?因為本部分內(nèi)容不是本文的關注點,若有興趣請出門左拐~
說明:關于使用Map来农、List鞋真、數(shù)組等接受
@RequestBody
參數(shù)的情況類似,區(qū)別在于綁定器上沃于,對Map涩咖、List的校驗前面文章有過講解海诲,此處就不展開了。
希望讀者能掌握這部分內(nèi)容檩互,因為它和面向使用者比較重要的
@InitBinder
強關聯(lián)~~~
實際使用中一般使用@Validated
分組校驗(若需要)特幔,然后結合全局異常的處理方式來友好的對調(diào)用者展示錯誤消息~
全局異常處理示例
當校驗失敗時,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("\r\n");
}
return Result.error(MsgDefinition.ILLEGAL_ARGUMENTS.codeOf(), stringBuilder.toString());
}
}
遺留痛點
你是否發(fā)現(xiàn)横辆,雖然Spring MVC
給我們提供了極其方便的數(shù)據(jù)校驗方式,但是它還是有比較大的局限性的:它要求待校驗的入?yún)⑹荍avaBean
請注意:并不一樣要求是請求Body體哦茄猫,比如get請求的入?yún)⑷粲肑avaBean接收的話狈蚤,依舊能啟用校驗
但在實際應用中,其實我們非常多的Controller
方法的方法入?yún)⑹?code>平鋪的划纽,也就是所謂的平鋪參數(shù)
脆侮,形如這樣:
@PutMapping("/hello/id/{id}/status/{status}")
public Object helloGet(@PathVariable Integer id, @PathVariable Integer status) {
...
return "hello world";
}
其實,特別是get請求的case阿浓,@RequestParam
入?yún)⒁话闶欠浅6嗟模ū热绶猪摬樵儯┧拢y道對于這種平鋪參數(shù)的case,我們真的是能通過人肉if else
的去校驗嗎芭毙?
興許你對此問題有興趣,那就參閱本文吧卸耘,它能給你提供解決方案:【小家Spring】讓Controller支持對平鋪參數(shù)執(zhí)行數(shù)據(jù)校驗(默認Spring MVC使用@Valid只能對JavaBean進行校驗)
==@Validated和@Valid的區(qū)別==
如題的問題退敦,我相信是很多小伙伴都很關心的一個對比,若你把這個系列都有喵過蚣抗,那么這個問題的答案就浮出水面了:
-
@Valid
:標準JSR-303規(guī)范的標記型注解侈百,用來標記驗證屬性和方法返回值,進行級聯(lián)和遞歸校驗 -
@Validated
:Spring
的注解翰铡,是標準JSR-303
的一個變種(補充)钝域,提供了一個分組功能,可以在入?yún)Ⅱ炞C時锭魔,根據(jù)不同的分組采用不同的驗證機制 - 在
Controller
中校驗方法參數(shù)時例证,使用@Valid和@Validated并無特殊差異(若不需要分組校驗的話) -
@Validated
注解可以用于類級別,用于支持Spring進行方法級別的參數(shù)校驗迷捧。@Valid
可以用在屬性級別約束织咧,用來表示級聯(lián)校驗胀葱。 -
@Validated
只能用在類、方法和參數(shù)上笙蒙,而@Valid
可用于方法抵屿、字段、構造器和參數(shù)上
最后提示一點:
Spring Boot
的Web Starter
已經(jīng)加入了Bean Validation
以及實現(xiàn)的依賴捅位,可以直接使用轧葛。但若是純Spring MVC
環(huán)境,請自行導入~
總結
本文介紹的是我們平時使用得最多的數(shù)據(jù)校驗場景:使用@Validated
完成Controller
的入?yún)⑿r炌Р螅瑢崿F(xiàn)優(yōu)雅的處理數(shù)據(jù)校驗朝群。同時希望通過本文能讓你徹底弄懂@Validated和@Valid
使用上的區(qū)別以及聯(lián)系,在實際生產(chǎn)使用中能夠做到更加的得心應手~
知識交流
若文章格式混亂中符,可點擊
:原文鏈接-原文鏈接-原文鏈接-原文鏈接-原文鏈接
==The last:如果覺得本文對你有幫助姜胖,不妨點個贊唄。當然分享到你的朋友圈讓更多小伙伴看到也是被作者本人許可的~
==
若對技術內(nèi)容感興趣可以加入wx群交流:Java高工淀散、架構師3群
右莱。
若群二維碼失效,請加wx號:fsx641385712
(或者掃描下方wx二維碼)档插。并且備注:"java入群"
字樣慢蜓,會手動邀請入群