不报错 ≠ 生效:三类"静默失效"缺陷与它们的共同指纹

不报错 ≠ 生效:三类"静默失效"缺陷与它们的共同指纹

主题:请求校验与静默失效

先说结论

有一类 bug 特别阴险:代码/配置"看着改了",但在运行时根本没起作用 ------ 而且不抛异常、不打日志。

它们能潜伏很久,因为平时看不出来。我遇到的是三类形态:

# 形态 典型
① 触发机制缺失 校验规则写得齐齐整整,但漏了触发注解 → 规则从不生效
② 异常处理器缺失 校验失败没有对应 handler → 落到兜底分支,返回 500 而不是 400
③ 隐式依赖运行环境 LocalDate.now() 取 JVM 默认时区(容器是 UTC)→ 业务时间整体偏移

案例一:时区 bug ------ 日预算在早上 8 点被重置

现象

一个"每日预算"功能,本该零点重置 ,实际在北京时间早上 8:00 重置。

根因

java 复制代码
// 修复前
private String buildKey() { return KEY_PREFIX + LocalDate.now(); }

LocalDate.now() 用的是 JVM 默认时区 ------ 而容器默认是 UTC:

vbnet 复制代码
UTC 的"一天"从 UTC 0 点开始 = 北京时间早上 8:00
→ 每日预算 key 在北京时间 8:00 切换

修复(~10 行)

java 复制代码
// 修复后 ------ 显式指定业务时区,与运行环境解耦
private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
private String buildKey() { return KEY_PREFIX + LocalDate.now(ZONE); }
private long getSecondsUntilMidnight() {
    ZonedDateTime now = ZonedDateTime.now(ZONE);
    ZonedDateTime midnight = now.toLocalDate().plusDays(1).atStartOfDay(ZONE);
    return Duration.between(now, midnight).getSeconds();
}

为什么长期没被发现

这个模块 0 调用、预算从未打满 → 重置时间错了也无人感知。

⭐ "没触发"不等于"没 bug"。

企业规范 :所有"按天/按小时"的业务 key,日期计算必须显式传 ZoneId ;更规范的是服务统一 spring.jackson.time-zone + 数据库存 UTC/时间戳,展示层转本地。


案例二:校验静默失效 ------ 而且"审计结论本身也是错的"

第一层:分清"漏触发"和"没规则"

原审计结论是"5 个接口漏触发校验注解"。我逐个回代码核查,发现真相分三类:

类 情况 真相 正确处理
A DTO 里规则齐全,但 @RequestBody 前漏了 @Valid 真 bug:规则在但不触发 补 @Valid
B DTO 里压根没有任何规则(import 被注释掉) 不是"漏触发",是"根本没规则" 先设计规则,再触发
C 同上 同上 同上

⭐ 核心认知:审计"校验失效"必须先分清「规则存在但没触发」与「规则本身不存在」------ 对后者补触发注解是空转。 审计报告是"线索"不是"结论",必须逐项回到代码核实。

第二层(更关键):补了 @Valid,校验仍然"假生效"

连带发现 :全局异常处理器只处理了 BindException (旧的表单场景),而 MethodArgumentNotValidException 和 ConstraintViolationException 没有 handler → 落到兜底分支 → 校验失败返回 500 而不是 400!

这意味着 :如果只补 @Valid 而不补异常处理器,校验"看起来修好了",实际上用户看到的是"服务器错误",前端也无法区分"参数错"还是"服务挂了"。

补全(两个 handler):

java 复制代码
@ExceptionHandler(MethodArgumentNotValidException.class)   // @RequestBody + @Valid 失败
// 取 bindingResult.getFieldError().getDefaultMessage() → 返回 400 + 友好提示

@ExceptionHandler(ConstraintViolationException.class)      // 参数级/类级校验失败
// 取 constraintViolations.stream().findFirst().getMessage() → 返回 400

三类校验异常都要有 handler:

触发方式 异常类型
@RequestBody + @Valid MethodArgumentNotValidException
参数级/类级 @Validated ConstraintViolationException
表单绑定 BindException

第三层:校验的表达边界(设计层面)

边界 说明
bean validation 表达不了"或"关系 如"手机/固话二选一" → ① 两者都放宽为"有则验格式" ② Service 层兜底"至少一个非空"(本项目采用)
编辑接口不能照抄新增的全必填 编辑走 MyBatis-Plus 动态 SQL(只更新非 null)→ 强制 @NotBlank 会误伤合法的部分更新
@Valid vs @Validated 包不同;@Valid 支持嵌套/级联校验,@Validated 支持分组;GET 绑定的 DTO 对象参数 需参数级/类级 @Validated(对 @RequestParam/@PathVariable 生效)

附带产出:全项目覆盖审计

把"点"的问题变成"面"的治理清单 ------ 39 个 @RequestBody 接口全覆盖分类:

类 数量 处理
真 bug(有规则无触发) 1 ✅ 补 @Valid
无触发 + DTO 无规则 3 ✅ 补规则 + 触发
有触发 + DTO 无规则(注解空转) 2 ✅ 补规则
正常(触发+规则都有) 31 ✅ 复核
String body 豁免 1 ✅

这一类问题的共同指纹

指纹 说明
⭐ 不报错、不打日志 所以能潜伏很久 ------ "平时看不出来"是它们的共同特征
⭐ "配置/代码存在"给人虚假的安全感 规则写在 DTO 里、机制写在配置里 ------ review 时很难注意"少了触发/少了开关"
⭐ 500 而不是 400(或 429)是"handler 没接住"的典型信号 本项目在两个地方遇到同一个信号:校验异常没 handler、限流 blockHandler 签名不匹配
⚠️ 审计结论本身也会错 必须回到代码逐项核实
⚠️ 时间计算不能依赖运行环境时区 "时间"必须带时区语义
⚠️ 环境变量优先级会埋雷 IDE Run Configuration > 系统/用户变量 > .env > yml 占位符默认值 ------ 排查"配置不生效"要先查 IDE(本项目真踩过:IDE 里残留的旧 key 覆盖了一切,重启 IDE 也不变)

排查顺序建议 :先直连验证 (排除配置/网络)→ 再加临时脱敏日志 看运行时实际值 → 别猜。


一句总结

补一半比不补更危险 ------ 只补 @Valid 不补异常 handler,校验依然是"假生效"。"不报错 ≠ 生效"应该成为一条自查清单。


本文基于一个个人微服务电商演示项目的真实实施记录整理,涉及的环境信息已做脱敏处理。

相关推荐
房西杭1 小时前
整理背包 SortBag 完整实现逻辑
后端
Marcus学原理1 小时前
Playwright 入门:让浏览器自动完成重复操作
后端
弦儿1 小时前
后端 Web 实战:部门管理模块全流程(前后端分离 + RESTful + 三层架构 + 日志)
后端·restful
房西杭1 小时前
背包<->仓库转移接口(BagMgr + StoreMgr,Lua)
后端
tntxia1 小时前
设计模式中模板方法的钩子使用
后端
我的div丢了肿么办1 小时前
go语言对路由中的业务逻辑进行抽离
后端·go
leo在掘金1 小时前
纯Rust重写Photoshop,一个月7.2万Star
后端·架构
韩振方1 小时前
Kubernetes 通过资源 requests 决定 Pending Pod 能否找到节点
后端
用户360055579001 小时前
Kestrel_VLLM - 18 · 投机解码位级对拍收口:batch 与 M=1 一致,注意力路径与 CPU 逐字节对齐
后端