一次踩坑记录 @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,所以不需要添加。

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:annotation-driven>标签时,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)处解析了<mvc:annotation-driven>中的validator属性,并将获取到的validator赋值给RequestMappingHandlerAdapter中的webBindingInitializer中的validator属性。

获取validator的方法如下所示

这里的逻辑是,如果<mvc:annotation-driven>标签里有配置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注解的方法参数的校验。

@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
@AssertFalse 限制必须为false
@AssertTrue 限制必须为true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@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 验证注解的元素值是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<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;
    }
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • SpringMvc @Valid如何抛出拦截异常

    SpringMvc中,校验参数可以使用 @Valid 注解,同时在相应的对象里使用 @NotBlank( message = "昵称不能为空") @NotNull( message = "ID不能为空") @Pattern( message = "不能包括空格" , regexp = "\\S+" ) 等等. 这个校验会把所有的参数都校验一遍,所以它的异常里会好些列表,直接使用e.getMessage(),会输出很多累赘的东西

  • Spring的@Validation和javax包下的@Valid区别以及自定义校验注解

    1.后台参数校验 Spring Validation验证框架对参数的验证机制提供了@Validated(Spring JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果 spring提供的验证:org.springframework.validation.annotation.Validated; javax提供的验证:javax.validation.Valid; 在检验Contr

  • @Validated验证List集合的方法示例

    在开发时发现,@Validated只能验证单个实体类,在验证List集合时则不生效 @PostMapping(value="/test") public ApiResult getRepaymentPlan(@RequestBody @Validated List<Test> repaymentPlanVOs){ } 经过查资料得知,@valid是可以使用的,我们自定义一个ValidList类来替换List就可以达到验证的目的,话不多说,直接看代码 public class

  • 浅谈spring方法级参数校验(@Validated)

    依赖的jar包: spring相关jar包版本:4.3.1.RELEASE <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.1.3.Final</version> </dependency> 一.配置与注入 MethodValidationPostProce

  • spring @Validated 注解开发中使用group分组校验的实现

    之前知道spring支持JSR校验,在自己定义的bean中加入@NotNull,@NotBlank,@Length等之类的校验用于处理前台传递过来的request请求,避免在写多余的代码去处理. 但是随着业务的复杂度增加,对于校验的制定也越来越有要求,这个时候就需要引入分组group的概念,在自定义注解@Validated中 定义了一个Class[]数组用来分组.这样我们就可以引入分组校验的概念,首先根据需要的分组新建自己的接口. 然后在需要校验的bean上加入分组: 最后根据需要,在Contr

  • SpringBoot @Validated注解实现参数分组校验的方法实例

    前言 在前后端分离开发的时候我们需要用到参数校验,前端需要进行参数校验,后端接口同样的也需要,以防传入不合法的数据. 1.首先还是先导包,导入pom文件. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> 2.解释一下注解的作用 @N

  • 一次踩坑记录 @valid注解不生效 排查过程

    一.背景 在进行一次Controller层单测时,方法参数违反Validation约束,发现却没有抛出预期的[违反约束]异常. 方法参数上的@Valid注解不生效?? 但是以Tomcatweb容器方式启动,请求该API,@Valid注解却生效了,甚是怪异. 代码如下: @RestController @RequestMapping("/api/user/") public class UserController @RequestMapping(value = ""

  • Spring Boot统一返回体的踩坑记录

    前言 在Spring Boot项目中我们可以通过RestControllerAdvice配合实现ResponseBodyAdvice<T>接口来保证Spring MVC接口具有统一的返回格式,以保证前端同学能够封装统一的数据接收工具.但是很多网上的文章并没有对实际开发中的细节作出更多的讲解.今天胖哥就来分享一下我的采坑经历,也算作一个总结. 控制作用范围 我记得在前面关于Swagger3的文章中提过,如果我们不指定范围将导致Swagger无法识别接口的元信息.因此如果你使用了Swagger必须

  • Spring Data JPA踩坑记录(@id @GeneratedValue)

    目录 Spring Data JPA踩坑记录 JPA踩坑:No property xxx found for type xxx 问题发现 问题解决 Spring Data JPA踩坑记录 最近在做自己的一个项目时 使用了spring jpa 由于数据库用的是mysql 在给实体类entity 的id给注解时@Id遇到了一个坑 自己找了许久才在stackoverflow 上找到了答案 注意 再查询数据库的时候并不会因此报错 而当你的主键是自增的时候 在添加数据的时候就会报错了 看看我们的实体类 注

  • .net core 3.1在iis上发布的踩坑记录

    前言 写这篇文章的目的是希望像我一样喜欢.net 的人在发布 core到 iis上时少走点弯路 网上找了些资料,其实实际操作比较简单,就是有几个坑很恶心 踩坑记录 首先是你的服务器需要有core 的运行环境,安装前先关闭iis dotnet-hosting-3.1.4-win.exe 可以去微软的官网找最新的版本(去微软的官网找你要的版本就好了) 安装成功后,第一个坑出现了,启动iis,发现原来在iis上的网站都报503错误了. 直接玩大了,最后发现就是这个东西搞的鬼,你卸载它iis之前的网站就

  • Linux/Docker 中使用 System.Drawing.Common 踩坑记录分享

    前言 在项目迁移到 .net core 上面后,我们可以使用 System.Drawing.Common 组件来操作 Image,Bitmap 类型,实现生成验证码.二维码,图片操作等功能.System.Drawing.Common 组件它是依赖于 GDI+ 的,然后在 Linux 上并没有 GDI+,面向谷歌编程之后发现,Mono 团队使用 C语言 实现了GDI+ 接口,提供对非Windows系统的 GDI+ 接口访问能力,这个应该就是libgdiplus.所以想让代码在 linux 上稳定运

  • Java踩坑记录之Arrays.AsList

    前言 java.util.Arrays的asList方法可以方便的将数组转化为集合,我们平时开发在初始化ArrayList时使用的比较多,可以简化代码,但这个静态方法asList()有几个坑需要注意: 一. 如果对集合使用增加或删除元素的操作将会报错 如下代码: List list = Arrays.asList("a","b","c"); list.add("d"); 输出结果: Exception in thread &q

  • Java踩坑记录之BigDecimal类

    前言 在java.math包中提供了对大数字的操作类,用于进行高精确计算,如BigInteger,BigDecimal类.而平常我们开发中使用最多的float和double只能适用于一般的科学和工程计算,如果要在比较精确的计算方面如货币,那么使用float和double会相应的丢失精度,因此用于精密计算大数字的类BigDecimal就必不可少了.所以BigDecimal适合商业计算场景,用来对超过16位有效位的数进行精确的运算.但是BigDecimal的使用并不像float和double那样,使

  • 关于python scrapy中添加cookie踩坑记录

    问题发现: 前段时间项目中,为了防止被封号(提供的可用账号太少),对于能不登录就可以抓取的内容采用不带cookie的策略,只有必要的内容才带上cookie去访问. 本来想着很简单:在每个抛出来的Request的meta中带上一个标志位,通过在CookieMiddleware中查看这个标志位,决定是否是给这个Request是否装上Cookie. 实现的代码大致如下: class CookieMiddleware(object): """ 每次请求都随机从账号池中选择一个账号去访

  • python中remove函数的踩坑记录

    摘要: 在python的使用过程中,难免会遇到要移除列表中对象的要求.这时可以使用remove函数. 对于python中的remove()函数,官方文档的解释是:Remove first occurrence of value.大意也就是移除列表中等于指定值的第一个匹配的元素. 语法 list.remove() 参数 obj 参数:从列表中删除的对象的索引 返回值 删除后不会返回值 常见用法: a = [1,2,3,4],a.remove(1),然后a就是[2,3,4]:对于a = [1,1,1

  • Echarts在Taro微信小程序开发中的踩坑记录

    背景 近期笔者在使用Taro进行微信小程序开发,当引入Echarts图表库时,微信检测单包超限2M的一系列优化措施的踩坑记录,期望能指导读者少走一些弯路. 为什么选择Echarts? 微信小程序目录市面上使用最多的两款图表库,如下: echarts-for-weixin--echarts微信小程序版本 wx-charts--基于微信小程序的图表库 对比两款图表库优缺点刚好相反. echarts-for-weixin:功能强大,但体积非常大 wx-charts:功能相对简单,但体积小 由于笔者对e

随机推荐