@Valid 和 @Validated 区别和用法及组合

常用注解

@AssertFalse 限制必须为false
@AssertTrue 限制必须为true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
@Future 限制必须是一个将来的日期
@FutureOrPresent 未来或当前的日期,此处的present概念是相对于使用约束的类型定义的。例如校验的参数为Year year = Year.now();此时约束是一年,那么“当前”将表示当前的整个年份。
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@Negative 绝对的负数,不能包含零,空元素有效可以校验通过
@NegativeOrZero 包含负数和零,空元素有效可以校验通过
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotNull 限制必须不为null
@Null 限制只能为null
@Past 限制必须是一个过去的日期
@PastOrPresent 过去或者当前时间,和@FutureOrPresent类似
@Pattern(value) 限制必须符合指定的正则表达式
@Positive 绝对的正数,不能包含零,空元素有效可以校验通过
@PositiveOrZero 包含正数和零,空元素有效可以校验通过
@Size(max,min) 限制字符长度必须在min到max之间

区别和场景

@Valid可以实现嵌套校验,对于对象中引用了其他的对象,依然可以校验。注意:只有在引用对象非空的情况下才会校验,如有必要,可以@Valid和@NotNull搭配使用,确保其不为空。
@Validated可以对参数校验进行分组,例如一个对象里面有一个字段id,id在新增数据时可以为空,但是在更新数据时不能为空,此时就需要用到校验分组

@Validated的分组校验

例如有一个场景,更新项目信息,项目id是必须要传的,但是在新增项目时,id可以不传,新增和更新用的同一个实体对象,这个时候需要根据不同的分组区分,不同的分组采用不同的校验策略

@NotBlank(message = "ID不能为空", groups = {TestValidGroup.Update.class})
private String id;

如上,注解参数中存在一个groups,表示将该参数归为update组,可以指定一个参数属于多个组
Controller的代码如下,@Validated有一个参数值value,可以校验指定分组的属性,下面就是指定校验groups包含TestValidGroup.Update.class的属性,在ProjectDTO中只有id这个属性的groups满足条件,所以只会校验id这个参数

@RestController
@RequestMapping("/valid")
public class TestValidController {
 
    @PostMapping("/post")
    public BaseResponse testValidPostRequest(@Validated(value = {TestValidGroup.Update.class}) @RequestBody ProjectDTO testAnnotationDto) {
        return new BaseResponse(testAnnotationDto);
    }
}

group如何自定义,其实很简单,就是自己定义一个接口,这个接口的作用只是用来分组,自己创建一个接口,代码如下:
分别表示在新增和更新两种情况,可以按实际需求在内部添加多个接口

public interface TestValidGroup {
    interface Insert {
    }
    interface Update {
    }
}

注意:未显示指定groups的字段,默认归于javax.validation.groups包下的Default.class(默认组)
@Validated的value不指定组时,只校验Default组的字段
@Validated的value指定组时,只校验属于指定组的字段,属于Default组的字段不会被校验

若想指定组和默认组都被校验,有两种方式:
1、在@Validated的value中加入默认组,如下:

@PostMapping("/post")
public BaseResponse testValidPostRequest(@Validated(value = {TestValidGroup.Update.class, Default.class}) @RequestBody ProjectDTO testAnnotationDto) {
    return new BaseResponse(testAnnotationDto);
}

2、将指定Insert或Update接口继承Default接口,如下:

public interface TestValidGroup {
    interface Insert extends Default{
    }
    interface Update extends Default {
    }
 
}

@Validated中的分组校验时@GroupSequence使用(指定字段的校验顺序)

指定校验顺序就会用到@GroupSequence注解,这个注解使用在group的接口上,可以针对每一个参数都进行分组,然后通过该注解去指定顺序,代码如下,例如update时,校验的顺序就是先校验group属于Id.class的字段,再校验group属于StrValue的字段。

public interface TestValidGroup {
    @GroupSequence(value = {StrValue.class})
    interface Insert {
    }
 
    @GroupSequence(value = {Id.class, StrValue.class})
    interface Update {
    }
 
    interface Id {
    }
    interface StrValue {
    }
}

注意:此时不是校验group属于Update.class的字段,而是校验 group属于@GroupSequence的value中的那些接口(Id.class, StrValue.class) 的字段,如下
正确用法:

@NotBlank(message = "ID不能为空", groups = {TestValidGroup.Id.class})
private String id;

错误用法:

@NotBlank(message = "ID不能为空", groups = {TestValidGroup.Update.class})
private String id;

小知识:一个字段上存在多个注解时,例如@Max和@NotBlank,是按注解从上至下的顺序进行校验的。

快速失败机制(单个参数校验失败后,立马抛出异常,不再对剩下的参数进行校验)

实际情况中,有时候并不需要校验完所有的参数,只要校验失败,立马抛出异常,Validation提供了快速失败的机制,代码如下:

import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ValidConfig {
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                // 快速失败模式
                .failFast(true)
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}

全局异常处理,统一返回校验异常信息

项目中一般会针对异常进行统一处理,valid校验失败的异常是MethodArgumentNotValidException,所以可以拦截此类异常,进行异常信息的处理,捕获后的具体逻辑,自行实现,例子代码如下:
项目中一般@Valid和@Validated会组合使用,捕获BindException.class即可

@Slf4j
@RestControllerAdvice
public class ExceptionHandlerConfig {
    /**
     * 拦截valid参数校验返回的异常,并转化成基本的返回样式
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public BaseResponse dealMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("this is controller MethodArgumentNotValidException,param valid failed", e);
        List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
        String message = allErrors.stream().map(s -> s.getDefaultMessage()).collect(Collectors.joining(";"));
        return BaseResponse.builder().code("-10").msg(message).build();
    }
}

@Interface List的使用场景

有时候会出现这种需求,同一个字段在不同的场景下,需要采用不同的校验规则,并返回不同的异常信息,目前有两种方式,一种是采用@List的方式,一种是在字段上重复使用同一个注解,具体代码如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
public class BaseDTO {
 
    @NotBlank.List({
            @NotBlank(message = "项目BaseId不能为空", groups = {TestValidGroup.Project.class}),
            @NotBlank(message = "团队BaseId不能为空", groups = {TestValidGroup.Team.class})
    })
    private String baseId;
 
    @Max(value = 10, message = "项目BaseId不能大于10", groups = {TestValidGroup.Project.class})
    @Max(value = 30, message = "团队BaseId不能大于30", groups = {TestValidGroup.Team.class})
    private Integer number;
}

目的是通过指定注解归属于不同的分组来到达区分的效果。
Controller代码如下:

@RestController
@RequestMapping("/valid")
public class TestValidController {
    @PostMapping("/projectList")
    public BaseResponse projectList(@Validated(value = {TestValidGroup.Project.class}) @RequestBody BaseDTO baseDTO) {
        return new BaseResponse(baseDTO);
    }
 
    @PostMapping("/teamList")
    public BaseResponse projectTeam(@Validated(value = {TestValidGroup.Team.class}) @RequestBody BaseDTO baseDTO) {
        return new BaseResponse(baseDTO);
    }
}

@Valid和@Validated组合使用

@Validated和Valid肯定是可以组合使用的,一种是分组,一种是嵌套,单独使用的注意点已经在上面的部分写过,下面简单描述下在Controller代码中的使用,其实很简单,就是在实体类上加@Validated即可,内部的@Valid校验也会生效,代码如下:

@RestController
@RequestMapping("/valid")
public class TestValidController {
    @PostMapping("/post")
    public BaseResponse testValidPostRequest(@Validated(value = {TestValidGroup.Update.class, Default.class}) @RequestBody ProjectDTO testAnnotationDto) {
        return new BaseResponse(testAnnotationDto);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容