
一、@Validated和@Valid對比
Spring Validation驗證架構對參數的驗證機制提供了@Validated(Spring's JSR-303規範,是标準JSR-303的一個變種),javax提供了@Valid(标準JSR-303規範),配合BindingResult可以直接提供參數驗證結果。
在檢驗Controller的入參是否符合規範時,使用@Validated或者@Valid在基本驗證功能上沒有太多差別。但是在分組、注解地方、嵌套驗證等功能上兩個有所不同。
1、分組
@Validated:提供了一個分組功能,可以在入參驗證時,根據不同的分組采用不同的驗證機制,可參考高效使用hibernate-validator校驗架構。
@Valid:作為标準JSR-303規範,不支援分組的功能。
2、注解地方
@Validated:可以用在類型、方法和方法參數上。但是不能用在成員屬性(字段)上。
@Valid:可以用在方法、構造函數、方法參數和成員屬性(字段)上。
兩者是否能用于成員屬性(字段)上直接影響能否提供嵌套驗證的功能。
二、SpringMvc接口參數校驗原理
springmvc接口方法中注有@Validated或@Valid參數是如何校驗的呢?怎麼就能把參數綁定之後的校驗結果給到BindingResult執行個體呢?
@RestController
public class TestController {
@RequestMapping("/xxx/yyy")
public void test1(@Validated Test test, BindingResult bindingResult) {
doSomething();
}
@RequestMapping("/yyy/zzz")
public void test1(@Valid Test test, BindingResult bindingResult) {
doSomething();
}
}
其實如果你對springmvc的方法參數解析器(HandlerMethodArgumentResolver)了解一些,就應該知道參數校驗這塊肯定是在對應的方法參數解析器裡執行的。如下是@RequestBody注解對應的參數解析器(RequestResponseBodyMethodProcessor)。
直接定位到resolveArgument這個方法,很明顯,該方法是根據參數類型找到支援的消息轉換器(Message Converter),然後從request body中讀取資訊,最後轉換成對應的參數實體。
WebDataBinder主要是完成對象屬性校驗的。如果你熟悉@ModelAttribute注解對應的方法參數解析器(ModelAttributeMethodProcessor),是先通過WebDataBinder進行入參屬性綁定,然後再進行校驗。
簡單說一下validateIfApplicable方法的邏輯,周遊目前參數methodParam所有的注解,如果注解是@Validated或注解的名字以‘Valid’開頭,則使用WebDataBinder對象執行校驗邏輯。
isBindExceptionRequired方法,說的通俗一點,就是要不要抛出異常。怎麼判斷呢?如果目前參數後面還有參數并且參數類型是Errors(BindingResult繼承Errors)則不抛出異常。
最後會把BindingResult結果放到ModelAndViewContainer對象中儲存起來,記住
BindingResult.MODEL_KEY_PREFIX這個key prefix。
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
都說到這裡了,BindingResult結果也已經拿到了,該怎麼傳遞給方法中參數呢?會不會有BindingResult參數類型的參數解析器呢?咦,還真他娘的有呀。。。
看到ErrorsMethodArgumentResolver這個參數解析器的注釋和源碼,的确是針對BindingResult這種參數類型的。
BindingResult.MODEL_KEY_PREFIX這個常量在這裡出現了,在ModelAndViewContainer對象中拿到BindingResult對象。注意最後面抛出了一個IllegalStateException異常,也就是在ModelAndViewContainer對象中沒有找到BindingResult對象的時候才會抛出這個異常,什麼情況找不到?
@RequestMapping("/xxx/yyy")
public void test1(BindingResult bindingResult, @Validated @RequestBody Test test) {
doSomething();
}
上述寫法就會出現IllegalStateException異常。因為springmvc解析參數的時候是按照順序, 是以BindingResult類型的參數一定要放在校驗實體的後面。
三、@Validated方法級校驗
注意:方法級别的入參有可能是各種平鋪的參數、也可能是一個或者多個對象。
下面這個例子就是@Validated注解方法級的校驗demo,不過需要配合MethodValidationPostProcessor這個後置處理器使用,需要我們手動注冊一下。
@Validated(Default.class)
public interface HelloService {
Object hello(@NotNull @Min(10) Integer id, @NotNull String name);
}
public class HelloServiceImpl implements HelloService {
@Override
public Object hello(Integer id, String name) {
return null;
}
}
簡單說一下MethodValidationPostProcessor。
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
protected Advice createMethodValidationAdvice(Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
validatedAnnotationType變量預設是就是@Validated注解類型,是以建立的切面Pointcut對象是切入所有注有@Validated注解的類的所有方法。
Spring環境裡建立動态代理核心指定一個或多個advisor,advisor由pointcut和advice組成,pointcut已經建立,advice執行個體即MethodValidationInterceptor。很明顯,MethodValidationInterceptor專門用于處理方法級别的資料校驗,包括入參校驗和出參校驗。本文就不詳細說明了,感興趣的可以直接參考一下源碼。