一次 @valid注解不生效 排查过程

目录

    • 一、背景
    • 二、解决过程
      • 1.测试过程
      • 2.原因探究
      • 3.解决方案
    • 三、Controller 层@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,所以不需要添加。(这就是以Tomcat方式启动没有报错的原因)

2.原因探究

众所周知,Spring Validation只是一个抽象,真正执行参数校验的是hibernate validator,既然以Tomcat的方式能够生效。那么我们的办法:以debug的方式启动Tomcat,在org.hibernate.validator.internal.engine.ValidatorFactoryImpl#getValidator打上断点,执行Controller层API调用,看是谁调用的该方法,进而执行参数校验的。

结果发现是由HandlerMethodArgumentResolver该接口的作用是对HandlerMethod的方法参数进行校验、解析、转换等工作)的实现类RequestResponseBodyMethodProcessor调用的。

RequestResponseBodyMethodProcessor类会转发给WebDataBinder类,由WebDataBinder最终委托给真正的Validator执行参数校验。如下所示:

下面是整体的调用链路:
一次 @valid注解不生效 排查过程_第1张图片
继而使用之前的UserControllerTest类进行测试,发现执行路径并不是如此,没有进DispatcherServlet类。

问题到此明了了,是因为测试的姿势不太对,我们应该使用Mock mvc的方式去进行测试,这样的话就会mock出一个mvc环境,路由到RequestResponseBodyMethodProcessor(标记@RequestBody或者@ResponseBody注解的参数解析器)进行处理,最终执行到方法参数校验的逻辑。

3.解决方案

修改后的测试代码如下所示,这样测试返回的结果是符合预期的,【违反约束】的异常信息被封装在了MvcResultresponse字段中了。

@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文件中如果配置了标签时,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,分别是RequestMappingHandlerMappingRequestMappingHandlerAdapterExceptionHandlerExceptionResolver,代码如下所示:

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类时被赋值给WebDataBindervalidators属性(准确来说,应该是作为validators的一项)。
一次 @valid注解不生效 排查过程_第2张图片
RequestResponseBodyMethodProcessor#validateIfApplicable方法中执行校验逻辑。binder.validate其实会路由给bindervalidators执行校验。
这里的validators是spring的一个抽象,最终会转发给真实的validator(也就是配置的providerClass 类)执行参数校验。

至此完成了标注@RequestBody注解的方法参数的校验。

你可能感兴趣的:(Java)