数据校验基础
参考: Java Bean Validation 规范
Spring对Bean Validation的支持
Spring定义了一个接口org.springframework.validation.Validator
,用于应用相关的对象的校验器。
这个接口完全从基础设施或者上下文中脱离的,这意味着它没有跟web层或者数据访问层或者其余任何的某一个层次发生耦合。所以它能用于应用中的任意一个层次,能对应用中的任意一个对象进行校验。
接口定义
public interface Validator {
// 此clazz是否可以被validate
boolean supports(Class<?> clazz);
// 执行校验,错误消息放在Errors中
// 如果能执行校验,通常也意味着supports方法返回true
// 可以参考ValidationUtils这个工具类
void validate(Object target, Errors errors);
}
接口实现
SmartValidator
对Validator接口进行了增强,能进行分组校验。
public interface SmartValidator extends Validator {
// validationHints:就是启动的校验组
// target:需要校验的结果
// errors:封装校验
void validate(Object target, Errors errors, Object... validationHints);
// 假设value将被绑定到指定对象中的指定字段上,并进行校验
// @since 5.1 这个方法子类需要复写 否则不能使用
default void validateValue(Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) {
throw new IllegalArgumentException("Cannot validate individual value for " + targetType);
}
}
SpringValidatorAdapter
SpringValidatorAdapter
实现了对 Bean Validation
的适配。
// 可以看到,这个接口同时实现了Spring中的SmartValidator接口跟JSR中的Validator接口
public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator {
//约束必须使用的3个属性
private static final Set<String> internalAnnotationAttributes = new HashSet<>(4);
static {
internalAnnotationAttributes.add("message");
internalAnnotationAttributes.add("groups");
internalAnnotationAttributes.add("payload");
}
// targetValidator就是实际完成校验的对象
@Nullable
private javax.validation.Validator targetValidator;
public SpringValidatorAdapter(javax.validation.Validator targetValidator) {
Assert.notNull(targetValidator, "Target Validator must not be null");
this.targetValidator = targetValidator;
}
SpringValidatorAdapter() {
}
void setTargetValidator(javax.validation.Validator targetValidator) {
this.targetValidator = targetValidator;
}
// 支持对所有类型的Bean的校验
@Override
public boolean supports(Class<?> clazz) {
return (this.targetValidator != null);
}
// 调用targetValidator完成校验,并通过processConstraintViolations方法封装校验后的结果到Errors中
@Override
public void validate(Object target, Errors errors) {
if (this.targetValidator != null) {
processConstraintViolations(this.targetValidator.validate(target), errors);
}
}
// 完成分组校验
@Override
public void validate(Object target, Errors errors, Object... validationHints) {
if (this.targetValidator != null) {
processConstraintViolations(
this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
}
}
// 完成对对象上某一个字段及给定值的校验
@SuppressWarnings("unchecked")
@Override
public void validateValue(
Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) {
if (this.targetValidator != null) {
processConstraintViolations(this.targetValidator.validateValue(
(Class) targetType, fieldName, value, asValidationGroups(validationHints)), errors);
}
}
// @since 5.1
// 将validationHints转换成JSR中的分组
private Class<?>[] asValidationGroups(Object... validationHints) {
Set<Class<?>> groups = new LinkedHashSet<>(4);
for (Object hint : validationHints) {
if (hint instanceof Class) {
groups.add((Class<?>) hint);
}
}
return ClassUtils.toClassArray(groups);
}
// 省略对校验错误的封装
// .....
// 省略对JSR中validator接口的实现,都是委托给targetValidator完成的
// ......
}
ValidatorAdapter
跟SpringValidatorAdapter
同一级别的类,但是不同的是他没有实现JSR
中的Validator
接口。一般不会使用这个类。
它实现了对 SmartValidator
的适配。
public class ValidatorAdapter implements SmartValidator, ApplicationContextAware, InitializingBean, DisposableBean {
private final SmartValidator target;
private final boolean existingBean;
ValidatorAdapter(SmartValidator target, boolean existingBean) {
this.target = target;
this.existingBean = existingBean;
}
public final Validator getTarget() {
return this.target;
}
@Override
public boolean supports(Class<?> clazz) {
return this.target.supports(clazz);
}
//... ...
}
CustomValidatorBean
配置一个bean,暴露一个 JSR-303 Validator
,使用了 JSR 的3个接口。
public class CustomValidatorBean extends SpringValidatorAdapter implements Validator, InitializingBean {
@Nullable
private ValidatorFactory validatorFactory;
@Nullable
private MessageInterpolator messageInterpolator;
@Nullable
private TraversableResolver traversableResolver;
}
LocalValidatorFactoryBean
OptionalValidatorFactoryBean
继承了LocalValidatorFactoryBean
,区别在于让校验器的初始化成为可选的,即使校验器没有初始化成功也不会报错。
@Validated跟@Valid的区别
定义
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
// 校验时启动的分组
Class<?>[] value() default {};
}
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
//没有提供任何属性
}
区别
-
来源不同:
@Valid
是JSR的规范,来源于javax.validation
包下,而@Validated
是Spring自身定义的注解,位于org.springframework.validation.annotation
包下 -
作用范围不同:
@Validated
无法作用在字段上,正因为如此它就无法完成对级联属性的校验。而@Valid
的没有这个限制。 -
注解中的属性不同:
@Validated
注解中可以提供一个属性去指定校验时采用的分组,而@Valid
没有这个功能,因为@Valid
不能进行分组校验
应用
准备
待校验的类:
@Data
public class ValidatedData {
@NotNull
public String name;
@Positive
public Integer age;
@NotNull
@NotEmpty
private List<@Email String> emails;
/**
* 定义的2个组,以接口的形式
*/
public interface GroupA {
}
public interface GroupB {
}
}
/**
* 外部验证数据,用于级联验证
*
* @author lihz
* @date 2023/2/18
*/
@Data
public class OuterValidatedData {
@NotNull
String name;
@Valid
ValidatedData validatedData;
}
测试Controller:
package com.jurassic.cloud.project.controller;
import com.jurassic.cloud.project.dto.OuterValidatedData;
import com.jurassic.cloud.project.dto.ValidatedData;
import com.jurassic.cloud.project.service.impl.ValidationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
@RestController
@RequestMapping("/validate")
public class ValidationController {
@Autowired
ValidationService validationService;
@PostMapping("/valid")
public String testValid(@RequestBody @Valid ValidatedData data) {
System.out.println(data);
return "OK";
}
@PostMapping("/validated")
public String testValidated(@RequestBody @Validated ValidatedData data) {
System.out.println(data);
return "OK";
}
@PostMapping("/valid/nest")
public String testValidNest(@RequestBody @Valid OuterValidatedData data) {
System.out.println(data);
return "OK";
}
@PostMapping("/validated/nest")
public String testValidatedNest(@RequestBody @Validated OuterValidatedData data) {
System.out.println(data);
return "OK";
}
@PostMapping("/valid/method")
public String testValidMethod(@RequestBody ValidatedData data) {
validationService.testValid(data);
return "OK";
}
@PostMapping("/validated/method")
public String testValidatedMethod(@RequestBody ValidatedData data) {
validationService.testValidated(data);
return "OK";
}
@PostMapping("/valid/nest/method")
public String testValidNestMethod(@RequestBody OuterValidatedData data) {
validationService.testValidNest(data);
return "OK";
}
@PostMapping("/validated/nest/method")
public String testValidatedNestMethod(@RequestBody OuterValidatedData data) {
validationService.testValidatedNest(data);
return "OK";
}
@PostMapping("/valid/simple")
public String testValid(@Valid @Max(10) int age, @Valid @NotBlank String name) {
System.out.println(age + " " + name);
return "OK";
}
@PostMapping("/validated/simple")
public String testValidated(@Validated @Max(10) int age, @Validated @NotBlank String name) {
System.out.println(age + " " + name);
return "OK";
}
@PostMapping("/non/method/simple")
public String testNonMethodSimple(@Max(10) int age, @NotBlank String name) {
validationService.testNon(age, name);
return "OK";
}
@PostMapping("/valid/method/simple")
public String testValidMethodSimple(@Max(10) int age, @NotBlank String name) {
validationService.testValid(age, name);
return "OK";
}
@PostMapping("/validated/method/simple")
public String testValidatedMethodSimple(@Max(10) int age, @NotBlank String name) {
validationService.testValidated(age, name);
return "OK";
}
}
测试数据:
{
"name": "demon",
"age": -1,
"emails": [
"demon7552003@hotmail.com"
]
}
{
"name": "xxxxx",
"data": {
"name": "demon",
"age": -1,
"emails": [
"demon7552003@hotmail.com"
]
}
}
测试服务
package com.jurassic.cloud.project.service.impl;
import com.jurassic.cloud.project.dto.OuterValidatedData;
import com.jurassic.cloud.project.dto.ValidatedData;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
/**
*
*
* @author lihz
* @date 2023/2/18
*/
@Service
//@Validated
@Valid
public class ValidationService {
public void testValid(@Valid ValidatedData data) {
System.out.println(data);
}
public void testValidated(@Validated ValidatedData data) {
System.out.println(data);
}
public void testValidNest(@Valid OuterValidatedData data) {
System.out.println(data);
}
public void testValidatedNest(@Validated OuterValidatedData data) {
System.out.println(data);
}
public void testNon( @Max(10) int age, @NotBlank String name) {
System.out.println(age+" "+name);
}
public void testValid(@Valid @Max(10) int age,@Valid @NotBlank String name) {
System.out.println(age+" "+name);
}
public void testValidated(@Validated @Max(10) int age, @Validated @NotBlank String name) {
System.out.println(age+" "+name);
}
}
对JavaBean的校验(Controller层)
测试结果
-
valid
测试{ "code": 1, // age 必须是正数 "msg": "Validation failed for argument [0] in public java.lang.String com.jurassic.cloud.project.controller.ValidationController.testValid(com.jurassic.cloud.project.dto.ValidatedData): [Field error in object 'validatedData' on field 'age': rejected value [-1]; codes [Positive.validatedData.age,Positive.age,Positive.java.lang.Integer,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validatedData.age,age]; arguments []; default message [age]]; default message [必须是正数]] ", "data": null }
-
validated
测试
{
"code": 1,
"msg": "Validation failed for argument [0] in public java.lang.String com.jurassic.cloud.project.controller.ValidationController.testValidated(com.jurassic.cloud.project.dto.ValidatedData): [Field error in object 'validatedData' on field 'age': rejected value [-1]; codes [Positive.validatedData.age,Positive.age,Positive.java.lang.Integer,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validatedData.age,age]; arguments []; default message [age]]; default message [必须是正数]] ",
"data": null
}
-
valid
测试 嵌套数据{ "code": 1, "msg": "Validation failed for argument [0] in public java.lang.String com.jurassic.cloud.project.controller.ValidationController.testValidNest(com.jurassic.cloud.project.dto.OuterValidatedData): [Field error in object 'outerValidatedData' on field 'validatedData.age': rejected value [-1]; codes [Positive.outerValidatedData.validatedData.age,Positive.validatedData.age,Positive.age,Positive.java.lang.Integer,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [outerValidatedData.validatedData.age,validatedData.age]; arguments []; default message [validatedData.age]]; default message [必须是正数]] ", "data": null }
-
validated
测试 嵌套数据{ "code": 1, "msg": "Validation failed for argument [0] in public java.lang.String com.jurassic.cloud.project.controller.ValidationController.testValidatedNest(com.jurassic.cloud.project.dto.OuterValidatedData): [Field error in object 'outerValidatedData' on field 'validatedData.age': rejected value [-1]; codes [Positive.outerValidatedData.validatedData.age,Positive.validatedData.age,Positive.age,Positive.java.lang.Integer,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [outerValidatedData.validatedData.age,validatedData.age]; arguments []; default message [validatedData.age]]; default message [必须是正数]] ", "data": null }
总结
从结果上看 @Valid
和 @Validated
都能触发级联验证。
如果要触发属性的级联验证,一定要放注解 @Valid
。
对普通方法的JavaBean校验
测试结果
Service类上不加注解
-
valid
测试OK
-
validated
测试ok
-
valid
测试 嵌套数据OK
-
validated
测试 嵌套数据OK
Service类上加@Valid
-
valid
测试OK
-
validated
测试ok
-
valid
测试 嵌套数据OK
-
validated
测试 嵌套数据OK
Service类上加@Validated
-
valid
测试{ "code": 1, "msg": "testValid.data.age: 必须是正数", "data": null }
-
validated
测试ok
-
valid
测试 嵌套数据{ "code": 1, "msg": "testValidNest.data.validatedData.age: 必须是正数", "data": null }
-
validated
测试 嵌套数据OK
总结
只有类上添加了@Vlidated
注解,并且待校验的**JavaBean上添加了@Valid
**的情况下校验才会生效。
返回的异常信息包含了 进行验证的方法的名称,以及验证的属性。
对简单参数的校验(Controller层)
测试结果
1、测试:http://127.0.0.1:8073/validate/valid/simple?age=20&name=demon
结果:OK
2、测试:http://127.0.0.1:8073/validate/validated/simple?age=20&name=demon
结果:OK
结论
参数上,不论加 @Valid
还是 @Validated
,都不会触发验证。
对简单参数的校验(Service层)
测试结果
1、测试:http://127.0.0.1:8073/validate/valid/simple?age=20&name=demon
结果:OK
2、测试:http://127.0.0.1:8073/validate/validated/simple?age=20&name=demon
结果:OK
结论
参数上,不论加 @Valid
还是 @Validated
,都不会触发验证。
对普通方法上的简单参数的校验(Service层)
1、参数不加注解: http://127.0.0.1:8073/validate/non/method/simple?age=20
2、参数加@Valid
: http://127.0.0.1:8073/validate/valid/simple?age=20
3、参数加@Validated
:http://127.0.0.1:8073/validate/validated/simple?age=20
测试结果
Service类上不加注解
1、OK
2、OK
3、OK
Service类上加@Valid
1、OK
2、OK
3、OK
Service类上加@Validated
1、
{
"code": 1,
"msg": "testNon.age: 最大不能超过10, testNon.name: 不能为空",
"data": null
}
2、
{
"code": 1,
"msg": "testValid.name: 不能为空, testValid.age: 最大不能超过10",
"data": null
}
3、
{
"code": 1,
"msg": "testValidated.age: 最大不能超过10, testValidated.name: 不能为空",
"data": null
}
结论
参数上,不论加 @Valid
还是 @Validated
,或者不加 ,都不会影响结果。
仅在类上加 @Validated
时,才会触发简单参数的验证。
普通方法验证的总结
仅在类上加@Validated
时,才会触发参数的验证。对于非简单类的验证,必须加 @Valid
触发级联验证。
Controller层验证的总结
仅能验证非简单类型。
从结果上看 @Valid
和 @Validated
都能触发级联验证。
如果要触发属性的级联验证,一定要放注解 @Valid
。
Spring对JSR的适配
在普通方法上的验证,抛出异常ConstraintViolationException
,在Controller层抛出的异常是MethodArgumentNotValidException
。Spring对参数绑定做了封装,错误信息被封装为BindingResult
,在方法RequestResponseBodyMethodProcessor#resolveArgument
中做处理。
全局异常处理
通过@RestControllerAdvice
做AOP。
@RestControllerAdvice
public class MethodArgumentNotValidExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder stringBuilder = new StringBuilder();
for (FieldError error : bindingResult.getFieldErrors()) {
String field = error.getField();
Object value = error.getRejectedValue();
String msg = error.getDefaultMessage();
String message = String.format("错误字段:%s,错误值:%s,原因:%s;", field, value, msg);
stringBuilder.append(message).append("\r\n");
}
return Result.error(MsgDefinition.ILLEGAL_ARGUMENTS.codeOf(), stringBuilder.toString());
}
}
附录
参考
官网:https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/spring-framework-reference/core.html#validation-beanvalidation