diff --git a/docs/工程结构.md b/docs/工程结构.md new file mode 100644 index 0000000..26f90c6 --- /dev/null +++ b/docs/工程结构.md @@ -0,0 +1,198 @@ +# 序列化结构检测器 — 工程结构说明 + +> 工程:`serialization-schema-checker`(`com.codechecker:serialization-schema-checker`) +> 定位:基于 JavaParser 的 **Redis 缓存 / MQ(RocketMQ、Kafka)value 序列化结构变更** 静态检测工具 +> 当前版本:`1.1.0` · Java 11 · 可执行 Fat Jar(maven-shade) + +--- + +## 1. 仓库顶层结构 + +```text +redisCheck/ +├── pom.xml # Maven 单模块工程 +├── .gitea/ # Gitea Actions 与示例/自检配置 +│ ├── workflows/ +│ │ └── serialization-schema-check.yaml +│ └── config/ +│ └── serialization-schema-check-config.yaml +├── docs/ # 方案、配置与集成文档(本目录) +├── src/ +│ ├── main/ +│ │ ├── java/com/codechecker/cache/ # 主代码(按职责分包) +│ │ └── resources/ +│ │ └── default-config.yaml # jar 内置默认配置 +│ └── test/ +│ ├── java/com/codechecker/cache/ # 单测 / 场景测 +│ └── resources/fixtures/ # 源码片段夹具 +└── target/ # 构建产物(不入库) +``` + +--- + +## 2. 主代码包结构 + +根包:`com.codechecker.cache` + +```text +com.codechecker.cache +├── cli/ # 命令行入口 +├── config/ # 配置模型与加载 +├── git/ # Git diff / show 封装 +├── analyze/ # 扫描编排与主分析流程 +├── detector/ # 写入点 / 读侧提示检测 +├── key/ # Redis key 表达式解析 +├── schema/ # 类型索引、字段 Schema、骨架 JSON +├── diff/ # Schema Diff 与变更类型 +├── report/ # 报告模型与渲染 +└── notify/ # 企微机器人通知 +``` + +### 2.1 包职责与主要类 + +| 包 | 职责 | 主要类 | +|----|------|--------| +| **cli** | picocli 入口;加载配置 → 分析 → 控制台报告 → 可选通知 | `SerializationSchemaCheckerMain` | +| **config** | 默认配置与业务配置深度合并;运行时配置模型 | `ConfigLoader`、`CheckerConfig` | +| **git** | 变更 Java 文件列表、按 SHA 取文件内容 | `GitDiffScanner`、`GitException` | +| **analyze** | 端到端编排:候选文件 → 写点检测 → Schema 提取 → Diff → 报告 | `SchemaCheckAnalyzer`、`FileScanner`、`GlobMatcher` | +| **detector** | 识别 Redis/MQ 写入(投递)点与读侧类型提示 | `RedisWritePointDetector`、`MqWritePointDetector`、`CacheReadHintDetector`、`MqReadHintDetector`、`BareValueTypes`、`WritePoint`、`CacheReadHint` | +| **key** | 将 key 表达式解析为模式(如 `demo:foo:*`) | `RedisKeyResolver` | +| **schema** | 源码类型索引、字段展开、骨架 JSON 渲染 | `SourceIndex`、`JavaSchemaExtractor`、`TypeSchema`、`FieldSchema`、`SkeletonJsonRenderer`、`AnnotationSupport`、`JsonType` | +| **diff** | 新旧 Schema 对比,产出变更明细 | `SchemaDiffer`、`SchemaChange`、`ChangeType`、`Severity` | +| **report** | 按 Key/Topic 聚合、企微 Markdown / 控制台文本 | `CheckReport`、`KeyStructureChange`、`ReportBuilder`、`SkeletonAnnotator` | +| **notify** | 企微机器人 Webhook 发送 | `WeComNotifier` | + +### 2.2 核心数据模型(实体) + +| 类 | 含义 | +|----|------| +| `WritePoint` | 一处 Redis 写入或 MQ 投递点(通道、key/topic、value 类型、位置) | +| `CacheReadHint` | 读侧反序列化推断出的类型提示(补强写入点,不单独告警) | +| `TypeSchema` / `FieldSchema` | value 类型展开后的扁平 Schema | +| `SchemaChange` | 单条结构变更(类型、路径、严重级别、说明) | +| `KeyStructureChange` | 按 Key/Topic 聚合的摘要(含前后骨架 JSON) | +| `CheckReport` | 一次检测的完整结果 | +| `CheckerConfig` | 运行配置(含 Notify / Ignore / Detection 等嵌套结构) | + +--- + +## 3. 运行时数据流 + +```text +SerializationSchemaCheckerMain + │ + ▼ + ConfigLoader.load(--config) + │ jar 内 default-config.yaml ⊕ 业务配置 + ▼ + SchemaCheckAnalyzer.analyze(oldSha, newSha) + │ + ├─ GitDiffScanner → 变更 .java 文件 + ├─ FileScanner → 读新旧源码内容 + ├─ SourceIndex → 建类型索引 + ├─ Redis/Mq WritePointDetector → 写入/投递点 + ├─ Cache/Mq ReadHintDetector → 读侧类型补强 + ├─ JavaSchemaExtractor → 展开 TypeSchema + ├─ SchemaDiffer → SchemaChange 列表 + └─ 聚合 KeyStructureChange → CheckReport + │ + ▼ + ReportBuilder (控制台 + 企微 Markdown) + │ + ▼ + WeComNotifier (可选,--dry-run 时跳过) +``` + +**退出码约定** + +| 码 | 含义 | +|----|------| +| `0` | 通过、跳过(`enabled=false` / 无基准 SHA)或 notify 模式有变更 | +| `1` | `mode=block` 且检测到结构变更 | +| `2` | 执行异常 | + +--- + +## 4. 资源与配置 + +| 路径 | 说明 | +|------|------| +| `src/main/resources/default-config.yaml` | 工具内置默认:检测模式、忽略规则、严重级别等 | +| 业务仓 `.gitea/config/serialization-schema-check-config.yaml` | 业务覆盖配置(流水线 `--config` 指向) | +| 本仓 `.gitea/config/serialization-schema-check-config.yaml` | 工具仓自用/示例配置 | + +配置合并与字段说明见:[配置说明.md](./配置说明.md) + +--- + +## 5. 测试结构 + +```text +src/test/java/com/codechecker/cache/ +├── TestSupport.java # 夹具加载等公共方法 +├── *ScenarioTest.java # 端到端场景(tenant / mq / recording-todo) +├── analyze/ / config/ / detector/ +├── diff/ / key/ / report/ / schema/ # 与主包对应的单测 +└── ... + +src/test/resources/fixtures/ +├── lock/ # 分布式锁等非结构写入样例 +├── template/ # 模板写入样例 +├── tenant/ # 包装层 / 字段迁移场景 +├── recording-todo/ # Redis 写入场景 +└── mq/ + ├── kafka-patrol/ # Kafka 巡检相关 + ├── kafka-record/ # Kafka Record 投递/消费 + ├── rocket-im/ # RocketMQ IM 通知 + └── rocket-wallet/ # RocketMQ 钱包扣款 +``` + +夹具多为 `.txt` 形式的 Java 源码片段,由场景测试拼装为新旧版本对比输入。 + +--- + +## 6. 构建与产物 + +| 项 | 说明 | +|----|------| +| 构建 | `mvn clean package` | +| 主类 | `com.codechecker.cache.cli.SerializationSchemaCheckerMain` | +| 产物 | `target/serialization-schema-checker-1.1.0.jar`(shade 可执行包) | +| 主要依赖 | JavaParser、SnakeYAML、Jackson、picocli、Lombok(provided)、JUnit 5(test) | + +--- + +## 7. 文档索引 + +| 文档 | 内容 | +|------|------| +| [工程结构.md](./工程结构.md) | 本文:目录与模块职责 | +| [配置说明.md](./配置说明.md) | 配置项、合并规则、忽略/抑制 | +| [CI集成说明.md](./CI集成说明.md) | 流水线接入方式 | +| [v1.0/](./v1.0/) | Redis / MQ 检测实施方案(历史方案) | +| [V1.1/](./V1.1/) | 非结构写入收敛、新增写入点展示等整改方案 | + +--- + +## 8. 模块依赖关系(简图) + +```mermaid +flowchart TB + CLI[cli] --> CFG[config] + CLI --> AN[analyze] + CLI --> RPT[report] + CLI --> NT[notify] + AN --> GIT[git] + AN --> DET[detector] + AN --> SCH[schema] + AN --> DIFF[diff] + AN --> RPT + DET --> KEY[key] + DET --> SCH + DIFF --> SCH + RPT --> DIFF + NT --> RPT +``` + +上层调用下层;`detector` / `schema` / `diff` 不依赖 `cli` 与 `notify`,便于单测与复用。 diff --git a/src/main/java/com/codechecker/cache/config/CheckerConfig.java b/src/main/java/com/codechecker/cache/config/CheckerConfig.java index ef16335..805e3bc 100644 --- a/src/main/java/com/codechecker/cache/config/CheckerConfig.java +++ b/src/main/java/com/codechecker/cache/config/CheckerConfig.java @@ -16,27 +16,37 @@ public class CheckerConfig { /** 总开关:false 时跳过检测与通知(流水线 exit 0) */ private boolean enabled = true; - /** notify | block */ + /** 运行模式:notify(仅通知)| block(有变更则失败退出) */ private String mode = "notify"; + /** 是否扫描 test 源码目录 */ private boolean scanTestSources = false; + /** 额外源码根路径(相对仓库根);空则按约定扫描 main */ private List sourceRoots = new ArrayList<>(); + /** 企微通知配置 */ private Notify notify = new Notify(); + /** 忽略规则(key / 文件 / 写入方法 / MQ destination) */ private Ignore ignore = new Ignore(); + /** 检测模式与推断参数 */ private Detection detection = new Detection(); + /** 按 ChangeType 名覆盖默认严重级别,如 FIELD_ADDED → P0 */ private Map severityOverrides = new LinkedHashMap<>(); + /** 人工写入点映射(自动推断不准时补齐) */ private List manualMappings = new ArrayList<>(); + /** 已知误报抑制规则 */ private List suppressions = new ArrayList<>(); + /** 仅检测这些模块(路径前缀/模块名);空表示不限制 */ private List includeModules = new ArrayList<>(); + /** 排除这些模块,不参与检测 */ private List excludeModules = new ArrayList<>(); public boolean isBlockMode() { @@ -46,18 +56,24 @@ public class CheckerConfig { /** 企微通知相关配置。 */ @Data public static class Notify { + /** 是否发送企微通知 */ private boolean enabled = true; /** 企微机器人 Webhook 完整 URL */ private String webhookUrl = ""; + /** 无变更时是否仍发「清洁」通知 */ private boolean notifyOnClean = false; + /** 报告标题前缀 */ private String titlePrefix = "[序列化结构变更]"; } /** 忽略规则:key / 文件路径 / 写入方法 / MQ destination。 */ @Data public static class Ignore { + /** 忽略的 Redis key / 模式 */ private List keyPatterns = new ArrayList<>(); + /** 忽略的源文件 glob */ private List filePatterns = new ArrayList<>(); + /** 忽略的写入方法(Class#method 或简单名) */ private List writerMethods = new ArrayList<>(); /** 忽略的 MQ destination 模式(topic / topic:tag) */ private List mqDestinations = new ArrayList<>(); @@ -66,10 +82,13 @@ public class CheckerConfig { /** 检测模式与推断参数(Redis W*、MQ MQ*、读侧补强开关等)。 */ @Data public static class Detection { + /** Redis 写入检测模式列表,如 W01~W06 */ private List patterns = new ArrayList<>(); /** MQ 投递检测模式:MQ01~MQ05、MQ-K01~MQ-K04 */ private List mqPatterns = new ArrayList<>(); + /** 低于此置信度的结构变更按低置信度降级处理 */ private double minConfidence = 0.6; + /** 字段展开最大深度,防止循环引用爆栈 */ private int maxFieldDepth = 8; /** W06:是否启用读侧反序列化类型辅助补强 */ private boolean readHintsEnabled = true; @@ -82,10 +101,15 @@ public class CheckerConfig { */ @Data public static class ManualMapping { + /** 规则 id(便于配置追溯) */ private String id; + /** 目标写入方法标识 */ private String writerMethod; + /** 指定的 key / destination 模式 */ private String keyPattern; + /** 指定的 value 类型 */ private String valueType; + /** 备注说明 */ private String description; } @@ -94,10 +118,15 @@ public class CheckerConfig { */ @Data public static class Suppression { + /** 规则 id */ private String id; + /** 可选:限定写入方法 */ private String writerMethod; + /** 可选:匹配的 key / destination 模式 */ private String keyPattern; + /** 要抑制的 ChangeType 名列表;空表示不按类型过滤 */ private List changeTypes = new ArrayList<>(); + /** 抑制原因(文档用) */ private String reason; } } diff --git a/src/main/java/com/codechecker/cache/detector/CacheReadHint.java b/src/main/java/com/codechecker/cache/detector/CacheReadHint.java index 6d433bf..2625af8 100644 --- a/src/main/java/com/codechecker/cache/detector/CacheReadHint.java +++ b/src/main/java/com/codechecker/cache/detector/CacheReadHint.java @@ -9,15 +9,22 @@ import lombok.Data; @Data public class CacheReadHint { + /** 源文件相对路径 */ private String filePath; + /** 反序列化调用所在行号 */ private int lineNumber; + /** 所在类 FQN */ private String enclosingClass; + /** 所在方法名 */ private String enclosingMethod; /** 推断出的 key 模式;无法关联 redis get 时为 null */ private String resolvedKeyPattern; + /** 源码中的 key 表达式原文 */ private String keyExpression; /** value 元素/对象 FQN */ private String resolvedValueType; + /** 是否按数组根(parseArray / getJsonToList 等)理解 value */ private boolean rootArray; + /** 提示置信度:仅有类型约 0.7,关联到 redis get 约 0.85 */ private double confidence = 0.7; } diff --git a/src/main/java/com/codechecker/cache/detector/WritePoint.java b/src/main/java/com/codechecker/cache/detector/WritePoint.java index 1c5e0d3..19d2209 100644 --- a/src/main/java/com/codechecker/cache/detector/WritePoint.java +++ b/src/main/java/com/codechecker/cache/detector/WritePoint.java @@ -10,23 +10,37 @@ import lombok.Setter; @Data public class WritePoint { + /** Redis 通道标识 */ public static final String CHANNEL_REDIS = "REDIS"; + /** RocketMQ 通道标识 */ public static final String CHANNEL_ROCKETMQ = "ROCKETMQ"; + /** Kafka 通道标识 */ public static final String CHANNEL_KAFKA = "KAFKA"; + /** 源文件相对路径 */ private String filePath; + /** 写入/投递调用所在行号 */ private int lineNumber; + /** 所在类 FQN */ private String enclosingClass; + /** 所在方法名 */ private String enclosingMethod; + /** 命中的检测模式编号,如 W01、MQ01、MQ-K01 */ private String pattern; + /** 源码中的 key / destination 表达式 */ private String keyExpression; + /** 解析后的 key / Topic 模式(如 demo:foo:*) */ private String resolvedKeyPattern; + /** 源码中的 value 表达式 */ private String valueExpression; + /** 推断的 value 类型 FQN */ private String resolvedValueType; + /** value 是否为数组根(List / 数组序列化) */ private boolean rootArray; + /** 类型/key 推断置信度,默认 1.0 */ private double confidence = 1.0; /** REDIS / ROCKETMQ / KAFKA;默认 REDIS 兼容现网 */ diff --git a/src/main/java/com/codechecker/cache/diff/SchemaChange.java b/src/main/java/com/codechecker/cache/diff/SchemaChange.java index 07d22fb..5684ba4 100644 --- a/src/main/java/com/codechecker/cache/diff/SchemaChange.java +++ b/src/main/java/com/codechecker/cache/diff/SchemaChange.java @@ -9,15 +9,25 @@ import lombok.Data; @Data public class SchemaChange { + /** 严重级别(可由 severity_overrides 覆盖默认值) */ private Severity severity; + /** 变更类型(字段增删、包装层、写入点增删等) */ private ChangeType changeType; + /** 解析后的 key / Topic 模式 */ private String keyPattern; + /** 源码中的 key / destination 表达式 */ private String keyExpression; + /** 写入位置,形如 Class#method:line */ private String writeLocation; + /** 展示用 value 类型名 */ private String valueType; + /** 字段路径(点分,数组用 []),写入点级变更可为空 */ private String fieldPath; + /** 变更前取值说明(类型名、路径等,依 changeType 而定) */ private String oldValue; + /** 变更后取值说明 */ private String newValue; + /** 人可读变更说明,用于报告明细 */ private String message; public SchemaChange(ChangeType changeType) { diff --git a/src/main/java/com/codechecker/cache/report/CheckReport.java b/src/main/java/com/codechecker/cache/report/CheckReport.java index 6a1020c..072cc3d 100644 --- a/src/main/java/com/codechecker/cache/report/CheckReport.java +++ b/src/main/java/com/codechecker/cache/report/CheckReport.java @@ -14,17 +14,28 @@ import java.util.List; @Data public class CheckReport { + /** 仓库名(报告抬头展示) */ private String repository; + /** 分支名 */ private String branch; + /** 对比基准提交 SHA */ private String oldSha; + /** 当前提交 SHA */ private String newSha; + /** 提交人 */ private String modifier; + /** 提交时间(展示用字符串) */ private String modifyTime; + /** 运行模式:notify / block */ private String mode; + /** 字段级变更明细(去重、抑制后) */ private final List changes = new ArrayList<>(); + /** 按 Key/Topic 聚合的结构变更摘要 */ private final List keyChanges = new ArrayList<>(); + /** block 模式下是否应阻断流水线 */ private boolean blocked; + /** 建议进程退出码:0 通过,1 阻断,2 执行错误 */ private int exitCode; /** @return 是否存在字段级或 Key 级结构变更 */ diff --git a/src/main/java/com/codechecker/cache/report/KeyStructureChange.java b/src/main/java/com/codechecker/cache/report/KeyStructureChange.java index f9c5c66..4f68edb 100644 --- a/src/main/java/com/codechecker/cache/report/KeyStructureChange.java +++ b/src/main/java/com/codechecker/cache/report/KeyStructureChange.java @@ -23,13 +23,18 @@ public class KeyStructureChange { private String writeLocation; /** 展示用 value 类型,如 List<ClockInExportVo> */ private String valueType; + /** key 是否未能静态解析(报告中标灰提示) */ private boolean keyUnresolved; /** REDIS / ROCKETMQ / KAFKA */ @Setter(AccessLevel.NONE) private String channel = "REDIS"; + /** 变更前序列化骨架 JSON(已删除写入/投递时仅有此项) */ private String oldSkeletonJson; + /** 变更后序列化骨架 JSON(新增写入/投递时仅有此项) */ private String newSkeletonJson; + /** 本聚合块最高严重级别 */ private Severity severity = Severity.P2; + /** 归属本 Key/Topic 的字段级变更明细 */ private final List fieldDetails = new ArrayList<>(); public void setChannel(String channel) { diff --git a/src/main/java/com/codechecker/cache/schema/FieldSchema.java b/src/main/java/com/codechecker/cache/schema/FieldSchema.java index 6fb2cf2..4774f67 100644 --- a/src/main/java/com/codechecker/cache/schema/FieldSchema.java +++ b/src/main/java/com/codechecker/cache/schema/FieldSchema.java @@ -9,8 +9,11 @@ import lombok.Value; @Value public class FieldSchema { + /** 字段路径,如 {@code vo.linkList[].id} */ String path; + /** 映射到的 JSON 类型(OBJECT / ARRAY / STRING 等) */ JsonType jsonType; + /** 原始 Java 类型简单名或 FQN 片段 */ String javaType; @Override diff --git a/src/main/java/com/codechecker/cache/schema/SourceIndex.java b/src/main/java/com/codechecker/cache/schema/SourceIndex.java index 6fadbdb..4e86f3b 100644 --- a/src/main/java/com/codechecker/cache/schema/SourceIndex.java +++ b/src/main/java/com/codechecker/cache/schema/SourceIndex.java @@ -178,9 +178,13 @@ public class SourceIndex { @Getter @AllArgsConstructor(access = AccessLevel.PACKAGE) public static final class IndexedType { + /** 类型 AST 声明 */ private final ClassOrInterfaceDeclaration declaration; + /** 所在包名(无包则为空串) */ private final String packageName; + /** 文件 import 列表(含通配 import.*) */ private final List imports; + /** 类型 FQN(含内部类,以 . 分隔) */ private final String fqn; } } diff --git a/src/main/java/com/codechecker/cache/schema/TypeSchema.java b/src/main/java/com/codechecker/cache/schema/TypeSchema.java index c513aa6..1cbfd2e 100644 --- a/src/main/java/com/codechecker/cache/schema/TypeSchema.java +++ b/src/main/java/com/codechecker/cache/schema/TypeSchema.java @@ -12,9 +12,12 @@ import java.util.Map; @Getter public class TypeSchema { + /** 根 value 类型 FQN(或展示名) */ private final String rootType; + /** Schema 展开置信度;类型解析不完整时会下调 */ @Setter private double confidence = 1.0; + /** path → 字段节点(插入顺序保留) */ private final Map fields = new LinkedHashMap<>(); public TypeSchema(String rootType) {