一次踩坑記錄 @valid註解不生效 排查過程
一、背景
在進行一次Controller層單測時,方法參數違反Validation約束,發現卻沒有拋出預期的【違反約束】異常。
方法參數上的@Valid註解不生效??
但是以Tomcatweb容器方式啟動,請求該API,@Valid註解卻生效瞭,甚是怪異。
代碼如下:
@RestController @RequestMapping("/api/user/") public class UserController @RequestMapping(value = "") public Response test(@RequestBody @Valid User user) { ... } }
其中Test對象如下所示
@Data public class User { @NotNull(message = "用戶名稱不能為空!") private String name; }
單元測試代碼如下,註意:這裡的user對象並沒有設置name屬性。
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath:/config/spring/application-core.xml", "classpath:/config/spring/application-mvc.xml" }) @Transactional @Commit public class UserControllerTest { @Autowired private UserController controller; @Test public void test(){ controller.test(new User()); } }
以上UserControllerTest在進行測試的時候並未拋出參數校驗ConstraintViolationException的異常。
下面是mvc配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <context:component-scan base-package="com.mtdp" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <mvc:annotation-driven validator="validator"/> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"> <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/> </bean> </beans>
二、解決過程
1.測試過程
在執行單元測試的時候首先暴露出的問題是缺少EL的jar包,因為Hibernate validater執行會依賴EL的jar包。引入對應的jar即可,@see EL依賴
<dependency> <groupId>org.glassfish</groupId> <artifactId>jakarta.el</artifactId> <version>3.0.3</version> </dependency>
web容器默認會引這個jar,所以不需要添加。
2.原因探究
眾所周知,Spring Validation隻是一個抽象,真正執行參數校驗的是hibernate validator,既然以Tomcat的方式能夠生效。那麼我們的辦法:以debug的方式啟動Tomcat,在org.hibernate.validator.internal.engine.ValidatorFactoryImpl#getValidator打上斷點,執行Controller層API調用,看是誰調用的該方法,進而執行參數校驗的。
結果發現是由HandlerMethodArgumentResolver(該接口的作用是對HandlerMethod的方法參數進行校驗、解析、轉換等工作)的實現類RequestResponseBodyMethodProcessor調用的。
RequestResponseBodyMethodProcessor類會轉發給WebDataBinder類,由WebDataBinder最終委托給真正的Validator執行參數校驗。
如下所示:
下面是整體的調用鏈路:
繼而使用之前的UserControllerTest類進行測試,發現執行路徑並不是如此,沒有進DispatcherServlet類。
問題到此明瞭瞭,是因為測試的姿勢不太對,我們應該使用Mock mvc的方式去進行測試,這樣的話就會mock出一個mvc環境,路由到RequestResponseBodyMethodProcessor(標記@RequestBody或者@ResponseBody註解的參數解析器)進行處理,最終執行到方法參數校驗的邏輯。
3.解決方案
修改後的測試代碼如下所示,這樣測試返回的結果是符合預期的,【違反約束】的異常信息被封裝在瞭MvcResult的response字段中瞭。
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath:/config/spring/application-core.xml", "classpath:/config/spring/application-mvc.xml" }) @Transactional @Commit @WebAppConfiguration @EnableWebMvc public class UserControllerTest { @Autowired private WebApplicationContext context; private MockMvc mockMVC; @Before public void initMockMvc() { mockMVC = MockMvcBuilders.webAppContextSetup(context).build(); } @Test public void testPage() throws Exception { String userJson = new Gson().toJson(new User()); MvcResult mvcResult = mockMVC.perform(MockMvcRequestBuilders.post("/api/user").contentType(MediaType.APPLICATION_JSON).content(userJson)).andReturn(); System.out.println(mvcResult.getResponse()); } }
三、Controller 層@Valid註解原理探究
眾所周知,spring mvc XML文件中如果配置瞭<mvc:annotation-driven>標簽時,annotation-driven標簽將會使用MvcNamespaceHandler中的org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser解析器進行解析。
MVC xml handler類如下:
public class MvcNamespaceHandler extends NamespaceHandlerSupport { @Override public void init() { registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); registerBeanDefinitionParser("default-servlet-handler", new DefaultServletHandlerBeanDefinitionParser()); registerBeanDefinitionParser("interceptors", new InterceptorsBeanDefinitionParser()); registerBeanDefinitionParser("resources", new ResourcesBeanDefinitionParser()); registerBeanDefinitionParser("view-controller", new ViewControllerBeanDefinitionParser()); registerBeanDefinitionParser("redirect-view-controller", new ViewControllerBeanDefinitionParser()); registerBeanDefinitionParser("status-controller", new ViewControllerBeanDefinitionParser()); registerBeanDefinitionParser("view-resolvers", new ViewResolversBeanDefinitionParser()); registerBeanDefinitionParser("tiles-configurer", new TilesConfigurerBeanDefinitionParser()); registerBeanDefinitionParser("freemarker-configurer", new FreeMarkerConfigurerBeanDefinitionParser()); registerBeanDefinitionParser("velocity-configurer", new VelocityConfigurerBeanDefinitionParser()); registerBeanDefinitionParser("groovy-configurer", new GroovyMarkupConfigurerBeanDefinitionParser()); registerBeanDefinitionParser("script-template-configurer", new ScriptTemplateConfigurerBeanDefinitionParser()); registerBeanDefinitionParser("cors", new CorsBeanDefinitionParser()); } }
org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser解析器主要是向spring容器中註冊瞭幾個mvc組件bean,分別是RequestMappingHandlerMapping,RequestMappingHandlerAdapter,ExceptionHandlerExceptionResolver,代碼如下所示:
mvc:annotation-driven will registers a RequestMappingHandlerMapping, a RequestMappingHandlerAdapter, and an ExceptionHandlerExceptionResolver (among others) in support of processing requests with annotated controller methods using annotations such as @RequestMapping, @ExceptionHandler, and others.
可以看到在上圖(1)(2)處解析瞭<mvc:annotation-driven>中的validator屬性,並將獲取到的validator賦值給RequestMappingHandlerAdapter中的webBindingInitializer中的validator屬性。
獲取validator的方法如下所示
這裡的邏輯是,如果<mvc:annotation-driven>標簽裡有配置validator屬性,將會使用該屬性引用的validator bean作為檢驗器執行參數校驗,否則會判斷classpath下是否存在JSR validator類,如果存在,將會使用FactoryBean的方式創建默認的OptionalValidatorFactoryBean。
這個validator最終會在RequestResponseBodyMethodProcessor執行參數解析,創建WebDataBinder類時被賦值給WebDataBinder的validators屬性(準確來說,應該是作為validators的一項)。
在RequestResponseBodyMethodProcessor#validateIfApplicable方法中執行校驗邏輯。binder.validate其實會路由給binder的validators執行校驗。
這裡的validators是spring的一個抽象,最終會轉發給真實的validator(也就是配置的providerClass 類)執行參數校驗。
至此完成瞭標註@RequestBody註解的方法參數的校驗。
@Valid註解是什麼
@Valid
用於驗證註解是否符合要求,直接加在變量user之前,在變量中添加驗證信息的要求,當不符合要求時就會在方法中返回message 的錯誤提示信息。
@RestController @RequestMapping("/user") public class UserController { @PostMapping public User create (@Valid @RequestBody User user) { System.out.println(user.getId()); System.out.println(user.getUsername()); System.out.println(user.getPassword()); user.setId("1"); return user; } }
然後在 User 類中添加驗證信息的要求:
public class User { private String id; @NotBlank(message = "密碼不能為空") private String password; }
@NotBlank 註解所指的 password 字段,表示驗證密碼不能為空,如果為空的話,上面 Controller 中的 create 方法會將message 中的”密碼不能為空”返回。
當然也可以添加其他驗證信息的要求:
限制 | 說明 |
---|---|
@Null | 限制隻能為null |
@NotNull | 限制必須不為null |
@AssertFalse | 限制必須為false |
@AssertTrue | 限制必須為true |
@DecimalMax(value) | 限制必須為一個不大於指定值的數字 |
@DecimalMin(value) | 限制必須為一個不小於指定值的數字 |
@Digits(integer,fraction) | 限制必須為一個小數,且整數部分的位數不能超過integer,小數部分的位數不能超過fraction |
@Future | 限制必須是一個將來的日期 |
@Max(value) | 限制必須為一個不大於指定值的數字 |
@Min(value) | 限制必須為一個不小於指定值的數字 |
@Past | 限制必須是一個過去的日期 |
@Pattern(value) | 限制必須符合指定的正則表達式 |
@Size(max,min) | 限制字符長度必須在min到max之間 |
@Past | 驗證註解的元素值(日期類型)比當前時間早 |
@NotEmpty | 驗證註解的元素值不為null且不為空(字符串長度不為0、集合大小不為0) |
@NotBlank | 驗證註解的元素值不為空(不為null、去除首位空格後長度為0),不同於@NotEmpty,@NotBlank隻應用於字符串且在比較時會去除字符串的空格 |
驗證註解的元素值是Email,也可以通過正則表達式和flag指定自定義的email格式 |
除此之外還可以自定義驗證信息的要求,例如下面的 @MyConstraint:
public class User { private String id; @MyConstraint(message = "這是一個測試") private String username; }
註解的具體內容:
@Constraint(validatedBy = {MyConstraintValidator.class}) @Target({ELementtype.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface MyConstraint { String message(); Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
下面是校驗器:
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> { @Autowired private UserService userService; @Override public void initialie(@MyConstraint constarintAnnotation) { System.out.println("my validator init"); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { userService.getUserByUsername("seina"); System.out.println("valid"); return false; } }
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- Spring超詳細講解註解開發
- 詳解Spring中@Valid和@Validated註解用法
- Spring詳解使用註解開發流程
- Spring五大類註解讀取存儲Bean對象的方法
- 詳解spring如何使用註解開發