【Java后端】一文讲透Java数据校验(Bean Validation)本文全方位的介绍了Java数据校验相关内容,讲
在开发过程中经常用到 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 额外提供的注解清单,官方文档如下:
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#validateParameters
和ExecutableValidator#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
参数中的约束校验都会生效!
奇了怪了,为啥呢?不是说需要在类上使用@Validated
Spring 才会自动进行约束校验吗?先说答案:@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