app开发者平台在数字化时代的重要性与发展趋势解析
1236
2023-01-12
一次踩坑记录 @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 UserControllerTeshttp://t {
@Autowired
private UserController controller;
@Test
public void test(){
controller.test(new User());
}
}
以上UserControllerTest在进行测试的时候并未抛出参数校验ConstraintViolationException的异常。
下面是mvc配置文件:
xmlns:xsi="http://w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://springframework.org/schema/beans http://springframework.org/schema/beans/spring-beans.xsd">
xmlns:xsi="http://w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://springframework.org/schema/beans http://springframework.org/schema/beans/spring-beans.xsd">
二、解决过程
1.测试过程
在执行单元测试的时候首先暴露出的问题是缺少EL的jar包,因为Hibernate validater执行会依赖EL的jar包。引入对应的jar即可,@see EL依赖
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 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最终会在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
http://
@AssertFalse
限制必须为false
@AssertTrue
限制必须为true
@DecimalMax(value)
限制必须为一个不大于指定值的数字
@DecimalMin(value)
限制http://必须为一个不小于指定值的数字
@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
@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;
}
}
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~