Skip to the content.

契约匹配详解

跨程序分析的核心问题:本程序改的这个东西,和对方程序用的是不是同一个东西? 六类契约各有自己的对齐规则,下面逐一说清楚。

HTTP 接口

按规范化路径模板匹配。

规范化做了什么

/api/users/{id}          →  /api/users/{}      (变量名丢掉,只留占位)
/api/users/{userId}      →  /api/users/{}      (同上,两个程序变量名不同也能对上)
http://host/api/users/1  →  /api/users/1       (去掉协议与主机)
/api/users/?page=2       →  /api/users/        (去掉查询串)
/api//users/             →  /api/users         (折叠重复斜杠、去尾斜杠)

客户端写法差异的处理

客户端很少照抄模板,常见三种写法都要能对上:

客户端写法 扫描到的路径 匹配方式
getForObject("/api/users/42") /api/users/42 模板变量填值
getForObject("/api/users/" + id) /api/users 前缀匹配(拼接截断)
Feign 接口 @GetMapping("/api/users/{id}") /api/users/{} 直接相等

第三种情况(拼接)最容易漏。"/api/users/" + id 只能静态扫出字面量部分 /api/users, 所以匹配时还要判断”这个前缀是否正好是某个模板的前缀且剩余部分只有一个变量”。

支持哪些消费方写法

数据库字段

契约的单位是 表.字段(table.column),不是表。

但「表.字段同名」仍然只是候选。一条数据库变更要跨程序成立,还要通过三道准入:同一数据库对象、变更可被观察、类型兼容。完整规则与已知边界见 数据库影响判断。

先确认是同一数据库对象

系统从 spring.datasource.url / spring.datasource.*.url / spring.datasource.hikari.jdbc-url 解析数据库名,从 SQL 的 schema.table 与 DDL 的 schema.table 读取显式 schema。

本程序侧:哪些改动才算数据库契约

改动 例子 契约单位 扩散
显式删列 ALTER TABLE orders DROP COLUMN remark orders.remark 读方、写方、映射方
快照推断字段消失(无 DROP COLUMN) 改 CREATE TABLE 的列清单、删掉建表脚本 —— 不扩散,只计数
表重命名 ALTER TABLE orders RENAME TO orders_archive orders(表级,旧表名) 读方、写方、映射方
列重命名 ALTER TABLE orders RENAME COLUMN remark TO note orders.remark(旧列名) 读方、写方、映射方
收窄 / 不兼容改类型 VARCHAR(64) → VARCHAR(32)、INT → VARCHAR orders.remark 读方、写方、映射方
放宽 / 兼容改类型 VARCHAR(32) → VARCHAR(64)、INT → BIGINT —— 不扩散,只计数
加列 ALTER TABLE orders ADD COLUMN remark —— 不扩散;SELECT * / 无列名 INSERT 记盲区
加必填列(NOT NULL 无默认值) ADD COLUMN tenant VARCHAR(32) NOT NULL orders.tenant 命中省略该列的完整 INSERT(usage=writer);自动填充列除外
一条 ALTER 多个动作 ADD a INT, DROP b, MODIFY c BIGINT NOT NULL 每个动作各自的列 / 表 逐动作判定,每个动作一条 DDL_CHANGED
建表 CREATE TABLE ... —— 不扩散(新表不可能是既有依赖)
删表 DROP TABLE orders orders(表级) 读方、写方、映射方
写入字段集合变化 INSERT INTO orders(...) 的列清单、UPDATE ... SET 的列 新增的列 只命中读方;停止写入某列不命中(列仍存在)
无列名的整表写入 INSERT INTO t VALUES (...) t(表级) 只命中读方;动态 SQL 不退回表级
实体字段新增 JPA @Column 字段新增 —— 不扩散
实体字段删除 / Java 类型变化 JPA 字段删除、Long → long 对应列 只命中映射方(低置信度)
删除迁移脚本 删掉 V1__init.sql —— 不扩散(已执行的 migration 不回滚)

只改本程序读多少列,仍然不构成数据库契约变化。

列删除只认显式迁移动作。 只有迁移脚本里的 ALTER TABLE ... DROP COLUMN(或 RENAME COLUMN 的旧列名) 才算字段删除;「本程序停止写这一列」和「快照里 CREATE TABLE 不再列这一列」都不会让已部署的列消失, 因此不向对方读写方传播,只在「对对方不可观察的变更」中计数并给出示例。

命中条件:对方在哪算用了这个字段

字段级命中看对方程序里有没有这个字段的使用点:

对方的使用点 关系 usage
对方的 SQL 语句读这个字段 READS_COLUMN reader
对方的 SQL 语句写这个字段 WRITES_COLUMN writer
对方的实体字段映射这个字段 MAPS_COLUMN mapping

命中条目记 DB_COLUMN,契约写 table.column,置信度 0.85;数据库身份有一侧未知时降到约 0.72。

本程序改的是哪一类,决定命中对方谁:

字段盲区:没读到 ≠ 没影响

字段级匹配要求能从 SQL 里解析出具体列名。两种情况解析不出来:

这类语句不再命中字段级契约,但会被计数,并在报告的「分析边界与盲区」里标注为字段盲区:

字段盲区  peer-service · ReportMapper.xml:31
          SELECT * FROM orders — 列清单无法静态确定,未参与字段级匹配

它同时说明两件事:这条 SQL 确实碰了本程序改的表,但工具判断不了它碰没碰那个字段。把盲区读成「没影响」是最容易犯的错误,所以它出现在盲区清单里,而不是被静默丢掉。

盲区口径是「读写列都没解析出来」或「语句被标记为动态 SQL」。一条已经作为 writer 命中的纯写语句不会重复计入盲区;必填列规则命中的 INSERT 同样不重复计入。SELECT * 与动态 SQL 不命中必填列,仍计入盲区。

一张对照表

本程序的改动 是否扩散 命中对方谁
多查 / 少查一个字段 否 ——
只改 WHERE / JOIN / 排序 否 ——
写入字段集合新增或消失 是 读该字段的代码(reader)
表重命名 是(表级,旧表名) 读、写、映射旧表的代码
列重命名 是(旧列名) 读、写、映射旧列名的代码
加必填列(NOT NULL 无默认值) 是 省略该列的完整 INSERT(writer)
实体字段映射新增 否 ——
实体字段映射删除 / Java 类型变化 是(低置信度) 映射方
DDL 加字段 否(计数) 盲区:SELECT * / 无列名 INSERT
DDL 删字段 / 收窄 / 不兼容改类型 是 读方、写方、映射方
DDL 可空性收窄(NULL → NOT NULL) 是 写方(读方不受影响)
DDL 默认值变化 否(计数) 盲区:无列名 INSERT
DDL 放宽 / 兼容改类型 否(计数) ——
CREATE TABLE 否 ——
DROP TABLE 是(表级) 读写该表的代码
删除迁移脚本 否 ——

配置项

按 key 匹配,并且向上追溯父级前缀:

本程序改动 config:app.limit
  ↓ 候选
config:app.limit    ← 精确匹配
config:app          ← 父级前缀
  ↓
对方绑定 @ConfigurationProperties(prefix = "app") 的类   ← 命中

反过来也成立:改 app 前缀下的任意 key,会影响绑定该前缀的类。

@Value 注解在字段上和方法参数上都会被扫描。字段注入是最常见的写法, 只扫方法会漏掉大量消费方。

Maven 依赖

如果对方 pom.xml 声明了对本程序某个模块 artifactId 的依赖,就是编译期契约。

本程序模块 artifactId: common
  ↓
对方 pom.xml 里有 <dependency><artifactId>common</artifactId>
  ↓
编译期契约命中

这类命中只报告”存在依赖关系”,不产生具体影响条目——因为依赖本身不是某个符号的使用点, 需要靠其他契约(类型引用、方法调用)才能定位到代码。

消息主题

按规范化标识匹配,覆盖三种中间件:

中间件 契约标识
Kafka Topic 名
RabbitMQ Exchange + Queue + Routing Key
RocketMQ Topic + Tag

消息 Schema

同一中间件下,消息体的结构指纹一致即视为同一契约:

Schema 变化特别隐蔽:字段类型从 int 改 long,生产者能跑,消费者也能跑, 但反序列化可能丢精度。这类变更需要靠契约匹配才能发现。

匹配失败怎么办

匹配不到不代表没影响,只代表静态分析找不到连接点。报告里会区分两种情形:

情形 报告表现
契约没被改动 不出现(共享接口没变,本来就不该报)
契约改了但对方没使用点 显示”没有发现相关契约使用点”
对方程序读取失败 该 peer 条目标注原因,其他 peer 不受影响

下一步:知道怎么匹配了,看 评分模型 了解匹配上之后如何算风险。