目录
一、背景
二、Bean Validation 简介
1. 什么是 JSR ?
2. JSR-303 定义的是什么标准?
3. Spring Validation 的产生背景
3.1 为什么会同时存在两种方式?
3.2 为什么不合入到 JSR-303 中?
3.3 @Validated 的内置自动化校验
3.4 @Validated 和 @Valid 的区别
三、Bean Validation 的使用
1. 引入 POM
2. Bean 层面校验
2.1 变量层面约束
2.2 属性层面约束
2.3 容器元素约束
2.4 类层面约束
2.5 嵌套约束
2.6 手工验证 Bean 约束
3. 方法层面校验
3.1 函数参数约束
3.2 函数返回值约束
3.3 嵌套约束
3.4 在继承中方法约束
3.5 手工验证方法约束
4. 分组校验
5. 自定义校验
四、Bean Validation 自动执行以及原理
1. Validation 的常见误解
1.1 约束校验生效
1.2 约束校验不生效
2. Controller 自动执行约束校验原理
3. Service 自动执行约束校验原理
五、总结
在前后端开发过程中,数据校验是一项必须且常见的事,从展示层、业务逻辑层到持久层几乎每层都需要数据校验。如果在每一层中手工实现验证逻辑,既耗时又容易出错。
为了避免重复这些验证,通常的做法是将验证逻辑直接捆绑到领域模型中,通过元数据(默认是注解)去描述模型, 生成校验代码,从而使校验从业务逻辑中剥离,提升开发效率,使开发者更专注业务逻辑本身。
在 Spring 中,目前支持两种不同的验证方法:Spring Validation 和 JSR-303 Bean Validation,即 @Validated(org . springframework.validation.annotation.Validated)和 @Valid(javax.validation.Valid)。两者都可以通过定义模型的约束来进行数据校验,虽然两者使用类似,在很多场景下也可以相互替换,但实际上却完全不同,这些差别长久以来对我们日常使用产生了较大疑惑,本文主要梳理其中的差别、介绍 Validation 的使用及其实现原理,帮助大家在实践过程中更好使用 Validation 功能。
什么是JSR?
JSR-303定义的是什么标准?
常用的校验注解补充:
@NotBlank 检查约束字符串是不是 Null 还有被 Trim 的长度是否大于,只对字符串,且会去掉前后空格。
@NotEmpty 检查约束元素是否为 Null 或者是 Empty。
@Length 被检查的字符串长度是否在指定的范围内。
@Email 验证是否是邮件地址,如果为 Null,不进行验证,算通过验证。
@Range 数值返回校验。
@IdentityCardNumber 校验身份证信息。
@UniqueElements 集合唯一性校验。
@URL 验证是否是一个 URL 地址。
Spring Validation的产生背景
Spring 增加 @Validated 是为了支持分组校验,即同一个对象在不同的场景下使用不同的校验形式。比如有两个步骤用于提交用户资料,后端复用的是同一个对象,第一步验证姓名,电子邮件等字段,然后在后续步骤中的其他字段中。这时候分组校验就会发挥作用。
之所以没有将它添加到 @Valid 注释中,是因为它是使用 Java 社区过程(JSR-303)标准化的,这需要时间,而 Spring 开发者想让人们更快地使用这个功能。
Spring 增加 @Validated 还有另一层原因,Bean Validation 的标准做法是在程序中手工调用 Validator 或者 ExecutableValidator 进行校验,为了实现自动化,通常通过 AOP、代理等方法拦截技术来调用。而 @Validated 注解就是为了配合 Spring 进行 AOP 拦截,从而实现 Bean Validation 的自动化执行。
@Valid 是 JSR 标准 API,@Validated 扩展了 @Valid 支持分组校验且能作为 SpringBean 的 AOP 注解,在 SpringBean 初始化时实现方法层面的自动校验。最终还是使用了 JSR API 进行约束校验。
引入POM
// 正常应该引入hibernate-validator,是JSR的参考实现
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
// Spring在stark中集成了,所以hibernate-validator可以不用引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Bean层面校验
public class EntryApplicationInfoCmd {
/**
* 用户ID
*/
private Long userId;
/**
* 证件类型
*/
private String certType;
}
主要为了限制 Setter 方法的只读属性。属性的 Getter 方法打注释,而不是 Setter。
public class EntryApplicationInfoCmd {
public EntryApplicationInfoCmd(Long userId, String certType) {
this.userId = userId;
this.certType = certType;
}
/**
* 用户ID
*/
private Long userId;
/**
* 证件类型
*/
private String certType;
public String getUserId() {
return userId;
}
public String getCertType() {
return userId;
}
}
public class EntryApplicationInfoCmd {
...
List<Long> categoryList;
}
@CategoryBrandNotEmptyRecord 是自定义类层面的约束,也可以约束在构造函数上。
public class EntryApplicationInfoCmd {
/**
* 用户ID
*/
private Long userId;
List<Long> categoryList;
}
嵌套对象需要额外使用 @Valid 进行标注(@Validate 不支持,为什么?请看产生的背景)。
public class EntryApplicationInfoCmd {
/**
* 主营品牌
*/
private MainBrandImagesCmd mainBrandImage;
}
public class MainBrandImagesCmd {
/**
* 品牌名称
*/
private String brandName;;
}
// 获取校验器
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
// 进行bean层面校验
Set<ConstraintViolation<User>> violations = validator.validate(EntryApplicationInfoCmd);
// 打印校验信息
for (ConstraintViolation<User> violation : violations) {
log.error(violation.getMessage());
}
方法层面校验
public class MerchantMainApplyQueryService {
MainApplyDetailResp detail(Long id) {
...
}
}
public class MerchantMainApplyQueryService {
public List< MainApplyStandDepositResp> getStanderNewDeposit(Long id) {
//...
}
}
嵌套对象需要额外使用 @Valid 进行标注(@Validate 不支持)。
public class MerchantMainApplyQueryService {
public NewEntryBrandRuleCheckApiResp brandRuleCheck( NewEntryBrandRuleCheckRequest request) {
...
}
}
public class NewEntryBrandRuleCheckRequest {
private Long level1CategoryId;
}
Validation 的设计需要遵循里氏替换原则,无论何时使用类型 T,也可以使用 T 的子类型 S,而不改变程序的行为。即子类不能增加约束也不能减弱约束。
子类方法参数的约束与父类行为不一致(错误例子):
// 继承的方法参数约束不能改变,否则会导致父类子类行为不一致
public interface Vehicle {
void drive(@Max(75) int speedInMph);
}
public class Car implements Vehicle {
public void drive(@Max(55) int speedInMph) {
//...
}
}
方法的返回值可以增加约束(正确例子):
// 继承的方法返回值可以增加约束
public interface Vehicle {
List<Person> getPassengers();
}
public class Car implements Vehicle {
1) (min =
public List<Person> getPassengers() {
//...
return null;
}
}
方法层面校验使用的是 ExecutableValidator。
// 获取校验器
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator executableValidator = factory.getValidator().forExecutables();
// 进行方法层面校验
MerchantMainApplyQueryService service = getService();
Method method = MerchantMainApplyQueryService.class.getMethod( "getStanderNewDeposit", int.class );
Object[] parameterValues = { 80 };
Set<ConstraintViolation<Car>> violations = executableValidator.validateParameters(
service,
method,
parameterValues
);
// 打印校验信息
for (ConstraintViolation<User> violation : violations) {
log.error(violation.getMessage());
}
分组校验
public class NewEntryMainApplyRequest {
private Long level1CategoryId;
private Long applyId;
private String operator;
}
// 校验分组UpdateMerchantMainApplyCmd.class
NewEntryMainApplyRequest request1 = new NewEntryMainApplyRequest( 29, null, "aaa");
Set<ConstraintViolation<NewEntryMainApplyRequest>> constraintViolations = validator.validate( request1, UpdateMerchantMainApplyCmd.class );
assertEquals("申请单ID不能为空", constraintViolations.iterator().next().getMessage());
// 校验分组AddMerchantMainApplyCmd.class
NewEntryMainApplyRequest request2 = new NewEntryMainApplyRequest( 29, "12345", "");
Set<ConstraintViolation<NewEntryMainApplyRequest>> constraintViolations = validator.validate( request2, AddMerchantMainApplyCmd.class );
assertEquals("审批人不能为空", constraintViolations.iterator().next().getMessage());
自定义校验
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyConstraint {
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
自定义校验器:
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> {
public void initialize(MyConstraint constraintAnnotation) {
}
public isValid isValid(Object value, ConstraintValidatorContext context) {
String name = (String)value;
if("xxxx".equals(name)) {
return true;
}
return false;
}
}
使用自定义约束:
public class Test {
String name;
}
Validation的常见误解
Spring-mvc 中在 @RequestBody 后面加 @Validated、@Valid 约束即可生效。
public class MerchantEnterController {
// 使用@Validated
public HttpMessageResult addOrUpdateV1( MerchantEnterApplicationReq req){
...
}
// 使用@Valid
public HttpMessageResult addOrUpdate2( MerchantEnterApplicationReq req){
...
}
}
然而下面这个约束其实是不生效的,想要生效得在 MerchantEntryServiceImpl 类目加上 @Validated 注解。
// @Validated 不加不生效
public class MerchantEntryService {
public Boolean applicationAddOrUpdate( MerchantEnterApplicationReq req) {
...
}
public Boolean applicationAddOrUpdate2( MerchantEnterApplicationReq req) {
...
}
}
那么究竟为什么会出现这种情况呢,这就需要对 Spring Validation 的注解执行原理有一定的了解。
Controller自动执行约束校验原理
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
// 类上或者方法上标注了@ResponseBody注解都行
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class));
}
// 这是处理入参封装校验的入口
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
// 获取请求的参数对象
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
// 获取参数名称
String name = Conventions.getVariableNameForParameter(parameter);
// 只有存在binderFactory才会去完成自动的绑定、校验~
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
// 这里完成数据绑定+数据校验~~~~~(绑定的错误和校验的错误都会放进Errors里)
validateIfApplicable(binder, parameter);
// 若有错误消息hasErrors(),并且仅跟着的一个参数不是Errors类型,Spring MVC会主动给你抛出MethodArgumentNotValidException异常
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
// 把错误消息放进去 证明已经校验出错误了~~~
// 后续逻辑会判断MODEL_KEY_PREFIX这个key的~~~~
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
...
}
约束的校验逻辑是在 RequestResponseBodyMethodProcessor.validateIfApplicable 实现的,这里同时兼容了 @Validated 和 @Valid,所以该场景下两者是等价的。
// 校验,如果合适的话。使用WebDataBinder,失败信息最终也都是放在它身上~
// 入参:MethodParameter parameter
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
// 拿到标注在此参数上的所有注解们(比如此处有@Valid和@RequestBody两个注解)
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
// 先看看有木有@Validated
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
// 这个里的判断是关键:可以看到标注了@Validated注解 或者注解名是以Valid打头的 都会有效哦
//注意:这里可没说必须是@Valid注解。实际上你自定义注解,名称只要一Valid开头都成~~~~~
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
// 拿到分组group后,调用binder的validate()进行校验~~~~
// 可以看到:拿到一个合适的注解后,立马就break了~~~
// 所以若你两个主机都标注@Validated和@Valid,效果是一样滴~
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
binder.validate(validationHints);
break;
}
}
binder.validate() 的实现中使用的 org.springframework.validation.Validator 的接口,该接口的实现为 SpringValidatorAdapter。
public void validate(Object... validationHints) {
Object target = getTarget();
Assert.state(target != null, "No target to validate");
BindingResult bindingResult = getBindingResult();
for (Validator validator : getValidators()) {
// 使用的org.springframework.validation.Validator,调用SpringValidatorAdapter.validate
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
((SmartValidator) validator).validate(target, bindingResult, validationHints);
}
else if (validator != null) {
validator.validate(target, bindingResult);
}
}
}
在 ValidatorAdapter.validate 实现中,最终调用了 javax.validation.Validator.validate,也就是说最终是调用 JSR 实现,@Validate 只是外层的包装,在这个包装中扩展的分组功能。
public class SpringValidatorAdapter {
...
private javax.validation.Validator targetValidator;
public void validate(Object target, Errors errors) {
if (this.targetValidator != null) {
processConstraintViolations(
// 最终是调用JSR实现
this.targetValidator.validate(target), errors));
}
}
}
targetValidator.validate 就是 javax.validation.Validator.validate 上述 2.6 Bean 层面手工验证一致。
Service自动执行约束校验原理
非Controller的@RequestBody注解,自动执行约束校验,是通过 MethodValidationPostProcessor 实现的,该类继承。
BeanPostProcessor, 在 Spring Bean 初始化过程中读取 @Validated 注解创建 AOP 代理(实现方式与 @Async 基本一致)。该类开头文档注解(JSR 生效必须类层面上打上 @Spring Validated 注解)。
/**
* <p>Target classes with such annotated methods need to be annotated with Spring's
* {@link Validated} annotation at the type level, for their methods to be searched for
* inline constraint annotations. Validation groups can be specified through {@code @Validated}
* as well. By default, JSR-303 will validate against its default group only.
*/
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
implements InitializingBean {
private Class<? extends Annotation> validatedAnnotationType = Validated.class;
private Validator validator;
.....
/**
* 设置Validator
* Set the JSR-303 Validator to delegate to for validating methods.
* <p>Default is the default ValidatorFactory's default Validator.
*/
public void setValidator(Validator validator) {
// Unwrap to the native Validator with forExecutables support
if (validator instanceof LocalValidatorFactoryBean) {
this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
}
else if (validator instanceof SpringValidatorAdapter) {
this.validator = validator.unwrap(Validator.class);
}
else {
this.validator = validator;
}
}
/**
* Create AOP advice for method validation purposes, to be applied
* with a pointcut for the specified 'validated' annotation.
* @param validator the JSR-303 Validator to delegate to
* @return the interceptor to use (typically, but not necessarily,
* a {@link MethodValidationInterceptor} or subclass thereof)
* @since 4.2
*/
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
// 创建了方法调用时的拦截器
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}
真正执行方法调用时,会走到 MethodValidationInterceptor.invoke,进行约束校验。
public class MethodValidationInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
// Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
// Standard Bean Validation 1.1 API
ExecutableValidator execVal = this.validator.forExecutables();
...
try {
// 执行约束校验
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);
}
...
return returnValue;
}
}
execVal.validateParameters 就是 javax.validation.executable.ExecutableValidator.validateParameters 与上述 3.5 方法层面手工验证一致。
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single
往期回顾
关注得物技术,每周一、三、五更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
“
扫码添加小助手微信
如有任何疑问,或想要了解更多技术资讯,请添加小助手微信: