⼀次踩坑记录@valid注解不⽣效排查过程
⼀、背景
在进⾏⼀次Controller层单测时,⽅法参数违反Validation约束,发现却没有抛出预期的【违反约束】异常。
⽅法参数上的@Valid注解不⽣效??
但是以Tomcatweb容器⽅式启动,请求该API,@Valid注解却⽣效了,甚是怪异。
代码如下:
@RestController
@RequestMapping("/api/ur/")
public class UrController
@RequestMapping(value = "")
public Respon test(@RequestBody @Valid Ur ur) {
...
}
}
其中Test对象如下所⽰
@Data
public class Ur {
@NotNull(message = "⽤户名称不能为空!")
private String name;
}
单元测试代码如下,注意:这⾥的ur对象并没有设置name属性。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:/config/l",
"classpath:/config/l"
})
@Transactional
@Commit
public class UrControllerTest {
@Autowired
private UrController controller;
@Test
public void test(){
}
}
以上UrControllerTest在进⾏测试的时候并未抛出参数校验ConstraintViolationException的异常。
下⾯是mvc配置⽂件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="/schema/beans"
xmlns:xsi="/2001/XMLSchema-instance"
xsi:schemaLocation="/schema/beans /schema/beans/spring-beans.xsd">
<context:component-scan ba-package="dp" u-default-filters="fal">
<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即可,@e <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.ine.ValidatorFactoryImpl#getValidator打上断点,执⾏Controller层API调⽤,看是谁调⽤的该⽅法,进⽽执⾏参数校验的。
结果发现是由HandlerMethodArgumentResolver(该接⼝的作⽤是对HandlerMethod的⽅法参数进⾏校验、解析、转换等⼯作)的实现类RequestResponBodyMethodProcessor调⽤的。
RequestResponBodyMethodProcessor类会转发给WebDataBinder类,由WebDataBinder最终委托给真正的Validator执⾏参数校验。如下所⽰:
下⾯是整体的调⽤链路:
继⽽使⽤之前的UrControllerTest类进⾏测试,发现执⾏路径并不是如此,没有进DispatcherServlet类。
问题到此明了了,是因为测试的姿势不太对,我们应该使⽤Mock mvc的⽅式去进⾏测试,这样的话就会mock出⼀个mvc环境,路由到RequestResponBodyMethodProcessor(标记@RequestBody或者@ResponBody注解的参数解析器)进⾏处理,最终执⾏到⽅法参数校验的逻辑。
3.解决⽅案
修改后的测试代码如下所⽰,这样测试返回的结果是符合预期的,【违反约束】的异常信息被封装在了MvcResult的respon字段中了。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:/config/l",
"classpath:/config/l"
})
@Transactional
@Commit
@WebAppConfiguration
@EnableWebMvc
public class UrControllerTest {
@Autowired
private WebApplicationContext context;
private MockMvc mockMVC;
@Before
public void initMockMvc() {
mockMVC = MockMvcBuilders.webAppContextSetup(context).build();
}
@Test
public void testPage() throws Exception {
String urJson = new Gson().toJson(new Ur());
MvcResult mvcResult = mockMVC.perform(MockMvcRequestBuilders.post("/api/ur").contentType(MediaType.APPLICATION_JSON).content(urJson)).andReturn(); System.out.Respon());
}
}
三、Controller 层@Valid注解原理探究
众所周知,spring mvc XML⽂件中如果配置了<mvc:annotation-driven>标签时,annotation-driven标签将会使⽤MvcNamespaceHandler中的org.springframework.fig.AnnotationDrivenBeanDefinitionParr解析器进⾏解析。
MVC xml handler类如下:
public class MvcNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParr("annotation-driven", new AnnotationDrivenBeanDefinitionParr());
registerBeanDefinitionParr("default-rvlet-handler", new DefaultServletHandlerBeanDefinitionParr());
registerBeanDefinitionParr("interceptors", new InterceptorsBeanDefinitionParr());
registerBeanDefinitionParr("resources", new ResourcesBeanDefinitionParr());
registerBeanDefinitionParr("view-controller", new ViewControllerBeanDefinitionParr());
registerBeanDefinitionParr("redirect-view-controller", new ViewControllerBeanDefinitionParr());
registerBeanDefinitionParr("status-controller", new ViewControllerBeanDefinitionParr());
registerBeanDefinitionParr("view-resolvers", new ViewResolversBeanDefinitionParr());
registerBeanDefinitionParr("tiles-configurer", new TilesConfigurerBeanDefinitionParr());
registerBeanDefinitionParr("freemarker-configurer", new FreeMarkerConfigurerBeanDefinitionParr());
registerBeanDefinitionParr("velocity-configurer", new VelocityConfigurerBeanDefinitionParr());
registerBeanDefinitionParr("groovy-configurer", new GroovyMarkupConfigurerBeanDefinitionParr());
registerBeanDefinitionParr("script-template-configurer", new ScriptTemplateConfigurerBeanDefinitionParr());
registerBeanDefinitionParr("cors", new CorsBeanDefinitionParr());
}
}
org.springframework.fig.AnnotationDrivenBeanDefinitionParr解析器主要是向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最终会在RequestResponBodyMethodProcessor执⾏参数解析,创建WebDataBinder类时被赋值给WebDataBinder的validators属性(准确来说,应该是作为validators的⼀项)。
在RequestResponBodyMethodProcessor#validateIfApplicable⽅法中执⾏校验逻辑。binder.validate其实会路由给binder的validators执⾏校验。
这⾥的validators是spring的⼀个抽象,最终会转发给真实的validator(也就是配置的providerClass 类)执⾏参数校验。