拒绝 DTO 爆炸:详解 Spring Boot 参数校验中的“分组校验” (Validation Groups) 技巧

一、 为什么要用分组校验?(痛点)

在日常开发中,我们经常遇到同一个 DTO(数据传输对象)在不同场景下有不同校验规则的情况。

典型场景:用户管理

  • 新增用户ID 必须为空(因为是自增的),但 Password 必填。
  • 修改用户ID 不能为空(指定改谁),但 Password 选填(不改就不传)。

如果没有分组校验,我们通常有两种"笨办法":

  1. 定义两个 DTOUserCreateReqVOUserUpdateReqVO
    • 缺点:类爆炸,大量重复字段,维护麻烦。
  2. 去掉校验注解 :在 DTO 里不写 @NotNull,全靠在 Service 层手写 if (id == null) 判断。
    • 缺点:代码臃肿,失去了注解校验的优雅。

分组校验(Groups) 就是为了解决这个问题------用一个 DTO,搞定多种场景。


二、 核心步骤(用法总结)

使用分组校验只需要三步:定义分组 -> 标记规则 -> 触发校验

1. 定义分组接口 (The Marker)

这只是一个普通的 Java 接口,不需要任何方法,它的唯一作用就是做一个"标签"。

java 复制代码
public class UserReqVO {
    // 定义两个分组接口
    public interface Create {}
    public interface Update {}
}
2. 在字段上标记分组 (The Rule)

在校验注解中配置 groups 属性。

java 复制代码
@Data
public class UserReqVO {

    @Schema(description = "用户ID")
    @Null(groups = Create.class, message = "新增时ID必须为空")
    @NotNull(groups = Update.class, message = "修改时ID不能为空")
    private Long id;

    @Schema(description = "用户名")
    @NotBlank(message = "用户名不能为空") // 没有指定 groups,属于默认分组 (Default)
    private String username;

    @Schema(description = "密码")
    @NotBlank(groups = Create.class, message = "新增时密码不能为空")
    private String password;
}
3. 触发校验 (The Trigger)

这是最关键的一步。你必须告诉 Spring,当前这次请求,你要检查哪个分组。

注意:必须使用 @Validated 注解,@Valid 不支持分组。

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    // 场景 A:新增 -> 触发 Create 分组
    @PostMapping("/create")
    public Result create(@RequestBody @Validated(UserReqVO.Create.class) UserReqVO req) {
        return success(userService.create(req));
    }

    // 场景 B:修改 -> 触发 Update 分组
    @PutMapping("/update")
    public Result update(@RequestBody @Validated(UserReqVO.Update.class) UserReqVO req) {
        return success(userService.update(req));
    }
}

三、 进阶:如何处理"默认规则"?(坑点预警)

这里有一个初学者极容易踩的坑。

问题:

在上面的例子中,username 字段只加了 @NotBlank,没有加 groups

当你使用 @Validated(Create.class) 时,Spring 只会检查标记了 Create.class 的字段,username 会被忽略!

解决方案:让分组接口继承 Default

如果你希望在检查 Create 分组时,也顺便检查那些没有分组的默认字段,可以这样定义接口:

java 复制代码
import javax.validation.groups.Default;

public class UserReqVO {
    // 让 Create 分组继承 Default 分组
    public interface Create extends Default {}
    
    public interface Update extends Default {}
}

这样,@Validated(Create.class) 就会同时检查:

  1. 标记了 Create 的规则。
  2. 没有标记 group 的规则(归属于 Default)。

四、 高阶实战:业务逻辑中的动态校验

除了在 Controller 自动校验,我们有时需要在 Service 层根据业务开关动态校验(例如:验证码开关)。

场景: 系统配置了"开启验证码"开关。开启时校验,关闭时不校验。

代码示例:

java 复制代码
@Service
public class AuthService {

    @Resource
    private Validator validator; // 注入 Java 标准校验器

    public void login(LoginReqVO req) {
        // 1. 动态判断业务开关
        if (isCaptchaEnabled()) {
            // 2. 手动触发特定分组 (CodeEnableGroup) 的校验
            Set<ConstraintViolation<LoginReqVO>> errors = 
                validator.validate(req, CodeEnableGroup.class);

            // 3. 如果有异常,手动抛出
            if (!errors.isEmpty()) {
                throw new ServiceException("验证码不能为空");
            }
        }
        // ... 其他逻辑
    }
}

五、 总结

Validation Groups (分组校验) 是 Spring Boot 开发中精简代码的神器。

  1. 本质:给校验注解加上"条件判断"。
  2. 核心 :通过空接口定义分组 (interface GroupA {})。
  3. 使用 :在注解中指定 groups = GroupA.class
  4. 触发
    • 自动 :Controller 层使用 @Validated(GroupA.class)
    • 手动 :Service 层使用 validator.validate(obj, GroupA.class)
  5. 最佳实践 :让自定义分组继承 Default 接口,避免漏掉通用校验规则。
  6. 缺点:创建与更新复用一个类,高度耦合;需要在Controller中指定class参数;无法实现复杂的方法级别的逻辑校验。有一种平替方案可以见@AssertTrue。
相关推荐
程序员清风5 小时前
北京回长沙了,简单谈谈感受!
java·后端·面试
lucky67075 小时前
Spring Boot集成Kafka:最佳实践与详细指南
spring boot·kafka·linq
何中应5 小时前
请求头设置没有生效
java·后端
NPE~5 小时前
自动化工具Drissonpage 保姆级教程(含xpath语法)
运维·后端·爬虫·自动化·网络爬虫·xpath·浏览器自动化
Coder_Boy_5 小时前
基于Spring AI的分布式在线考试系统-事件处理架构实现方案
人工智能·spring boot·分布式·spring
亓才孓5 小时前
[JDBC]批处理
java
春日见5 小时前
车辆动力学:前后轮车轴
java·开发语言·驱动开发·docker·计算机外设
宋小黑6 小时前
JDK 6到25 全版本网盘合集 (Windows + Mac + Linux)
java·后端
念何架构之路6 小时前
Go进阶之panic
开发语言·后端·golang
7哥♡ۣۖᝰꫛꫀꪝۣℋ6 小时前
Spring-cloud\Eureka
java·spring·微服务·eureka