spring-boot-starter-validation
官网
为什么使用?
- 优点
- 可方便修改校验(对于值的特定校验,比如email,对于业务的校验,比如存在userId,唯一email)
- 对于性能来说,我不知道有没有损失,但是应该差不多
依赖
- spring-boot-starter-validation就是直接使用hibernate-validator的
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
spring的valid支持
- 方法参数和方法返回值
- MethodValidationInterceptor进行AOP代理实现的(对Validated注解的类的所有方法进行AOP拦截)
- 但是对于MethodValidationInterceptor注解需要注意的是
- MethodValidationPostProcessor是在Spring的Bean Factory后处理期间应用的,所以@Validated注解只能用于类层次上,而不能用于方法层次上(因为PostProcessor进行处理时bean才刚创建,并没有进行解析,因此只能解析类层次,方法层次还不能解析)
- 是调用的validator.forExecutables().validateParameters()对方法参数进行校验(联级校验会对Valid注解的参数的属性进行校验)
- bean对象
- 使用org.springframework.validation.DataBinder.validate(都有一个差不多的validateIfApplicable方法进行遍历参数注解进行调用)
- 在ModelAttributeMethodProcessor,RequestPartMethodArgumentResolver,RequestResponseBodyMethodProcessor这几个会解析为bean对象的参数解析器
- 因为仅仅调用validator.validate()方法,因此仅仅是校验了该参数bean对象属性的constrains,而不会校验该参数的constrains
- 异常:
- BindException:ModelAttributeMethodProcessor抛出,这个是最后一个ArgumentResolver,解析ModelAttribute/不加@RequestBody的bean对象
- ConstraintViolationException:MethodValidationInterceptor抛出,即普通参数,基本类/集合
- MethodArgumentNotValidException:RequestResponseBodyMethodProcessor/RequestPartMethodArgumentResolver抛出
- 都是使用javax.validation.Validator
- 用的是org.hibernate.validator.internal.engine.ValidatorImpl
注解valid和validated区别
- 看Validator实现的使用规范就知道了,在spring mvc自带的Validated拦截中, 一般是不使用Valid,而是完全使用Validated的,因为spring mvc对方法参数检验的实现方法和对bean校验的实现方法(但在Jersey中有其自己的实现,因此使用的Valid)。
Validator实现
- 对谁校验?
- org.hibernate.validator.internal.engine.ValidatorImpl : implements Validator, ExecutableValidator
- 校验bean的所有constraints:<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups)
- 校验bean的一个属性:<T> Set<ConstraintViolation<T>> validateProperty(,T object,String propertyName,Class<?>... groups)
- 更多校验:ExecutableValidator forExecutables()
- 校验一个方法的所有参数:validateParameters
- 这个hibernate官方都说需要自己实现,使用aop,代理之类的
- validate方法/ExecutableValidator的validateParameters(对其他的校验也差不多)
- 一开使用ValidationOrder计算Groups/Sequences(后面校验就是从这获取的Groups/Sequences进行校验)
- Groups和Sequences只会执行一种方式,默认Groups,有Sequences就不用Groups
- 对于Sequence, 默认failFast, 可以在 Validation.byProvider(HibernateValidator.class).configure().failFast(false)配置
- Group/Sequence校验
- 第一步:校验本身
- 对于bean,就是先校验bean字段/属性上的(值是字段值),再校验bean类上的(值是该bean)
- 对方法,就是先校验Cross参数(值是所有参数),再校验单个参数上的(值是参数值)
- 统一校验目标上有多个,不确定顺序
- 第二步:再校验validateCascadedConstraints
- 即校验有Valid的方法参数的属性/有Valid的bean的属性的属性
- 使用规范
- 普通校验写在属性/参数上
- 多参数校验写在交叉校验的方法上(通常是不都为空,都为空等这样的校验)
- 对一个bean的某些属性进行比如Io校验, 就在方法的参数上加注解
- 理由:spring提供的方法AOP类MethodvalidatedInterceptor是对执行方法时才进行方法校验,而对于属性/字段的校验在执行之前进行的,因此io操作是在最后执行的,并没有浪费性能
- 另外:如果是一个bean对象,而且还想对其进行方法校验
- 在spring提供的情况下,会在bean校验一次,在方法的联级校验阶段校验一次
- 使用:需要给这个参数javax.validation.Valid或者Validated或者startsWith("Valid")的其中之一的注解进行bean校验
- 但是Validator的方法参数校验逻辑是会进行联级校验,如果参数有Valid注解,就会进行联级校验,不过我使用spring的Validated就好了
Group
public TestLog {
String value();
String message() default "TestLog ";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
List {
TestLog[] value();
}
}
public class TestLogValidator implements ConstraintValidator<TestLog, Object> {
private Object annotationValue;
public void initialize(TestLog testInit) {
this.annotationValue = testInit.value();
}
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 { }
interface TestSequence { }
interface Circle{
interface A extends Test { }
interface B{ }
interface Test { }
}
interface TestSequenceDefault { }
}
public static class Bean{
private String f1;
private String f2;
private String f3;
private String f4;
}
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){
}
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));
}
public static class BeanConvertGroup{
private Bean bean;
}
//还可以动态调整顺序, 类似于自定义Validator
//@GroupSequenceProvider(XXXGroupSequenceProvider.class) implements DefaultGroupSequenceProvider<T>
//https://docs.jboss.org/hibernate/validator/8.0/reference/en-US/html_single/#_groupsequenceprovider
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));
}
public static class BeanReplaceDefault{
private String f1;
private String f2;
private String f3;
}
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));
}
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));
}
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));
}
}
正常需求例子
- 注意:在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
*/
public class NormalController {
public static class Bean {
private String name;
private String name2;
private String name3;
}
/**
* 重复校验反例
*/
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();
}
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 {}
}
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();
}
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个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
作者分享
感觉有点自恋了,有几个招聘要求写得感觉就是我,都是说积极参与开源的,可能是因为我只提过3个非常简单的pr和一个文档pr,所以都不鸟我😞
3
刚才收到一个面试邀请,但说明了如果顺利,入职时间是11长假之后,兄弟们,你说这个HR是不是闲的无聊来玩我啊🥲
3
有谁比较了解大四实习啊。我到现在还没找到工作,导员通知8/21下午前还没填就延毕一年,我问老师他说得和专业有关的工作,我是计算机科学与技术的😭
2
类型:跨域BUG
环境:springboot 2.7.12,Chrome 127.0.6533.100(正式版本) (64 位)
描述:找了几个小时,最后才发现现在Cookie必须设置SameSite=None/Script,总之不能是默认的Lax,SameSite=None是Secure必须为true(localhost时允许http),这样才允许跨域😢
2
星球gitlab又挂了吗?
1
