Spring Boot 加了 @Valid,嵌套参数为什么还没校验?

Spring Boot 加了 @Valid,嵌套参数为什么还没校验?

开篇

请求对象里包着登录参数,用户名明明为空,却没有提示必填。嵌套对象需要级联校验,而校验本身也得被调用。只在字段上写注解,不能保证所有入口都会自动执行。

环境:JDK 21、Spring Boot 3、Spring Framework 6、Jakarta Validation 3、Hibernate Validator 8、Lombok 1。下面的请求包装类和参数处理器均为应用自有类型。

坑点

坑点一:约束写在里面,外层却没有级联入口

外层检查通过,不代表内部属性也检查过。下面这个包装类在 bizParam 上同时使用 @NotNull 和 @Valid:

java 复制代码
@Data
@Schema
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class InternalBizParamReq<T extends Param> extends InternalReq {

    @NotNull
    @Valid
    @Schema(title = "业务参数", requiredMode = REQUIRED)
    private T bizParam;
}

@NotNull 检查业务参数对象是否存在;@Valid 让校验器继续检查对象内部。只有 @Valid 时,null 本身通常不会因为级联而被拒绝。

坑点二:处理器拿到空校验结果,就以为参数已经合法

读取 BindingResult 的处理器不会主动创建校验结果。没有触发校验,或者没有把结果传进来,它就无从判断内部字段是否合格。

正确写法

避坑一:外层负责级联,内部负责字段约束

业务参数各自声明自己的规则,避免在包装类里写用户名、密码等业务知识:

java 复制代码
@Data
@Schema
public class LoginParam implements Param {
    @NotBlank
    @Schema(title = "用户名", requiredMode = REQUIRED)
    private String userName;

    @NotBlank
    @Schema(title = "密码", requiredMode = REQUIRED)
    private String password;
}

调用示例直接使用校验器,方便把"注解有没有生效"与 HTTP 参数绑定分开观察。父类的 provider、consumer 也是必填项,因此先补齐它们:

java 复制代码
InternalBizParamReq<LoginParam> request = new InternalBizParamReq<>();
request.setProvider("account-service");
request.setConsumer("demo-client");

LoginParam login = new LoginParam();
login.setPassword("demo-password");
request.setBizParam(login);

try (ValidatorFactory factory = Validation.byDefaultProvider()
        .configure()
        .messageInterpolator(new ParameterMessageInterpolator())
        .buildValidatorFactory()) {
    Validator validator = factory.getValidator();
    System.out.println(validator.validate(request).stream()
            .map(v -> v.getPropertyPath().toString())
            .sorted().toList());
    // [bizParam.userName]

    login.setUserName("demo");
    System.out.println(validator.validate(request).isEmpty());
    // true

    request.setBizParam(null);
    System.out.println(validator.validate(request).stream()
            .map(v -> v.getPropertyPath().toString())
            .sorted().toList());
    // [bizParam]
}

ParameterMessageInterpolator 来自 Hibernate Validator,示例用它避免依赖表达式语言引擎;这是直接调用验证器的局部测试,不是一次真实 HTTP 请求。

避坑二:先触发校验,再让处理器读取结果

处理器的执行逻辑如下。它拿到 FieldError 后,区分自定义提示、必填缺失和格式错误:

java 复制代码
    @Override
    public Resp preHandle(AspectInfo aspectInfo) {
        // 从参数列表中提取BindingResult
        BindingResult bindingResult = aspectInfo.findParam(BindingResult.class);
        if (bindingResult == null || !bindingResult.hasErrors()) {
            return Resp.ok();
        }

        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            String message = fieldError.getDefaultMessage();
            String fieldName = fieldError.getField();

            //自定义的message以@开头
            if (Strings.CS.startsWith(message, "@")) {
                Resp resp = Resp.error(ErrorCode.PARAM_INVALID);
                resp.setMessage(StringUtils.substringAfter(message, "@"));
                return resp;
            }

            //必填参数未传
            if (Strings.CS.equalsAny(fieldError.getCode(), PARAM_REQUIRED_CODES)) {
                return Resp.error(ErrorCode.PARAM_REQUIRED, fieldName);
            }

            //参数格式不对
            return Resp.error(ErrorCode.PARAM_INVALID, fieldName);
        }

        return Resp.ok();
    }

实际接在 MVC 控制器时,应由 @Valid 等机制触发参数校验。需要 BindingResult 时,它的位置要紧跟被校验的参数,且请求要经过对应的切面入口。

下面是接入形式示例,不是完整控制器;其中操作仍由业务方法完成:

java 复制代码
public Resp submit(
        @Valid @RequestBody InternalBizParamReq<LoginParam> request,
        BindingResult bindingResult) {
    return Resp.ok();
}

这个方法签名本身不包含路由、切点和业务逻辑,不能复制后就当成完整接口。没有经过切面的入口,应由控制器或其他既有机制处理 BindingResult,不能直接返回成功。

还要注意:字符串里装着的 JSON 不是嵌套 JavaBean。若先接收字符串、再自行反序列化出业务对象,就要确认后续确实调用了校验器;外层 @Valid 不会自动钻进一段字符串校验其 JSON 内容。

总结

嵌套校验要同时满足三件事:外层有级联标记,内部有约束,调用链真正触发验证。最后的错误处理器只负责解释结果,不会替前面的步骤补做校验。


如果这篇文章对您有用,欢迎关注。后续会继续拆解 Java 后端的实用代码,讲清写法、原理和使用时要注意的细节。

相关推荐
半杯咖啡半行码1 小时前
QML与 Qt Quick入门实战:从零基础到 C++ 双向通信详解
java·qt·交互
霸道流氓气质1 小时前
Lingua 语言检测库:n-gram 统计模型、置信度阈值调优与 Spring Boot 分层检测管道的生产级实战
java·spring boot·后端
文丑颜不良啊2 小时前
LeetCode1281~1300题目解析
java·leetcode
missile1112 小时前
# HDR 算法 ISP 通路客制化方案(G100 / mt6789)
java·前端·javascript
geovindu2 小时前
rust: Read-Write Lock Pattern
开发语言·后端·设计模式·rust·读写锁模式·同步型模式
怪我冷i2 小时前
Rust智能指针Rc<T> 使用总结
开发语言·后端·rust
全栈陈序员2 小时前
【Python】基础语法入门(三十三)—— 日志系统详解:用 logging 记录程序运行
java·开发语言·python
万少2 小时前
Harmony 7.0 这个skill帮你自动化操作AGC
前端·javascript·后端
她的男孩2 小时前
低代码加字段后表结构怎么同步:预览安全 DDL,高风险变更再强制执行
java·spring boot·低代码