一次踩坑記錄 @valid注解不生效 排查過程
一、背景
在進(jìn)行一次Controller層單測(cè)時(shí),方法參數(shù)違反Validation約束,發(fā)現(xiàn)卻沒有拋出預(yù)期的【違反約束】異常。
方法參數(shù)上的@Valid注解不生效??
但是以Tomcatweb容器方式啟動(dòng),請(qǐng)求該API,@Valid注解卻生效了,甚是怪異。
代碼如下:
@RestController
@RequestMapping("/api/user/")
public class UserController
@RequestMapping(value = "")
public Response test(@RequestBody @Valid User user) {
...
}
}
其中Test對(duì)象如下所示
@Data
public class User {
@NotNull(message = "用戶名稱不能為空!")
private String name;
}
單元測(cè)試代碼如下,注意:這里的user對(duì)象并沒有設(shè)置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在進(jìn)行測(cè)試的時(shí)候并未拋出參數(shù)校驗(yàn)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.測(cè)試過程
在執(zhí)行單元測(cè)試的時(shí)候首先暴露出的問題是缺少EL的jar包,因?yàn)镠ibernate validater執(zhí)行會(huì)依賴EL的jar包。引入對(duì)應(yīng)的jar即可,@see EL依賴
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>3.0.3</version>
</dependency>
web容器默認(rèn)會(huì)引這個(gè)jar,所以不需要添加。
2.原因探究
眾所周知,Spring Validation只是一個(gè)抽象,真正執(zhí)行參數(shù)校驗(yàn)的是hibernate validator,既然以Tomcat的方式能夠生效。那么我們的辦法:以debug的方式啟動(dòng)Tomcat,在org.hibernate.validator.internal.engine.ValidatorFactoryImpl#getValidator打上斷點(diǎn),執(zhí)行Controller層API調(diào)用,看是誰調(diào)用的該方法,進(jìn)而執(zhí)行參數(shù)校驗(yàn)的。
結(jié)果發(fā)現(xiàn)是由HandlerMethodArgumentResolver(該接口的作用是對(duì)HandlerMethod的方法參數(shù)進(jìn)行校驗(yàn)、解析、轉(zhuǎn)換等工作)的實(shí)現(xiàn)類RequestResponseBodyMethodProcessor調(diào)用的。
RequestResponseBodyMethodProcessor類會(huì)轉(zhuǎn)發(fā)給WebDataBinder類,由WebDataBinder最終委托給真正的Validator執(zhí)行參數(shù)校驗(yàn)。
如下所示:

下面是整體的調(diào)用鏈路:

繼而使用之前的UserControllerTest類進(jìn)行測(cè)試,發(fā)現(xiàn)執(zhí)行路徑并不是如此,沒有進(jìn)DispatcherServlet類。
問題到此明了了,是因?yàn)闇y(cè)試的姿勢(shì)不太對(duì),我們應(yīng)該使用Mock mvc的方式去進(jìn)行測(cè)試,這樣的話就會(huì)mock出一個(gè)mvc環(huán)境,路由到RequestResponseBodyMethodProcessor(標(biāo)記@RequestBody或者@ResponseBody注解的參數(shù)解析器)進(jìn)行處理,最終執(zhí)行到方法參數(shù)校驗(yàn)的邏輯。
3.解決方案
修改后的測(cè)試代碼如下所示,這樣測(cè)試返回的結(jié)果是符合預(yù)期的,【違反約束】的異常信息被封裝在了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>標(biāo)簽時(shí),annotation-driven標(biāo)簽將會(huì)使用MvcNamespaceHandler中的org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser解析器進(jìn)行解析。
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容器中注冊(cè)了幾個(gè)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>標(biāo)簽里有配置validator屬性,將會(huì)使用該屬性引用的validator bean作為檢驗(yàn)器執(zhí)行參數(shù)校驗(yàn),否則會(huì)判斷classpath下是否存在JSR validator類,如果存在,將會(huì)使用FactoryBean的方式創(chuàng)建默認(rèn)的OptionalValidatorFactoryBean。

這個(gè)validator最終會(huì)在RequestResponseBodyMethodProcessor執(zhí)行參數(shù)解析,創(chuàng)建WebDataBinder類時(shí)被賦值給WebDataBinder的validators屬性(準(zhǔn)確來說,應(yīng)該是作為validators的一項(xiàng))。

在RequestResponseBodyMethodProcessor#validateIfApplicable方法中執(zhí)行校驗(yàn)邏輯。binder.validate其實(shí)會(huì)路由給binder的validators執(zhí)行校驗(yàn)。
這里的validators是spring的一個(gè)抽象,最終會(huì)轉(zhuǎn)發(fā)給真實(shí)的validator(也就是配置的providerClass 類)執(zhí)行參數(shù)校驗(yàn)。

至此完成了標(biāo)注@RequestBody注解的方法參數(shù)的校驗(yàn)。
@Valid注解是什么
@Valid
用于驗(yàn)證注解是否符合要求,直接加在變量user之前,在變量中添加驗(yàn)證信息的要求,當(dāng)不符合要求時(shí)就會(huì)在方法中返回message 的錯(cuò)誤提示信息。
@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 類中添加驗(yàn)證信息的要求:
public class User {
private String id;
@NotBlank(message = "密碼不能為空")
private String password;
}
@NotBlank 注解所指的 password 字段,表示驗(yàn)證密碼不能為空,如果為空的話,上面 Controller 中的 create 方法會(huì)將message 中的"密碼不能為空"返回。
當(dāng)然也可以添加其他驗(yàn)證信息的要求:
| 限制 | 說明 |
|---|---|
| @Null | 限制只能為null |
| @NotNull | 限制必須不為null |
| @AssertFalse | 限制必須為false |
| @AssertTrue | 限制必須為true |
| @DecimalMax(value) | 限制必須為一個(gè)不大于指定值的數(shù)字 |
| @DecimalMin(value) | 限制必須為一個(gè)不小于指定值的數(shù)字 |
| @Digits(integer,fraction) | 限制必須為一個(gè)小數(shù),且整數(shù)部分的位數(shù)不能超過integer,小數(shù)部分的位數(shù)不能超過fraction |
| @Future | 限制必須是一個(gè)將來的日期 |
| @Max(value) | 限制必須為一個(gè)不大于指定值的數(shù)字 |
| @Min(value) | 限制必須為一個(gè)不小于指定值的數(shù)字 |
| @Past | 限制必須是一個(gè)過去的日期 |
| @Pattern(value) | 限制必須符合指定的正則表達(dá)式 |
| @Size(max,min) | 限制字符長(zhǎng)度必須在min到max之間 |
| @Past | 驗(yàn)證注解的元素值(日期類型)比當(dāng)前時(shí)間早 |
| @NotEmpty | 驗(yàn)證注解的元素值不為null且不為空(字符串長(zhǎng)度不為0、集合大小不為0) |
| @NotBlank | 驗(yàn)證注解的元素值不為空(不為null、去除首位空格后長(zhǎng)度為0),不同于@NotEmpty,@NotBlank只應(yīng)用于字符串且在比較時(shí)會(huì)去除字符串的空格 |
| 驗(yàn)證注解的元素值是Email,也可以通過正則表達(dá)式和flag指定自定義的email格式 |
除此之外還可以自定義驗(yàn)證信息的要求,例如下面的 @MyConstraint:
public class User {
private String id;
@MyConstraint(message = "這是一個(gè)測(cè)試")
private String username;
}
注解的具體內(nèi)容:
@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 {};
}
下面是校驗(yàn)器:
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;
}
}
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
淺談java如何實(shí)現(xiàn)Redis的LRU緩存機(jī)制
今天給大家?guī)淼氖顷P(guān)于Java的相關(guān)知識(shí),文章圍繞著java如何實(shí)現(xiàn)Redis的LRU緩存機(jī)制展開,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06
SSH框架網(wǎng)上商城項(xiàng)目第29戰(zhàn)之使用JsChart技術(shù)顯示商品銷售報(bào)表
這篇文章主要為大家詳細(xì)介紹了SSH框架網(wǎng)上商城項(xiàng)目第29戰(zhàn)之使用JsChart技術(shù)顯示商品銷售報(bào)表,感興趣的小伙伴們可以參考一下2016-06-06
SpringBoot+Mybatis使用Mapper接口注冊(cè)的幾種方式
本篇博文中主要介紹是Mapper接口與對(duì)應(yīng)的xml文件如何關(guān)聯(lián)的幾種姿勢(shì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-07-07
eclipse創(chuàng)建項(xiàng)目沒有dynamic web的解決方法
最近上課要用到eclipse,要用到Dynamic web project.但是我下載的版本上沒有.接下來就帶大家了解 eclipse創(chuàng)建項(xiàng)目沒有dynamic web的解決方法,文中有非常詳細(xì)的圖文示例,需要的朋友可以參考下2021-06-06
Java中ThreadLocal線程變量的實(shí)現(xiàn)原理
本文主要介紹了Java中ThreadLocal線程變量的實(shí)現(xiàn)原理,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
Java實(shí)現(xiàn)注冊(cè)登錄跳轉(zhuǎn)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)注冊(cè)登錄跳轉(zhuǎn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06

