# V1.1 非结构写入收敛 — 整改方案 > 版本:v1.1 > 日期:2026-08-03 > 状态:**已落地** > 依据:业务仓实跑误报 + 单测复现 `RecordingTodoReplaceScenarioTest` > 关联:`docs/redis序列化结构检测实施方案.md`、`docs/配置说明.md` > 约束:**不新增配置项**;收敛为工具默认行为,随 jar 升级生效 --- ## 1. 背景与已证实问题 ### 1.1 实跑样本(jnpf-java-cloud) ```text 【序列化结构变更】 jnpf-java-cloud 分支: patrol/dev_patrol_2.0 提交: adbb15c6 → 6ddc72dc - Key --> recording:todo:replace:*:* 位置: RecordingTodoDomainServiceImpl#replaceText:437 类型: <未解析> value 新增为: “{}” - Key --> recording:todo:replace:title:*:* 位置: RecordingTodoDomainServiceImpl#replaceText:452 类型: <未解析> value 新增为: “{}” ``` 业务代码(摘要): ```java // 缓存替换前原文,纯 String,无 JSON / VO 序列化 stringRedisTemplate.opsForValue().set(contentRedisKey, originalContent); stringRedisTemplate.opsForValue().set(titleRedisKey, originalTitle); ``` ### 1.2 复现结论 单测 `RecordingTodoReplaceScenarioTest` 已稳定复现: | 环节 | 现状 | |------|------| | 检测 | 命中 **W04**(直写对象) | | 类型 | `String` 不在本仓 `SourceIndex` → `resolvedValueType = null` → 展示 `<未解析>` | | Schema | `extract(null)` 空结构 → 骨架 `"{}"` | | 报告 | 旧提交无该写入点 → `WRITE_POINT_ADDED` →「value 新增为」 | **核心缺口**:新增写入点(`WRITE_POINT_ADDED`)路径上,**没有对「无结构 / 不可展开」的 value 做收敛**;只要 AST 命中 W04/W05(及部分边缘写法),就会告警。 ### 1.3 本方案范围 | 纳入 | 不纳入(另案) | |------|----------------| | Redis 非结构写入误报收敛 | MQ `asyncSend` 漏检(作用域过窄) | | 同类场景枚举与统一收敛规则 | 新增 Key 企微文案 / 整段染色体验 | | 检测层 + 分析层双闸门设计 | 业务仓 ignore 配置补丁(仅作临时手段) | --- ## 2. 问题本质 工具目标是监控 **可 JSON/对象 Schema 化的序列化结构变更**。 当前实现把「能调用 `set/insert/put`」近似当成「值得监控」,收敛只做了很窄的一层: ```text 现有收敛 ├─ 方法黑名单:setIfAbsent / increment / delete … ├─ isTrivialValue:字面量、UUID、toString/valueOf/randomUUID └─ ignore.key_patterns:lock / loginCount / Authorization … 缺失收敛 ├─ value 声明类型为 String / 数字 / 布尔 / byte[](变量,非字面量) ├─ value 类型无法在本仓展开(null / 空 Schema) ├─ 先 JSON 序列化到局部 String 再写入(Redis 未 unwrap,误成 String-W04) └─ WRITE_POINT_ADDED 不检查「是否有可对比结构」 ``` 因此:**凡是「新增一段无结构写入」都会变成「类型未解析 + value 新增为 {}」**,与 RecordingTodo 同形。 --- ## 3. 同类场景排查(是否会中招) 判定标准:能否在默认配置(W01~W05 全开)下产出与实跑同形的告警 (`WRITE_POINT_ADDED` + `<未解析>` 或空/`{}` 骨架,或明显无业务结构)。 ### 3.1 高概率同形(与实跑同类,应优先收敛) | ID | 场景 | 典型代码 | 命中模式 | 结果形态 | 与实跑关系 | |----|------|----------|----------|----------|------------| | S1 | **String 变量直写** | `stringRedisTemplate.opsForValue().set(k, originalContent)` | W04 | `<未解析>` + `{}` | **已证实** | | S2 | **String 方法返回值直写** | `set(k, todo.getContent())` | W04 | 同 S1(返回类型 String 时) | 同形;`isTrivialValue` 不认 `getXxx` | | S3 | **数字 / 布尔变量直写** | `set(k, ttlSeconds)` / `set(k, enabled)` | W04 | `<未解析>` + `{}` | 同形;标量无字段 | | S4 | **Hash 写入 String/标量** | `opsForHash().put(k, f, nameStr)` | W05 | 同 S1 | W05 仅挡字面量,不挡变量 | | S5 | **redisUtil.insert 写 String 变量** | `redisUtil.insert(k, token, ttl)` | W04 | 同 S1 | `insert` + 含 redis 的 scope → 走 W04 分支 | | S6 | **外部依赖类型直写** | `set(k, externalDto)` 且 DTO 不在本仓 | W04 | `<未解析>` + `{}` | 同形;「有对象语义」但工具无法展开,告警无信息量 | | S7 | **类型擦除 Object / 原始 Map** | `set(k, (Object) x)` / `set(k, map)` 且无法解析元素 | W04 | 常为 `<未解析>` / 空或弱 Schema | 同形或近似 | ### 3.2 中概率变形(会误报,但表现略不同) | ID | 场景 | 典型代码 | 命中模式 | 结果形态 | 说明 | |----|------|----------|----------|----------|------| | S8 | **局部 JSON 字符串再写入** | `String json = JSON.toJSONString(vo); set(k, json)` | W04 | 类型按 String → `<未解析>`+`{}` | Redis **无** `unwrapJsonStringVar`(MQ 已有);应归 W01~W03,却被当成无结构 | | S9 | **显式序列化标量** | `set(k, JSON.toJSONString(someString))` | W02 | 偶发低价值点 | 少见;unwrap 后仍是 String,应丢弃 | | S10 | **本仓 VO 但字段全 transient / 全忽略注解** | `set(k, emptyVo)` | W04 | 类型有名,骨架仍可能 `{}` | 有类型名,但「新增 {}」仍噪音 | | S11 | **删除无结构写入点** | 删掉 S1 类代码 | WRITE_POINT_REMOVED | 「原结构 {}」 | 对称噪音,收敛规则应一并覆盖 | ### 3.3 低概率 / 已部分防护(记录边界) | ID | 场景 | 现状 | 结论 | |----|------|------|------| | S12 | 字面量 `"1"` / `UUID.toString()` | `isTrivialValue` 已忽略 | 一般不中招 | | S13 | key 命中 `*:lock` / `loginCount:*` 等 | `ignore.key_patterns` | 仅覆盖命名约定,**挡不住** `recording:todo:replace:*` | | S14 | MQ 直传 String / byte[] | `isBareStringOrBytesPayload` 已忽略 | Redis 应对齐;MQ 数字变量仍可能漏(见下) | | S15 | MQ 直传业务 VO | 正常监控 | 不属本问题 | | S16 | Redis W01~W03 + 本仓 VO | 正常监控 | 不属本问题 | ### 3.4 MQ 侧对照(同类思想,程度不同) | 点 | Redis(现状) | MQ(现状) | |----|---------------|------------| | 裸 String / byte[] 变量 | **不忽略** → S1 | **忽略** | | 数字 / 布尔变量 | **不忽略** → S3 | **不忽略**(仅 trivial 字面量)→ **MQ 也有 S3 风险** | | 局部 `toJSONString` 再 send/set | Redis **不 unwrap** → S8 | **已 unwrap** | | 无法展开类型仍建点 | 会建点 → S6 | 会建点(直传对象时) | | WRITE_POINT_ADDED 空 Schema 闸门 | **无** | **无**(同样会「新增 {}」) | 结论: - Redis 是重灾区; - MQ 在 String 上已收敛,但 **Integer/Long/Boolean 变量** 与 **空 Schema 仍告警** 与 Redis 同类,应在统一规则里一并处理。 ### 3.5 分析层放大效应(与模式无关) `SchemaCheckAnalyzer` 在「新写入点、旧无配对」时: ```text fileChanged && oldWritePoint == null → 无条件 WRITE_POINT_ADDED → extract(type) 即使为空也 render → "{}" → 进入企微 ``` 对比「已有写入点」路径:`differ.diff` 空则 `continue`,**不会**为空 Schema 刷屏。 因此同类问题在 **新增代码** 时被放大;存量无结构写入若从未变更,不一定出告警。整改必须同时考虑: 1. **检测层少建点**(源头) 2. **分析层对无结构新增/删除做闸门**(兜底) --- ## 4. 收敛规则设计 ### 4.1 原则 1. **只监控有结构的序列化 value**(本仓可展开的业务类型,或 unwrap 后得到的业务类型)。 2. **宁漏勿滥**:无法证明「有结构」则不告警。 3. **检测层为主、分析层为辅**:避免脏 WritePoint 进入报告链路。 4. **Redis / MQ 规则对齐**,避免一边收敛一边漏。 ### 4.2 规则表(建议落码) | 规则 | 适用 | 行为 | |------|------|------| | R-A 标量类型忽略 | Redis W04/W05;MQ 直传 payload | value/payload 声明类型 ∈ `{String, CharSequence, 原始/包装数字, Boolean, UUID, byte/Byte/byte[]}` → **不建点** | | R-B 本仓可展开 | Redis W04/W05;MQ 直传对象 | `resolvedFqn == null` 或 `SourceIndex` 无此类 → **不建点** | | R-C 空 Schema 闸门 | Analyzer:ADDED / REMOVED | `extract` 后 `schema.isEmpty()` → **不产生** WRITE_POINT_ADDED/REMOVED(字段级 diff 路径保持:空 diff 已 skip) | | R-D 局部 JSON unwrap | Redis(对齐 MQ) | `String json = toJSONString(x); set/insert(k, json)` → 按 W01~W03 建点,类型为 `x`;若 `x` 仍为标量 → 再走 R-A | | R-E 方法返回值 | 与 R-A/R-B 相同 | `set(k, obj.getFoo())` 按方法返回类型判定,不因「非字面量」直接放行 | | R-F 集合 | W04/W05 / MQ | `List` / `Set`:元素 `T` 满足 R-B 才保留;`List` 丢弃 | ### 4.3 明确保留(不应误杀) | 写法 | 期望 | |------|------| | `set(k, JSON.toJSONString(vo))` / `insert` + 序列化 | W01~W03,监控 VO | | `redisTemplate.set(k, demoVo)` 且 DemoVo 在本仓 | W04,监控 | | `opsForHash().put(k, f, demoVo)` 本仓 VO | W05,监控 | | MQ `syncSend/asyncSend(topic, dto)` 本仓 DTO | 监控 | | 局部 JSON unwrap 后得到本仓 VO | 监控 | ### 4.4 配置策略 **不新增任何 YAML / 检测开关。** R-A~R-F 均为工具内置默认行为,升级 jar 即生效。 业务侧若需临时止血,仍可沿用既有 `ignore.key_patterns` / `ignore.writer_methods` / `suppressions`,但不作为本问题根治手段。 --- ## 5. 落地设计 ### 5.1 推荐分层 ```text ┌─────────────────────────────────────────┐ │ RedisWritePointDetector / MqWritePointDetector │ R-A 标量忽略 │ R-D Redis 局部 JSON unwrap │ R-B 本仓类型门槛(直写对象) │ R-E/R-F 返回值与集合元素 └──────────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────────┐ │ SchemaCheckAnalyzer │ R-C 空 Schema → 跳过 ADDED/REMOVED │ (字段级 diff 逻辑不变) └─────────────────────────────────────────┘ ``` ### 5.2 涉及文件(实施时) | 文件 | 变更要点 | |------|----------| | `RedisWritePointDetector.java` | R-A / R-B / R-D / R-E / R-F | | `MqWritePointDetector.java` | R-A 扩展数字/布尔;直传对象可加 R-B(与 Redis 对齐) | | `SchemaCheckAnalyzer.java` | R-C 空 Schema 闸门 | | (可选)抽取 `BareValueTypes` 工具类 | Redis/MQ 共用标量集合,防漂移 | | `RecordingTodoReplaceScenarioTest` | 修复后期望 **0 写入点**(或拆「修复前/后」) | | 新增场景单测 | 覆盖 §3.1 / §3.2 表中 S2~S8、S11 | | `docs/配置说明.md` | 同步 W04/W05 与空 Schema 行为 | ### 5.3 实施顺序建议 本版本一次性落地 R-A~R-F(Redis + Analyzer + MQ 对齐),无分阶段开关、无灰度配置。 --- ## 6. 测试与验收 ### 6.1 复现用例(已有) - `RecordingTodoReplaceScenarioTest`:当前断言「会误报」。 - 整改后应改为:检测结果为空,或 Analyzer 不产出 keyChanges。 ### 6.2 建议补充用例 | 用例 | 期望(整改后) | |------|----------------| | `set(k, getContent())` 返回 String | 0 点 | | `set(k, longVar)` | 0 点 | | `opsForHash().put(k, f, strVar)` | 0 点 | | `redisUtil.insert(k, tokenStr, ttl)` | 0 点 | | `set(k, externalDto)` 不在 SourceIndex | 0 点 | | `String json = toJSONString(vo); set(k, json)` | 1 点,类型 = VO,W02/W01 | | `set(k, demoVo)` 本仓 VO | 仍 1 点 W04 | | 删除 String 直写 | 不产生 WRITE_POINT_REMOVED | | MQ `send(topic, longVar)` | 0 点(Phase 4) | ### 6.3 回归 - `TenantScenarioTest`、既有 W01~W05 / MQ fixture 全绿。 - 业务仓抽样:RecordingTodo 类告警消失;真实 VO 包装层变更仍能检出。 --- ## 7. 临时手段与风险 | 项 | 说明 | |----|------| | 无新配置 | 不增加 `keep_empty_schema_*` 等开关;行为固化在检测/分析逻辑 | | 业务 ignore | 可对 `recording:todo:replace:*` 加 key 忽略,仅止血,不解决 S1~S7 类问题 | | 误杀风险 | 依赖 jar 内 DTO 的 W04 将被忽略 → 可用既有 `manual_mappings` 或把类型源码纳入仓 | | Object 擦除 | 可能漏检真实结构 → 接受宁漏勿滥;或要求业务写明 VO 类型 | | 与「展示优化」关系 | 即使将来给新增 Key 染色,空 `{}` 仍无意义;**必须先收敛再建点** | --- ## 8. 总结 | 问题 | 结论 | |------|------| | RecordingTodo 为何告警? | W04 把 String 原文当成对象写入;新增路径无空结构闸门 | | 是否只有这一处? | **否**。S2~S7 同形,S8 变形,S11 对称;MQ 在数字变量与空 Schema 上同类 | | 根因一句话 | **新增写入检测缺少「有结构才告警」的收敛** | | 整改抓手 | 检测层标量/本仓类型收敛 + Analyzer 空 Schema 闸门 + Redis JSON 局部 unwrap | --- ## 9. 检查表 - [x] 认可 R-A~R-F 与「宁漏勿滥」、**不新增配置** - [x] 外部 jar DTO(S6)默认不监控 - [x] 代码落地 + `RecordingTodoReplaceScenarioTest` 期望 0 点 - [x] 同步 `配置说明.md` - [x] 全量 `mvn test` 通过