likes
comments
collection
share

【Java后端】一文讲透Java数据校验(Bean Validation)本文全方位的介绍了Java数据校验相关内容,讲

作者站长头像
站长
· 阅读数 25

在开发过程中经常用到 Bean Validation 相关的注解进行参数校验,但是对于 JSR-303、Spring Validation、hibernate validator 这些概念有些模糊,没有形成体系。于是查阅相关资料,写下本篇文章。本文将会带你全面梳理 Bean Validation 相关概念,搞清楚它们之间的关系。

1. 简介

后端开发场景,对参数进行校验是常见且必须做的一件事。通常来说从 Controller、Service 到 DAO 层都需要进行数据的校验。在每一层手工编码验证参数费时费力,而且会让代码看起来非常乱。

Java Bean Validation 正是用来解决这个问题的,通过注解实现声明式的参数校验,极大的简化业务代码。

1.1. JSR-303 和 Hibernate Validator

JSR-303 是一套关于 Java Bean 数据验证的 Java 规范,定义了参数校验相关的 API 及注解(@Valid@NotNull@NotEmpty等其它 Constraint 相关注解),并未给出具体实现。具体的实现需要通过 SPI 的方式加载;

Hibernate Validator 是 JSR-303 规范的一种实现,对 JSR 中定义的 Constraint 注解和 API 给出了相应实现。除此之外,Hibernate Validator 还额外提供了一些 Constraint 注解(比如@CreditCardNumber@DurationMax等等)。

JSR 中定义的注解这里不多赘述了,可以参考文档:docs.jboss.org/hibernate/s…

Hibernate Validator 额外提供的注解清单,官方文档如下:

docs.jboss.org/hibernate/s…

1.2. Spring Bean Validation

Spring Bean Validation 是 Spring 框架对 Java Bean Validation(JSR 303)规范的集成和增强。可以更加方便的进行数据校验,不需要再通过 Java Bean Validation API 手动进行校验了。

具体来说,Spring 提供了@Validated注解,只需要在类上使用这个注解就可以实现自动数据校验。

2. 从 Java Bean Validation 到 Spring Bean Validation

本节首先演示了使用原生 Java Bean Validation API(即 JSR 303)手动进行数据校验(校验 Java Bean 以及校验方法入参/返回),直观感受使用 JSR 校验数据的过程;然后再介绍通过 Spring Validation 进行自动化参数校验及其实现原理。

2.1. Bean Validation 使用示例

本小节基于 SpringBoot 准备了一些测试案例,展示了如何通过编码方式基于 validator 实现 bean 校验

需要用到 pom 依赖如下:

<!--hibernate-validator是 JSR 的参考实现-->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
</dependency>

说明:JSR 303 规范只给出了参数校验相关接口和注解,并未给出具体实现。具体的参数校验实现需要通过 SPI 的方式与之整合。hibernate-validator 对 JSR 参数校验规范进行了相应的实现。

2.1.1. 校验普通 bean

Validator 用于校验普通的 bean 对象,基本使用如下:

@SpringBootTest
public class ValidatorTest {
    
    @Test
    void testValidator() {
        // 1. 准备一个bean
        User user = new User("", 1, Lists.newArrayList("label1", ""));
        
        // 2. 获取validator
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        Validator validator = validatorFactory.getValidator();
        
        // 3. 对bean进行校验
        Set<ConstraintViolation<User>> constraintViolations = validator.validate(user);
        
        // 4. 打印校验信息
        for (ConstraintViolation<User> constraintViolation : constraintViolations) {
            System.out.println(constraintViolation);
        }
        // 控制台输出如下: 
        // ConstraintViolationImpl{interpolatedMessage='name不能为空', propertyPath=name, rootBeanClass=class com.awesome.demo.validation.ValidatorTest$User, messageTemplate='name不能为空'}
        // ConstraintViolationImpl{interpolatedMessage='List中的标签值不能为空哦', propertyPath=labels[1].<list element>, rootBeanClass=class com.awesome.demo.validation.ValidatorTest$User, messageTemplate='List中的标签值不能为空哦'}
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class User {
        @NotBlank(message = "name不能为空")
        private String name;
        
        @Min(value = 1, message = "最小为1")
        @Max(value = 150, message = "最大为150")
        private Integer age;
        
        @NotEmpty(message = "标签集合不能为空")
        private List<@NotBlank(message = "List中的标签值不能为空哦") String> labels;
    }
}

说明:Set<ConstraintViolation<User>> constraintViolations = validator.validate(user); 中拿到的 constraintViolations 表示校验结果,每个不符合预期的参数都会对应一个 ConstraintViolation 实例。

若 constraintViolations 为空,则说明校验通过;反之则校验不通过。

注意,如果 bean 中存在嵌套属性,并且嵌套在内部的属性也需要被校验,则需要在属性上使用 Valid 注解:

@Data
public class OuterBean {
    @NotBlank(message = "name cannot be blank")
    private String name;
    // 想要校验innerBean里的属性,就需要@Valid注解
    @Valid
    private InnerBean innerBean;
}

@Data
public static class InnerBean {
   @NotBlank(message = "description cannot be blank")
   @Size(max = 200, message = "description should be less than 200 characters")
   private String description;
}

2.1.2. 分组校验

开发过程中,如果不同场景下复用一个 Bean,需要采用不同的校验方式,此时分组校验就派上用场了。

比如,UserVO 在新增的时候不需要校验 id(testValidateWhenInsert),更新时候需要对 id 进行校验(testValidateWhenUpdate):

@SpringBootTest
public class ValidateGroupTest {

    // 模拟Update场景校验
    @Test
    public void testValidateWhenUpdate() {
        // 1. 准备一个bean
        UserVO user = new UserVO(0L, "", 0, Lists.newArrayList("label1", ""));
        
        // 2. 获取validator
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        Validator validator = validatorFactory.getValidator();
        
        // 3. 对bean进行校验 分组为UpdateUserRequest.class
        Set<ConstraintViolation<UserVO>> constraintViolations = validator.validate(user, UserVO.UpdateUserRequest.class);
        
        // 4. 打印校验结果
        for (ConstraintViolation<UserVO> constraintViolation : constraintViolations) {
            System.out.println(constraintViolation);
        }
        // 控制台输出:
        // ConstraintViolationImpl{interpolatedMessage='id必须大于0', propertyPath=id, rootBeanClass=class com.awesome.demo.validation.ValidateGroupTest$UserVO, messageTemplate='id必须大于0'}
    }

    // 模拟Insert场景校验
    @Test
    public void testValidateWhenInsert() {
        // 1. 准备一个bean
        UserVO user = new UserVO(null, "", 0, Lists.newArrayList("label1", ""));
        
        // 2. 获取validator
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        Validator validator = validatorFactory.getValidator();
        
        // 3. 对bean进行校验 分组为InsertUserRequest.class
        Set<ConstraintViolation<UserVO>> constraintViolations = validator.validate(user, UserVO.InsertUserRequest.class);
        
        // 4. 打印校验结果
        for (ConstraintViolation<UserVO> constraintViolation : constraintViolations) {
            System.out.println(constraintViolation);
        }
        // 控制台输出:
        // ConstraintViolationImpl{interpolatedMessage='name不能为空', propertyPath=name, rootBeanClass=class com.awesome.demo.validation.ValidateGroupTest$UserVO, messageTemplate='name不能为空'}
        // ConstraintViolationImpl{interpolatedMessage='最小为1', propertyPath=age, rootBeanClass=class com.awesome.demo.validation.ValidateGroupTest$UserVO, messageTemplate='最小为1'}
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class UserVO {
        
        @NotNull(message = "id不能为空", groups = UpdateUserRequest.class)
        @Min(value = 1, message = "id必须大于0", groups = UpdateUserRequest.class)
        private Long id;
        
        @NotBlank(message = "name不能为空", groups = InsertUserRequest.class)
        private String name;
        
        @Min(value = 1, message = "最小为1", groups = {InsertUserRequest.class, UpdateUserRequest.class})
        @Max(value = 150, message = "最大为150", groups = {InsertUserRequest.class, UpdateUserRequest.class})
        @NotNull(message = "年龄不能为空", groups = InsertUserRequest.class)
        private Integer age;
        
        @NotEmpty(message = "标签集合不能为空", groups = InsertUserRequest.class)
        private List<@NotBlank(message = "List中的标签值不能为空哦") String> labels;
        
        private interface InsertUserRequest{}
        private interface UpdateUserRequest{}
    }
}

2.1.3. 校验方法入参/返回

ExecutableValidator 用于对方法进行校验,可以被校验的内容包括:方法入参和方法返回值

2.1.3.1. 校验方法入参

方法入参通常有两种类型:

  • 第一种是普通入参(基本数据类型 或 String),可以直接在参数上使用 @NotNull@NotEmpty@NotBlank等注解;
  • 第二种是 java bean 类型的入参,如果想校验 java bean 内定义的属性,需要使用@Valid注解;

下面是例子:

【① 普通入参

@SpringBootTest
public class ValidateMethodTest {
    
    @Test
    public void testValidateMethodParams() throws NoSuchMethodException {
        // 1. 准备需要被校验的方法
        DemoService demoService = new DemoService();
        Method methodToBeValidated = demoService.getClass().getDeclaredMethod("process", Long.class);
        Object[] params = new Object[]{null};
        
        // 2. 获取 executableValidator (注: ExecutableValidator用于校验方法)
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        ExecutableValidator executableValidator = validatorFactory.getValidator().forExecutables();

        // 3. 对方法入参进行校验,参数依次为: bean实例、Method对象本身、入参
        Set<ConstraintViolation<DemoService>> constraintViolations =
                executableValidator.validateParameters(demoService, methodToBeValidated, params);
        
        // 4. 打印校验结果
        for (ConstraintViolation<DemoService> constraintViolation : constraintViolations) {
            System.out.println(constraintViolation);
        }
        // 控制台输出:
        // ConstraintViolationImpl{interpolatedMessage='订单号不能为空', propertyPath=process.arg0, rootBeanClass=class com.awesome.demo.validation.ValidateMethodTest$DemoService, messageTemplate='订单号不能为空'}
    }
    
    
    public static class DemoService {
        // 需要校验的方法
        public String process(@NotNull(message = "订单号不能为空") Long orderId) {
            return String.valueOf(orderId);
        }
    }
}

【② 嵌套类型入参

当需要被校验的"方法入参"是被 java bean 封装的时候,需要在参数上使用 @Valid 注解

@SpringBootTest
public class ValidateMethodTest {
    
    @Test
    public void testValidateMethodNestedParams() throws NoSuchMethodException {
        // 1. 准备需要被校验的方法及入参
        DemoService demoService = new DemoService();
        Method methodToBeValidated = demoService.getClass().getDeclaredMethod("process2", DemoRequest.class);
        Object[] params = new Object[]{new DemoRequest(1L, "")};
        
        // 2. 获取 executableValidator
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        ExecutableValidator executableValidator = validatorFactory.getValidator().forExecutables();
        
        // 3. 对方法入参进行校验
        Set<ConstraintViolation<DemoService>> constraintViolations =
                executableValidator.validateParameters(demoService, methodToBeValidated, params);
        
        // 4. 打印校验结果
        for (ConstraintViolation<DemoService> constraintViolation : constraintViolations) {
            System.out.println(constraintViolation);
        }
        // 控制台输出:
        // ConstraintViolationImpl{interpolatedMessage='content不能为空', propertyPath=process2.arg0.content, rootBeanClass=class com.awesome.demo.validation.ValidateMethodTest$DemoService, messageTemplate='content不能为空'}
    }

    
    public static class DemoService {
        // 在需要被校验的java bean上使用@Valid注解
        public void process2(@Valid DemoRequest request){
            // do sth.
        }
    }
    
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class DemoRequest {
        @NotNull(message = "id不能为空")
        private Long id;
        @NotBlank(message = "content不能为空")
        private String content;
    }
}

2.1.3.2. 校验方法返回

@SpringBootTest
public class ValidateMethodTest {

    @Test
    public void testValidateMethodReturnVal() throws NoSuchMethodException {
        // 1. 准备需要被校验的方法
        DemoService demoService = new DemoService();
        Method methodToBeValidated = demoService.getClass().getDeclaredMethod("process1");
        String returnValue = demoService.process1(); // 执行方法,获取返回值
        
        // 2. 获取 executableValidator (注: ExecutableValidator用于校验方法)
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        ExecutableValidator executableValidator = validatorFactory.getValidator().forExecutables();
        
        // 3. 对方法返回进行校验,参数依次为: bean实例、Method对象本身、方法返回值
        Set<ConstraintViolation<DemoService>> constraintViolations =
                executableValidator.validateReturnValue(demoService, methodToBeValidated, returnValue);
        
        // 4. 打印校验结果
        for (ConstraintViolation<DemoService> constraintViolation : constraintViolations) {
            System.out.println(constraintViolation);
        }
        // 控制台输出:
        // ConstraintViolationImpl{interpolatedMessage='返回不能为空', propertyPath=process1.<return value>, rootBeanClass=class com.awesome.demo.validation.ValidateMethodTest$DemoService, messageTemplate='返回不能为空'}
    }
    
    
    public static class DemoService {
        // 校验方法返回
        @NotEmpty(message = "返回值不能为空")
        public String process1() {
            return "";
        }
    }
}

注:如果返回类型是一个 java bean,需要校验内部的属性,同样需要使用 @Valid 注解。

2.1.4. 自定义校验

① 自定义校验器

这里定义一个非常简单的校验器,校验逻辑为:只有参数为 String 类型并且值为"xxx"时才会校验通过。

// MyDemoValidation是我们自定义的校验注解
public class MyDemoConstraintValidator implements ConstraintValidator<MyDemoValidation, Object> {
    
    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        // 只有值为"xxx"才会校验通过,其余情况全部返回false
        if (value instanceof String) {
            String val = (String) value;
            if ("xxx".equals(val)) {
                return true;
            }
        }
        return false;
    }
}

② 自定义校验注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyDemoConstraintValidator.class) // MyDemoConstraintValidator 为自定义校验器
public @interface MyDemoValidation {
    
    String message();
    
    Class<?>[] groups() default {};
    
    Class<? extends Payload>[] payload() default {};
}

注:注解中 message、groups、payload 这三个字段是必须要有的,否则会报错

③ 使用自定义校验器

@SpringBootTest
public class CustomValidateTest {
    
    @Test
    public void testCustomValidate() {
        // 1. 准备bean
        UserVO userVO = new UserVO("abc");
        
        // 2. 获取validator
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        Validator validator = validatorFactory.getValidator();
        
        // 3. 对bean进行校验
        Set<ConstraintViolation<UserVO>> constraintViolations = validator.validate(userVO);
        
        // 4. 打印校验结果
        for (ConstraintViolation<UserVO> constraintViolation : constraintViolations) {
            System.out.println(constraintViolation);
        }
        // 控制台输出:
        // ConstraintViolationImpl{interpolatedMessage='你不是xxx,不通过', propertyPath=name, rootBeanClass=class com.awesome.demo.validation.CustomValidateTest$UserVO, messageTemplate='你不是xxx,不通过'}
    }
    
    @Data
    @AllArgsConstructor
    public static class UserVO {
        @MyDemoValidation(message = "你不是xxx,不通过")
        private String name;
    }
}

2.2. 深入 Spring Bean Validation 自动参数校验

在 2.1 我们分别通过 Validator 和 ExecutableValidator 实现了对 bean 和方法的参数校验。但是,校验过程较为繁琐,尤其是对于方法的参数校验,每次都需要获取 Method 实例、入参等信息。对于这种共性问题,很容易想到使用 AOP 抽离出校验逻辑,简化代码。

Spring 中的 @Validated 注解正是通过 AOP 来实现参数校验的。一般地,我们只需要在类上使用 @Validated 注解,Spring 就会自动帮我们进行 2.1 中的方法数据校验,约束校验不通过会抛出ConstraintViolationException 异常。比如下面的例子:

// DemoService.java
@Service
@Validated // 要点: 类上使用@Validated
public class DemoService {

    public String demoHandler(@Valid DemoReq demoReq) {
        System.out.println(demoReq);
        return "ok";
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class DemoReq {
        
        @NotBlank(message = "字符串不能为空")
        private String id;
        
        @Min(value = 1, message = "最小为1")
        @Max(value = 10, message = "最大为10")
        private Integer count;
        
        @NotEmpty(message = "strList集合不能为空")
        private List<String> strList;
    }
}

// DemoServiceTest.java
@SpringBootTest
public class DemoServiceTest {
    
    @Autowired
    DemoService demoService;
    
    @Test
    public void testDemoHandler() {
        DemoService.DemoReq demoReq = new DemoService.DemoReq(null, 1, null);
        // 异常信息: javax.validation.ConstraintViolationException: demoHandler.demoReq.strList: strList集合不能为空, demoHandler.demoReq.id: 字符串不能为空
        demoService.demoHandler(demoReq);
    }
}

2.2.1. Spring @Validated自动执行参数校验原理

自动数据校验的基本原理是 Spring AOP,相比这一点大家都比较清除了,这里带着大家过一遍用到的相关组件。

SpringBoot 中关于 Spring Validation 的自动配置类如下:

@AutoConfiguration
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	@ConditionalOnMissingBean(Validator.class)
	public static LocalValidatorFactoryBean defaultValidator(ApplicationContext applicationContext) {
		LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
		MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(applicationContext);
		factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
		return factoryBean;
	}

	@Bean
	@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
	public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
			@Lazy Validator validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
		FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(
				excludeFilters.orderedStream());
		boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
		processor.setProxyTargetClass(proxyTargetClass);
		processor.setValidator(validator);
		return processor;
	}

}

这段代码往容器里注册了两个组件:LocalValidatorFactoryBean 和 MethodValidationPostProcessor。

  • LocalValidatorFactoryBean 是一个适配器类,继承自 SpringValidatorAdapter,主要作用是在 Spring 容器中整合 Java Bean Validation(JSR-303 );
  • MethodValidationPostProcessor 是一个 bean 后置处理器,主要作用是给类上有 @Validated 的 bean 创建代理类;

MethodValidationPostProcessor 核心代码如下:

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
		implements InitializingBean {

	private Class<? extends Annotation> validatedAnnotationType = Validated.class;

    // JSR-303 Validator
	@Nullable
	private Validator validator;

    // ......省略setter方法

	@Override
	public void afterPropertiesSet() {
        // 1. 创建基于注解匹配的切点,需要匹配的注解为@Validated
		Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
		// 2. 实例化advisor,父类会使用这个advisor中的pointcut判读是否为bean创建代理类
        this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
	}

	/**
     * 创建切面逻辑,本质是一个拦截器
     * 入参validator是一个JSR-303 Validator实例
	 */
	protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
		return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
	}

}

核心点为:初始化方法中实例化了一个 DefaultPointcutAdvisor 实例,将其赋值给父类 advisor 属性,后续创建代理类的逻辑在其父类AbstractAdvisingBeanPostProcessor#postProcessAfterInitialization中,这里就不展开了。

下面看看拦截器(Advice) MethodValidationInterceptor 中有关方法数据校验的逻辑:

public class MethodValidationInterceptor implements MethodInterceptor {
    // 持有一个 Validator 实例,来自Java Bean Validation API
    private final Validator validator;
	
    @Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		// 避免校验 FactoryBean.getObjectType/isSingleton
		if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
			return invocation.proceed();
		}

		Class<?>[] groups = determineValidationGroups(invocation);

        // 1. 获取 ExecutableValidator 实例,用于校验方法
		ExecutableValidator execVal = this.validator.forExecutables();
		// 2. 获取目标方法
        Method methodToValidate = invocation.getMethod();
		Set<ConstraintViolation<Object>> result;

		try {
            // 3. 执行校验
			result = execVal.validateParameters(
					invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
		}
		catch (IllegalArgumentException ex) {
			// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
			// Let's try to find the bridged method on the implementation class...
			methodToValidate = BridgeMethodResolver.findBridgedMethod(
					ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
			result = execVal.validateParameters(
					invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
		}
        // 4. 如果校验结果非空,抛出异常
		if (!result.isEmpty()) {
			throw new ConstraintViolationException(result);
		}

		Object returnValue = invocation.proceed();

        // 5. 校验返回值
		result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
		// 6. 如果校验结果非空,抛出异常
        if (!result.isEmpty()) {
			throw new ConstraintViolationException(result);
		}

		return returnValue;
	}
}

上述代码校验逻辑与手动调用 ExecutableValidator#validateParametersExecutableValidator#validateReturnValue 校验方法入参和返回的逻辑(2.1.3)一致。

2.2.2. Spring MVC Controller 自动执行参数校验原理

为什么 Controller 的参数校验需要拿出来单独说呢?

因为它比较特殊,特殊的点在于 Spring MVC @RequestBody在进行参数绑定的时候自动做了参数校验,也就是说 Controller 中 @RequestBody的参数校验并不是通过 Srping AOP 去做的。(注意哦,我们这里说的只是@RequestBody!!!不包括其他 Controller 中的参数接收方式)

先来看下面这段代码:

// @Validated
@RestController
public class DemoController {
    // 参数校验生效!
    @PostMapping("/demo/post1")
    public String demoHandler1(@Valid @RequestBody DemoReq demoReq) {
        System.out.println(demoReq);
        return "ok";
    }

    // 参数校验同样生效!
    @PostMapping("/demo/post2")
    public String demoHandler2(@Validated @RequestBody DemoReq demoReq) {
        System.out.println(demoReq);
        return "ok";
    }

    @Data
    @ToString
    @AllArgsConstructor
    @NoArgsConstructor
    public static class DemoReq {
        
        @NotBlank(message = "字符串不能为空")
        private String id;
        
        @Min(value = 1, message = "最小为1")
        @Max(value = 10, message = "最大为10")
        private Integer count;
        
        @NotEmpty(message = "strList集合不能为空")
        private List<String> strList;
    }
}

可以发现一个有意思的现象:Controller 类上没有使用@Validated注解,demoHandler1、demoHandler2入参上无论使用@Validated还是@Valid@RequestBody参数中的约束校验都会生效!

奇了怪了,为啥呢?不是说需要在类上使用@ValidatedSpring 才会自动进行约束校验吗?先说答案:@RequestBody在参数绑定的时候调用 validator 做了约束校验,相关源码如下:

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
    
    // ......省略其余代码
    
    // 处理@RequestBody,解析http请求中的数据并绑定到对象中
    @Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		parameter = parameter.nestedIfOptional();
        // 1. 读取请求中的参数
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
		// 2. 获取参数名
        String name = Conventions.getVariableNameForParameter(parameter);

		if (binderFactory != null) {
			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
			if (arg != null) {
                // 重点!!! 这里会判断参数上有没有注解, 然后通过binder执行参数校验!
				validateIfApplicable(binder, parameter);
                // 参数校验不通过 -> 抛出MethodArgumentNotValidException异常
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
				}
			}
			if (mavContainer != null) {
				mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
			}
		}

		return adaptArgumentIfNecessary(arg, parameter);
	}

    // ......省略其余代码
}

来看下 validateIfApplicable 方法的逻辑:

public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
    // ......省略其余代码
    
    /**
     * Validate the binding target if applicable.
     * 在适用情况下,验证绑定目标对象
	 */
	protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
		Annotation[] annotations = parameter.getParameterAnnotations();
		for (Annotation ann : annotations) {
            // 先看看有没有@Validated注解
			Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
			// 注意看这个判断条件: 有@Validated注解 或 有以"Valid"开头的注解
            if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
				Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
				Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
				binder.validate(validationHints);
				break;
			}
		}
	}
    
    // ......省略其余代码
}

关键的地方在于是否执行校验的判断逻辑:参数上有 @Validated或 有名字以"Valid"开头的注解。所以,只要是Valid这几个字母开头的注解(自定义注解也可)都会触发@RequestBody参数绑定过程中的约束校验!binder.validate(validationHints)内部最终也是通过调用 JSR-303 中的 Validator 实现约束校验的。

到这里,关于 Controller 中@RequestBody入参的校验原理就介绍完了。

3. 总结

本文主要总结了 Java 数据校验相关的概念,介绍了 JSR、Hibernate Validator 和 Spring Validation 之间的关系,然后演示了 Java Bean Validation API 的基本用法(2.1),最后介绍了 Spring 实现自动化数据校验的原理(2.2)。需要特别注意的是,Spring MVC Controller 中针对 @RequestBody入参的校验并不是基于 Spring AOP 实现的,所以只需要在参数前面使用@Validated@Valid即可进行校验(只要是"Valid"这五个字母开头的注解就可以!)。

转载自:https://juejin.cn/post/7409148560778919987
评论
请登录