在进行一次Controller层单测时,方法参数违反Validation约束,发现却没有抛出预期的【违反约束】异常。
方法参数上的@Valid注解不生效??
但是以Tomcat
web容器方式启动,请求该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>
在执行单元测试的时候首先暴露出的问题是缺少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,所以不需要添加。(这就是以Tomcat
方式启动没有报错的原因)
众所周知,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
注解的参数解析器)进行处理,最终执行到方法参数校验的逻辑。
修改后的测试代码如下所示,这样测试返回的结果是符合预期的,【违反约束】的异常信息被封装在了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());
}
}
众所周知,spring mvc XML文件中如果配置了
标签时,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)处解析了
中的validator属性,并将获取到的validator
赋值给RequestMappingHandlerAdapter
中的webBindingInitializer
中的validator
属性。
获取validator
的方法如下所示
这里的逻辑是,如果
标签里有配置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
注解的方法参数的校验。