spring-boot-starter-validation

官网


  1. https://docs.jboss.org/hibernate/validator/8.0/reference/en-US/html_single/#preface
  2. 超清晰


为什么使用?


  1. 优点
  2. 可方便修改校验(对于值的特定校验,比如email,对于业务的校验,比如存在userId,唯一email)
  3. 对于性能来说,我不知道有没有损失,但是应该差不多


依赖


  1. spring-boot-starter-validation就是直接使用hibernate-validator的
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>


spring的valid支持


  1. 方法参数和方法返回值
  2. MethodValidationInterceptor进行AOP代理实现的(对Validated注解的类的所有方法进行AOP拦截)
  3. 但是对于MethodValidationInterceptor注解需要注意的是
  4. MethodValidationPostProcessor是在Spring的Bean Factory后处理期间应用的,所以@Validated注解只能用于类层次上,而不能用于方法层次上(因为PostProcessor进行处理时bean才刚创建,并没有进行解析,因此只能解析类层次,方法层次还不能解析)
  5. 是调用的validator.forExecutables().validateParameters()对方法参数进行校验(联级校验会对Valid注解的参数的属性进行校验)
  6. bean对象
  7. 使用org.springframework.validation.DataBinder.validate(都有一个差不多的validateIfApplicable方法进行遍历参数注解进行调用)
  8. 在ModelAttributeMethodProcessor,RequestPartMethodArgumentResolver,RequestResponseBodyMethodProcessor这几个会解析为bean对象的参数解析器
  9. 因为仅仅调用validator.validate()方法,因此仅仅是校验了该参数bean对象属性的constrains,而不会校验该参数的constrains
  10. 异常:
  11. BindExceptionModelAttributeMethodProcessor抛出,这个是最后一个ArgumentResolver,解析ModelAttribute/不加@RequestBody的bean对象
  12. ConstraintViolationExceptionMethodValidationInterceptor抛出,即普通参数,基本类/集合
  13. MethodArgumentNotValidExceptionRequestResponseBodyMethodProcessor/RequestPartMethodArgumentResolver抛出
  14. 都是使用javax.validation.Validator
  15. 用的是org.hibernate.validator.internal.engine.ValidatorImpl



注解valid和validated区别


  1. 看Validator实现的使用规范就知道了,在spring mvc自带的Validated拦截中, 一般是不使用Valid,而是完全使用Validated的,因为spring mvc对方法参数检验的实现方法和对bean校验的实现方法(但在Jersey中有其自己的实现,因此使用的Valid)。


Validator实现



  1. 对谁校验?
  2. org.hibernate.validator.internal.engine.ValidatorImpl : implements Validator, ExecutableValidator
  3. 校验bean的所有constraints:<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups)
  4. 校验bean的一个属性:<T> Set<ConstraintViolation<T>> validateProperty(,T object,String propertyName,Class<?>... groups)
  5. 更多校验:ExecutableValidator forExecutables()
  6. 校验一个方法的所有参数:validateParameters
  7. 这个hibernate官方都说需要自己实现,使用aop,代理之类的
  8. validate方法/ExecutableValidator的validateParameters(对其他的校验也差不多)
  9. 一开使用ValidationOrder计算Groups/Sequences(后面校验就是从这获取的Groups/Sequences进行校验)
  10. Groups和Sequences只会执行一种方式,默认Groups,有Sequences就不用Groups
  11. 对于Sequence, 默认failFast, 可以在 Validation.byProvider(HibernateValidator.class).configure().failFast(false)配置
  12. Group/Sequence校验
  13. 第一步:校验本身
  14. 对于bean,就是先校验bean字段/属性上的(值是字段值),再校验bean类上的(值是该bean)
  15. 对方法,就是先校验Cross参数(值是所有参数),再校验单个参数上的(值是参数值)
  16. 统一校验目标上有多个,不确定顺序
  17. 第二步:再校验validateCascadedConstraints
  18. 即校验有Valid的方法参数的属性/有Valid的bean的属性的属性


  1. 使用规范
  2. 普通校验写在属性/参数上
  3. 多参数校验写在交叉校验的方法上(通常是不都为空,都为空等这样的校验)
  4. 对一个bean的某些属性进行比如Io校验, 就在方法的参数上加注解
  5. 理由:spring提供的方法AOP类MethodvalidatedInterceptor是对执行方法时才进行方法校验,而对于属性/字段的校验在执行之前进行的,因此io操作是在最后执行的,并没有浪费性能
  6. 另外:如果是一个bean对象,而且还想对其进行方法校验
  7. 在spring提供的情况下,会在bean校验一次,在方法的联级校验阶段校验一次
  8. 使用:需要给这个参数javax.validation.Valid或者Validated或者startsWith("Valid")的其中之一的注解进行bean校验
  9. 但是Validator的方法参数校验逻辑是会进行联级校验,如果参数有Valid注解,就会进行联级校验,不过我使用spring的Validated就好了



Group



@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = TestLogValidator.class)
@Repeatable(TestLog.List.class)
public @interface TestLog {
String value();
String message() default "TestLog ";

Class<?>[] groups() default { };

Class<? extends Payload>[] payload() default { };
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
TestLog[] value();
}
}
public class TestLogValidator implements ConstraintValidator<TestLog, Object> {
private Object annotationValue;
@Override
public void initialize(TestLog testInit) {
this.annotationValue = testInit.value();
}

@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
System.out.printf("annotationValue=%s, value=%s\n",annotationValue, value);
return false;
}
}





package com.liruo.learn.spring.mvc.valid;

import com.liruo.learn.spring.mvc.validate.TestLog;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.junit.jupiter.api.Test;

import javax.validation.GroupSequence;
import javax.validation.Valid;
import javax.validation.groups.ConvertGroup;
import javax.validation.groups.Default;

import static com.liruo.learn.spring.mvc.valid.ValidatorUtil.validator;

/**
* @Author:liruo
* @Date:2023-06-10-21:40:22
* @Desc
*/
public class GroupTest {
interface Group{
interface Create{ }
interface Update{ }
interface InheritDefault extends Default { }

@GroupSequence({ Update.class, Create.class, Default.class })
interface TestSequence { }

interface Circle{
interface A extends Test { }
interface B{ }
@GroupSequence({ A.class, B.class})
interface Test { }
}

@GroupSequence({ Update.class, Create.class, Default.class })
interface TestSequenceDefault { }
}

@Data
public static class Bean{
@TestLog(value = "f1-Default")
private String f1;
@TestLog(value = "f2-Create", groups = Group.Create.class)
private String f2;
@TestLog(value = "f3-Update", groups = Group.Update.class)
private String f3;

@TestLog(value = "f4-InheritDefault", groups = Group.InheritDefault.class)
private String f4;
}


@Test
public void testConvertGroupOnMethod() throws NoSuchMethodException {
//annotationValue=f3-Update, value=null
validator.forExecutables().validateParameters(
this,
this.getClass().getDeclaredMethod("ConvertGroupOnMethod", Bean.class),
new Object[]{new Bean()}
);
System.err.println("=".repeat(20));
}
public void ConvertGroupOnMethod(@Valid @ConvertGroup(to=Group.Update.class) Bean bean){

}

@Test
public void testConvertGroup(){
//@ConvertGroup用于指定级联校验的对象应用什么组,也就是修改默认组,但是validate方法传入的组还是影响到该对象
// 比如validate指定Create, 级联Convert为Update, 最终级联时还是会执行Create, 而不是Update

//annotationValue=f3-Update, value=null
validator.validate(new BeanConvertGroup(new Bean()));
System.err.println("=".repeat(20));

//annotationValue=f2-Create, value=null
validator.validate(new BeanConvertGroup(new Bean()), Group.Create.class);
System.err.println("=".repeat(20));
}
@Data
@AllArgsConstructor
public static class BeanConvertGroup{
@Valid
@ConvertGroup(to=Group.Update.class)
private Bean bean;
}
//还可以动态调整顺序, 类似于自定义Validator
//@GroupSequenceProvider(XXXGroupSequenceProvider.class) implements DefaultGroupSequenceProvider<T>
//https://docs.jboss.org/hibernate/validator/8.0/reference/en-US/html_single/#_groupsequenceprovider

@Test
public void testReplaceDefault(){
//@GroupSequence在类上就是定义默认分组顺序, 也顺便定义了默认执行的分组
//注意默认分组必须包含在内, 但是不是使用Default.class而是要定义默认分组顺序的类
//annotationValue=f2-Create, value=null
//annotationValue=f1-Default, value=null
validator.validate(new BeanReplaceDefault());
System.err.println("=".repeat(20));
}
@GroupSequence({Group.Create.class, BeanReplaceDefault.class})
@Data
public static class BeanReplaceDefault{
@TestLog(value = "f1-Default")
private String f1;
@TestLog(value = "f2-Create", groups = Group.Create.class)
private String f2;
@TestLog(value = "f3-Update", groups = Group.Update.class)
private String f3;
}

@Test
public void testSequenceCircle(){
//GroupDefinitionException: HV000046: Sequence definitions are not allowed as composing parts of a sequence.
validator.validate(new Bean(), Group.Circle.Test.class);
System.err.println("=".repeat(20));
}
@Test
public void testSequence(){
//对于Sequence, 默认failFast, 可以在 Validation.byProvider(HibernateValidator.class).configure().failFast(false)配置
//annotationValue=f3-Update, value=null
//annotationValue=f2-Create, value=null
//annotationValue=f1-Default, value=null
validator.validate(new Bean(), Group.TestSequence.class);
System.err.println("=".repeat(20));
}
@Test
public void testGroup(){
System.err.println("=".repeat(20));

//annotationValue=f1-Default, value=null
validator.validate(new Bean());
System.err.println("=".repeat(20));

//annotationValue=f2-Create, value=null
validator.validate(new Bean(), Group.Create.class);
System.err.println("=".repeat(20));

//annotationValue=f1-Default, value=null
//annotationValue=f2-Create, value=null
validator.validate(new Bean(), Default.class, Group.Create.class);
System.err.println("=".repeat(20));


//会把继承的一起执行了
//annotationValue=f2-Create, value=null
//annotationValue=f4-InheritDefault, value=null
//annotationValue=f1-Default, value=null
validator.validate(new Bean(), Group.Create.class, Group.InheritDefault.class);
System.err.println("=".repeat(20));
}
}





正常需求例子


  1. 注意:在Validated注解的类的方法中,不要使用Valid注解,会造成重复校验(详细请查看Validator实现的使用规范
package com.liruo.learn.spring.mvc.controller.valid;

import com.liruo.learn.spring.mvc.validate.TestIO;
import com.liruo.learn.spring.mvc.validate.TestLog;
import lombok.Data;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.groups.Default;

/**
* @Author:liruo
* @Date:2023-06-10-23:06:49
* @Desc
*/
@Validated
@RequestMapping("/normal")
@RestController
public class NormalController {
@Data
public static class Bean {
@TestLog("校验name")
private String name;

@TestLog(value = "校验name2-Create", groups = Groups.Create.class)
private String name2;

@TestLog(value = "校验name3-Update", groups = Groups.Update.class)
private String name3;
}


/**
* 重复校验反例
*/
@PostMapping("/Valid/{id}")
public String valid(@Valid @NotNull @TestIO("io1") @RequestBody Bean bean, @Min(2) @PathVariable("id") Integer id) {
//annotationValue=校验name, value=123
//TestIO annotationValue=io1, value=NormalController.Bean(name=123)
//annotationValue=校验name, value=123
return "id=" + id + " " + bean.toString();
}

@PostMapping("/Validated/{id}")
public String validated(@Validated @NotNull @TestIO("io1") @RequestBody Bean bean, @Min(2) @PathVariable("id") Integer id) {
//annotationValue=校验name, value=awd
//TestIO annotationValue=io1, value=NormalController.Bean(name=awd)
return "id=" + id + " " + bean.toString();
}


interface Groups{
interface Create {}
interface Update {}
}
@PostMapping("/Validated/group/Update/{id}")
public String postUpdateGroup(@Validated({Default.class, Groups.Update.class}) @NotNull @TestIO("io1") @RequestBody Bean bean, @Min(2) @PathVariable("id") Integer id) {
//annotationValue=校验name, value=1
//annotationValue=校验name3-Update, value=1
//TestIO annotationValue=io1, value=NormalController.Bean(name=1, name2=1, name3=1)
return "id=" + id + " " + bean.toString();
}
@PostMapping("/Validated/group/Create/{id}")
public String postCreateGroup(@Validated({Default.class, Groups.Create.class}) @NotNull @TestIO("io1") @RequestBody Bean bean, @Min(2) @PathVariable("id") Integer id) {
//annotationValue=校验name, value=w
//annotationValue=校验name2-Create, value=w
//TestIO annotationValue=io1, value=NormalController.Bean(name=w, name2=w, name3=w)
return "id=" + id + " " + bean.toString();
}

}



0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
李若
下载 APP