Spring数据校验体系


此篇将大致领略一下Spring的数据校验体系及应用,在此之前你需要提前了解Bean Validation

Spring Validation

spring在spring-context中设计了自己的一套校验体系,位于org.springframework.validation包下,与Bean Validation类似,核心也是校验器(org.springframework.validation.Validator

1
2
3
4
5
package org.springframework.validation;
public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Errors errors);
}

与Bean Validation校验器不同,spring校验器显得更抽象些,前者通过约束注解+约束验证器组合校验JavaBean,后者更多是泛化了校验过程。spring校验器通过supports方法声明了校验器支持的类型,提供validate方法进行后续的校验,校验结果通过Errors对象回调保存。

就使用场景来说,spring校验器在框架内部应用的比较多,如spring-mvc数据绑定过程、配置bean的数据绑定等。

下面以数据绑定操作为例

定义普通的java bean与其spring校验器,基础的非空等校验可使用ValidationUtils辅助之。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ... lombok
public class Person {
private String email;
}

// 校验器
public class PersonValidator implements Validator {

@Override
public boolean supports(Class<?> clazz) {
return Person.class.isAssignableFrom(clazz);
}

@Override
public void validate(Object target, Errors errors) {
// 字段名
final String filed = "email";
// 错误码(通常是国际化消息code)
final String errCode = "ws.error-code.person.email";
ValidationUtils.rejectIfEmptyOrWhitespace(errors, filed, errCode, "邮箱地址不能为空");
// 其它自定义实现的校验
String email = (String)errors.getFieldValue(filed);
if (!isEmail(email)) {
// 以error对象写入检验结果,以拒绝xx字段等操作写入
errors.rejectValue(filed, errCode, "邮箱地址无效");
}
}

private boolean isEmail(String email) {
// ... todo
}
}

数据绑定操作使用spring校验器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Person person = new Person();
// 绑定器
DataBinder binder = new DataBinder(person);
// 设置spring校验器
binder.setValidator(new PersonValidator());
// 属性值键值对
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.addPropertyValue("email", "123465ws.com");
// 绑定
binder.bind(pvs);
// 验证绑定对象,使用已经添加的Validator
binder.validate();
// 获取绑定结果
BindingResult result = binder.getBindingResult();
if (result.hasFieldErrors()) {
throw new BindException(result);
} else {
System.out.println(person);
}

显然spring校验器可自定义的能力更宽泛些,校验逻辑实现完全开放。

Spring应用Bean Validation

作为框架来说,spring是要把校验这个基础能力单独封装起来的,引入其它的校验框架时,应该要去适配对接spring的校验体系,达到插件化的效果。故而spring在其Validator的实现中也适配了Bean Validation。

其关键类便是:org.springframework.validation.SmartValidatororg.springframework.validation.beanvalidation.SpringValidatorAdapter

其中SmartValidator重载了validate校验方法,提供了分组校验的可能,即validationHints参数,但此处也是以Object参数化处理。

1
2
3
4
5
6
7
8
package org.springframework.validation;
public interface SmartValidator extends Validator {
void validate(Object target, Errors errors, Object... validationHints);
default void validateValue(
Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) {
throw new IllegalArgumentException("Cannot validate individual value for " + targetType);
}
}

SpringValidatorAdapter更是将Bean Validation适配进来,同时实现两个体系的Validator

1
2
3
4
package org.springframework.validation.beanvalidation;
public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator {
// ...
}

继承关系如图

SpringValidatorAdapter借助Bean Validation的Validator来实现spring的Validator。

因此在前文数据绑定过程中,我们可使用该校验器来使用Bean Validation的能力

此时先给Person类加上约束

1
2
3
4
5
6
// ... lombok
public class Person {
@Email
@Size(max = 30, groups = Group.Insert.class)
private String email;
}

使用SpringValidatorAdapter作为Person的spring校验器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Person person = new Person();
DataBinder binder = new DataBinder(person);
// 构建Bean Validation的校验器,并提供给spring的适配器
javax.validation.Validator javaxValidator = Validation.buildDefaultValidatorFactory()
.getValidator();
SpringValidatorAdapter adapter = new SpringValidatorAdapter(javaxValidator);
// 设置spring校验器
binder.setValidator(adapter);
// 属性值键值对
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.addPropertyValue("email", "123465ws.com");
// 绑定
binder.bind(pvs);
// 验证绑定对象
binder.validate();
// 可分组校验
binder.validate(Group.Insert.class);
// 获取绑定结果
BindingResult result = binder.getBindingResult();
if (result.hasFieldErrors()) {
throw new BindException(result);
} else {
System.out.println(person);
}

到此Bean Validation与Spring Validation的异同与联系介绍完毕。


Spring 校验功能的应用

显然实际场景中spring更多是借助Bean Validation来“成就”自己的校验体系。

开发者在实际开发中若是使用Bean Validation的原始校验操作,需要编写过于重复的代码,spring肯定不会“坐以待毙”,势必进行了封装。那么在日常开发中,spring的校验体系(能力)是如何激活的呢?

核心便是

org.springframework.validation.annotation.Validated注解,它便是开启校验的关键开关。

应用于Spring Bean的装配验证

在SpringBoot中,通常我们在设计配置类时,会有一定约束,此时可以借助spring帮我们进行字段的校验,确保配置类bean的有效。只需要在配置类上加以@Validated注解修饰,spring在创建spring bean过程中,bean初始化之际校验该bean。

1
2
3
4
5
6
7
8
9
@Validated
@ConfigurationProperties(prefix = "custom.config")
public class CustomConfig {

@Email
@NotNull
private String adviceEmail;
// ...
}

应用于Spring Bean的方法验证增强

Bean Validation提供了方法参数和返回值验证的能力,spring整合Bean Validation后将这一步骤通过aop来增强实现。

SpringBoot中Validation的自动装配配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.springframework.boot.autoconfigure.validation;
// ...
public class ValidationAutoConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator() {
// ...
}

@Bean
@ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, @Lazy Validator validator) {
// ...
}
}

其中 LocalValidatorFactoryBean便是实际使用的校验器Validator,MethodValidationPostProcessor 负责处理需要增加方法层面校验的bean,通过aop为其提供校验能力。

基本使用

Spring中@Validated 注解是开启校验的关键。

在SpringBean上使用@Validated 修饰,Spring将会对其进行动态代理,以提供校验方法参数和返回值的能力

1
2
3
4
5
6
7
8
@Validated
@Service
public class SomeService {
@NotNull
public Integer validateBasic(@NotBlank String str) {
// ...
}
}

后续该SpringBean在任一地方使用时,将会校验方法入参与返回值是否满足约束,校验不通过则以ConstraintViolationException异常通知。

分组校验

具有校验能力的SpringBean如何指定校验的组呢?

只需要在@Validated注解中声明即可

1
2
3
@Validated(Group.Update.class) // 声明校验组
@Service
public class SomeService {}

应用于Spring MVC参数绑定的验证

在web-mvc的controller中,在以下参数绑定场景下可以使用@Validated注解对该参数进行校验。

  • @RequestBody绑定

    通过@RequestBody注解将Request-body的内容(通常是json)绑定到java bean时,可使用@Validated开启校验,由于是在方法参数上开启,所以更能灵活的指定分组,如新增接口使用校验Insert组,更新接口使用Update组。

    1
    2
    3
    4
    5
    6
    7
    8
    @RestController
    @RequestMapping("/api/v1")
    public class BeanValidatedController {
    @PostMapping("/person")
    public RestResponse handler(@Validated @RequestBody Person person) {
    // ...
    }
    }
  • @ModelAttribute绑定

    通过@ModelAttribute注解将web视图org.springframework.ui.Model中的对象绑定到参数上时,可支持校验

    1
    2
    3
    4
    5
    6
    7
    8
    @RestController
    @RequestMapping("/api/v2")
    public class BeanValidatedController {
    @PostMapping("/person")
    public RestResponse handler(@Validated @ModelAttribute Person person) {
    // ...
    }
    }
  • @RequestPart绑定(非文件类型MultipartFile

    通过@RequestPart注解将表单中的部分数据绑定到参数上时(如json),可支持校验,MultipartFile不支持校验

    1
    2
    3
    4
    5
    6
    7
    8
    @RestController
    @RequestMapping("/api/v2")
    public class BeanValidatedController {
    @PostMapping("/form")
    public RestResponse handler(@Validated @RequestPart Person person) {
    // ...
    }
    }

上述参数绑定场景的校验能力均来自于org.springframework.web.bind.WebDataBinder,它是web-mvc参数绑定的核心组件。其继承于前文提到的DataBinder

值得注意的是,在web-mvc参数绑定校验中,参数绑定的能力来源于对应的参数解析器HandlerMethodArgumentResolver,对绑定参数的校验逻辑是其内部解析流程之一,通过源码分析可知,上述几种场景的绑定是支持校验的,激活开关为@Validated注解(实际上@Valid注解也是开关,spring也做了对 @Valid注解的支持)

而诸如通过@RequestParam注解绑定String等基础类型时,是不支持校验的

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/api/v2")
public class BeanValidatedController {
// 此处web-mvc不支持对 email 的校验
@PostMapping("/query")
public RestResponse handler(@Validated @RequestParam @Email String email) {
// ...
}
}

如果想要支持上述校验,我们需要退回到《Bean Validation应用于Spring Bean的方法验证增强》的功能点上,对controller开启校验的aop代理增强。

改写如下

1
2
3
4
5
6
7
8
9
10
@Validated // 对spring bean 开启校验的aop代理
@RestController
@RequestMapping("/api/v2")
public class BeanValidatedController {
// 此处web-mvc不支持对 email 的校验
@PostMapping("/query")
public RestResponse handler(@RequestParam @Email String email) {
// ...
}
}

这就意味着当我们对controller加以@Validated 注解修饰时,controller将得到校验的aop代理增强,同时由于web-mvc对参数绑定的校验支持,会同时有两套校验逻辑加在controller上。

如果我们代码如下编写时,即如badHandler方法使用@Valid 修饰参数,会触发两次校验!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Validated // 对spring bean 开启校验的aop代理
@RestController
@RequestMapping("/api/v2")
public class BeanValidatedController {
// 对参数绑定开启校验
@PostMapping("/person-bad")
public RestResponse badHandler(@Valid @RequestBody Person person) {
// ...
}

@PostMapping("/person-good")
public RestResponse goodHandler(@Validated @RequestBody Person person) {
// ...
}
}
  1. 一次校验是web-mvc参数绑定过程的校验(开关为 @Valid)

    触发机制为:controller参数绑定机制 + @Valid修饰参数激活校验开关

    此时是spring的校验器在工作;

  2. 另外一次是aop代理增强的校验

    触发机制为:@Validated开启aop代理 + @Valid修饰参数激活Bean Validation方法级验证

    此时是Bean Validation的校验器在工作;

两者的原理大不相同。

故上述代码badHandler方法会触发两次校验,goodHandler仅触发web-mvc参数绑定过程的校验。


到此Spring的数据校验体系的应用与部分原理介绍完毕。