契约漂移审计方法论:如何系统化核查 Caffeine 文档承诺与实现行为的偏差
【免费下载链接】caffeineA high performance caching library for Java项目地址: https://gitcode.com/gh_mirrors/ca/caffeine
导读
本文围绕 Caffeine 仓库中面向 AI 审计 Agent 的契约漂移审计技能文档(audit-contract-drift/SKILL.md)展开,完整还原其“先读文档、再追踪实现”的逆向审计方法论:如何从公开 API 的 Javadoc 中枚举行为承诺,如何把每一条承诺追踪到所有应当履行它的代码路径,以及如何用“契约漂移”这一统一口径收敛 javadoc 承诺与实现行为之间的静默矛盾。读完本文,你将掌握一套可直接复用的 API 契约审计清单、常见漂移模式库,以及针对 Caffeine 核心接口与视图(asMap、集合视图、批量操作、synchronous()往返、序列化往返)的逐点核查框架。
说明:本文中“技能文档”指 .claude/skills/audit-contract-drift/SKILL.md,配套的审计执行框架见 .claude/agents/auditor.md;文中所引 Javadoc 与实现均来自本仓库当前源码,未引入仓库之外的外部断言。
一、什么是契约漂移(Contract Drift)
1.1 技能文档给出的核心定义
技能文档开门见山地界定了这一类审计的独特定位:
“大多数审计读代码;这个审计先读文档,然后把每一条承诺追踪到所有应当履行它的实现路径。这里要抓的 bug 不是并发缺陷或算术缺陷——而是 javadoc 承诺与代码实际行为之间的静默矛盾。”
契约漂移审计的输入不是“某个方法写得对不对”,而是“文档承诺了什么、所有相关路径是否一致地兑现了承诺”。技能文档据此定义了漂移的判定标准:
“一条路径如果使用了与契约承诺不同的等价性、顺序或可见性,就是一个契约漂移发现。同样,一条在往返(round-trip)过程中静默降级配置的路径也是。”
配套的执行框架 .claude/agents/auditor.md 对此给出了佐证:Caffeine 核心实现的锁层级、节点生命周期等“机械事实”记录在 .claude/rules/concurrency.md 中,而哪些看似可疑的行为属于有意设计(如 weight=0 条目是用户可见的 pinning 特性、EXPIRE_TOLERANCE是有意为之的过期语义、瞬态负weightedSize是可接受的最终一致)则在 .claude/docs/design-decisions.md 中说明——契约审计的职责是识别文档与实现的偏差,而不是把有意设计误报为缺陷。
1.2 与其它审计的边界
Caffeine 仓库的.claude/skills/目录下还有大量姊妹技能,契约漂移审计与它们的区别在于:
- audit-linearizability:关注并发下的线性化顺序,属于正确性证明范畴;
- audit-adversarial:无设计上下文的敌意全面审查;
- audit-contract-drift:关注“文档说了什么 vs 代码做了什么”的静默矛盾,是唯一一个以文档为第一阅读对象的审计。
根据 .claude/CLAUDE.md 的审计选择表,文档化行为与实现漂移的疑虑应运行/audit-contract-drift,其使用场景是“Documented behavior vs. implementation drift”。
二、契约清单:从哪些文档枚举行为承诺
2.1 第一来源:Caffeine.java 构建器 Javadoc
技能文档要求以 Caffeine.java 中每个构建器方法 javadoc 里的<b>Note:</b>与<b>Warning:</b>块为最高优先级契约来源。以下是在当前源码中实际存在、可直接作为核查锚点的示例:
等价性语义翻转(强相关:技能文档点名的第一类)
- weakKeys() 的
<b>Warning:</b>块承诺:使用该方法后,缓存将以身份(==)比较判定键的相等性;其asMap视图因此会在技术上违反 Map 规范(与IdentityHashMap同理)。同时承诺:键已被 GC 回收的条目可能仍被estimatedSize()计数,但“绝不会对读写操作可见”。 - weakValues() 的
<b>Note:</b>块承诺:使用该方法后,缓存以身份比较判定值的相等性;并给出“弱值不擅长做缓存,优先考虑 softValues”的工程建议。 - 类级 Javadoc(Caffeine.java 顶部)统一承诺:“默认使用 equals 比较;若指定了 weakKeys,键改为身份比较;若指定了 weakValues 或 softValues,值改为身份比较”。
过期与刷新
expireAfter*、refreshAfterWrite相关方法承诺时长约束与“过期”在边界上的定义。refreshAfterWrite的<b>Note:</b>明确承诺“刷新期间抛出的所有异常将被记录日志然后吞掉”。- 类级 Javadoc 承诺:过期条目可能被
estimatedSize()计数,但“绝不会对读写操作可见”;可通过scheduler(Scheduler)提供过期条目的及时移除。
容量与逐出
maximumSize、maximumWeight承诺逐出触发时机与摊销行为(类级 Javadoc 承诺“若指定了最大容量,条目可能在每次缓存修改时被逐出”)。
监听器
removalListener、evictionListener承诺通知的时机与方式,且均有“must not在回调中修改缓存”或“异常不会被传播”之类的<b>Warning:</b>。
2.2 第二来源:核心接口的全部方法 Javadoc
技能文档点名的接口均存在于caffeine/src/main/java/com/github/benmanes/caffeine/cache/下:
- Cache.java:每个方法都要核查,尤其关注 “must”、“will”、“guarantees”、“for any reason” 这类强承诺词汇。例如其类级 Javadoc 承诺“实现预期是线程安全的,可被多个并发线程安全访问”,
get方法承诺映射函数“在同一键上至多应用一次”、且映射函数必须不得在计算期间修改本缓存。 - LoadingCache.java、AsyncCache.java、AsyncLoadingCache.java:同上逐方法核查。
- Policy.java:每个方法的文档化行为。
- 用户侧契约接口:Weigher.java、Expiry.java、RemovalListener.java、RemovalCause.java。
其中 RemovalCause.java 本身就是一个浓缩的契约字典:EXPLICIT(用户显式移除)、REPLACED(值被替换,条目并未真正移除)、COLLECTED(键/值被 GC 回收)、EXPIRED(过期时间戳已过)、SIZE(因容量约束被逐出),并承诺wasEvicted()对后三者返回true。任何路径若在文档未承诺的时机触发或遗漏这些 cause,都构成漂移候选。
2.3 第三来源:内部义务(Internal Obligations)
技能文档明确要求把.claude/rules/*.md与各模块内部类 Javadoc 中“callers must”式的内部契约也纳入清单,例如:
- jcache 模块的
EventDispatcher要求每个发布事件的线程都必须通过awaitSynchronous/ignoreSynchronous排空同步监听器队列; - 异步操作必须向 in-flight 集合注册。
对这些内部义务,要逐一验证每一个调用点(包括不经过明显入口的执行器线程路径与 refresh 路径)是否履行。仓库规则文件中有对应佐证,例如 .claude/rules/async-cache.md 记录了异步缓存异常传播与 future 完成的内部义务,.claude/rules/concurrency.md 记录了写缓冲区任务不丢失、drain 状态机等内部约定。
2.4 每条契约要记录什么
技能文档要求为每条契约记录三要素:契约的精确措辞、激活该契约的配置组合、受影响的操作集合。这是后续逐路径追踪的索引,缺一不可。
三、追踪矩阵:把每条承诺映射到全部实现路径
技能文档给出了一个明确的“追踪矩阵”,每条契约都要沿以下路径逐一核对:
- 直接 API 调用:
Cache/LoadingCache/AsyncCache上的直接方法调用; asMap()视图方法:size、isEmpty、containsKey、containsValue、get、put、remove(k)、remove(k,v)、replace、compute*、merge、equals、hashCode;- 集合视图
asMap().keySet()/values()/entrySet():contains、remove、removeAll、retainAll、removeIf、iterator、spliterator; - 批量操作:
putAll、getAll、getAllPresent、invalidateAll; AsyncCache.synchronous()往返:同步视图是否与异步缓存履行相同的契约?- 序列化往返:反序列化后的缓存是否仍履行关于过期时长、loader 是否存在等承诺?
对每一条,都要问两个问题:这条路径使用的等价性、顺序或可见性与契约承诺一致吗?往返过程中配置是否被静默降级?只要答案是否定的,就是一个契约漂移发现。
配套的审计框架 .claude/agents/auditor.md 为这类追踪提供了两条实用准则:
- 生成代码溯源:
PS.java、WSSMS.java等节点类是代码生成的,字段与方法形态的实际定义在caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/的AddX.java生成器中。审计字段或方法时必须先溯源到生成器,不能只在BoundedLocalCache中下结论; - 配置矩阵:测试可能只覆盖矩阵中的一种配置(如强引用值)而未覆盖弱引用值,因此“现有测试通过”不是契约成立的证据,必须说明测试覆盖的具体场景与发现路径是否一致。
四、七种常见漂移模式(检查清单)
技能文档显式列出七种要优先核查的漂移模式,这是全文最具实战价值的部分:
4.1 弱/软缓存上的身份比较 vs equals 比较
weakKeys/weakValues/softValues承诺身份比较,但使用o.equals(value)或Collection.contains(value)的视图集合可能反而应用了用户的 equals 语义。核查点:asMap()及三个集合视图的所有contains/remove/iterator路径。
4.2 基数与存在性的不对称
被文档描述为“将 in-flight 异步值视为不存在”的方法,必须与同一视图上的size、isEmpty、equals、hashCode和迭代器保持一致。核查点:异步缓存中 in-flight 条目在查询、变更、基数方法三套口径下的可见性是否自洽。
4.3 “for any reason”通知承诺
removalListener被文档承诺“为任何原因触发”,但异步路径可能对 null 或异常完成的结果丢弃通知。核查点:AsyncCache完成路径上监听器触发的完备性。
4.4 跨版本序列化
跨版本间被重命名或默认值不同的序列化字段,可能在往返时静默丢失配置或直接抛异常。核查点:序列化代理对字段的读写是否与当前配置一一对应。
4.5 同实例返回语义
例如compute(k, (k,v) -> v)被文档描述为一种行为,但实现无论是否真的改变值都会更新时间戳/权重。核查点:compute 族方法在“返回同一实例”时的副作用是否符合文档。
4.6size()被文档化为估计值
技能文档特别提示这是显式豁免(explicit out):只要在 size 与逻辑存在性产生分歧的每个位置确实都文档化为“估计值”,就不构成漂移。核查点:estimatedSize相关文档是否在所有相关接口位置保持一致口径。
4.7 同步视图与异步视图的分歧
synchronous()视图的asMap()可能对 in-flight 条目在查询、变更、基数方法之间出现不一致处理。核查点:AsyncCache.synchronous()往返后的行为一致性。
五、发现的标准形态:锚定漂移的两端
技能文档对每条最终发现给出了严格的落盘格式——必须同时锚定契约端与实现端,并给出最小用户可观察场景:
- 契约来源:文件路径 + javadoc 片段(承诺了什么);
- 分歧实现:文件路径 + 方法(实际做了什么);
- 最小可观察场景:一个文档与代码确实产生分歧的用户可复现场景。
配套的 .claude/agents/auditor.md 输出契约进一步要求每条发现包含:位置(文件+方法)、一句话问题摘要、严重度(critical/high/medium/low)、证据、被违反的不变量/契约、置信度(high/medium)、定价(Priced,high/critical 必须附实测证据)、验证测试想法。例如其示例验证命令:
./gradlew :caffeine:test --tests 'BoundedLocalCacheTest.methodName' -Pcompute=async -Pvalues=weak这条命令同时印证了仓库测试体系的配置矩阵能力(.claude/CLAUDE.md 中记录了-Pkeys/-Pvalues/-Pcompute/-Pstats等过滤旗标,CI 在 40 个分片上跑完整矩阵),也说明契约审计中的每一条漂移都应落到某个具体配置组合上去验证。
六、证据边界与执行纪律
技能文档与配套审计框架对“什么能算证据”划了清晰的红线,写作与执行时同样适用:
- 以源码证据为准,不依据先前审计历史下结论;“先前的审计没有发现问题”不是证据;
- 设计决策文档(.claude/rules/design-decisions.md、.claude/docs/design-decisions.md)是机械事实(代码有意如此),可用于排除误报,但不能替代对代码的独立分析;
- high/critical 级别发现必须构建可运行 witness(JUnit 方法、jshell 片段或针对
caffeine/build/libs/caffeine-*.jar的 main)实测定价,且要在用户真实配置(Ticker.systemTicker()、公共线程池)下复现,仅在FakeTicker或直接执行器下复现的应视为仪器伪影并降低严重度; - 无法从源码静态确认的并发问题应升级到动态工具,测试选择依据见 .claude/docs/testing.md:Fray 用于同步点交错、LinCheck 用于普通字段竞争、jcstress 用于弱内存发布,对应测试源位于
caffeine/src/frayTest、caffeine/src/lincheckTest、caffeine/src/jcstress。
七、最小可执行的审计工作流
综合技能文档与配套框架,一次契约漂移审计的完整流程可浓缩为:
- 枚举:从 Caffeine.java 的 Note/Warning 块、四个核心接口、Policy.java 与四个用户侧契约接口、
.claude/rules/内部义务中,产出“契约 + 激活配置 + 受影响操作”三要素清单; - 追踪:按第三节的六类路径矩阵逐条映射,尤其不要遗漏
asMap视图、集合视图、批量操作、synchronous()往返、序列化往返这五类间接路径; - 对照:用第四节七种漂移模式做靶向扫描;
- 锚定:每条发现按第五节格式锚定契约端与实现端,给出最小可观察场景;
- 定价:对 high/critical 发现构建 witness 实测,标注运行配置与测得数值;
- 输出:报告写入审计输出目录(约定见 .claude/docs/audit-output.md),并按 finding-taxonomy 分类,置信度标注遵循 .claude/agents/auditor.md 的 high/medium 分级与“不得静默丢弃中等置信度怀疑”的纪律。
这套方法论的价值在于:它把“文档与实现不一致”这一模糊的直觉,变成了可枚举、可追踪、可锚定、可复现的工程流程——先建立契约清单,再沿路径矩阵逐点核对,最后用最小可观察场景与实测定价让每一条发现都可被审阅与裁决。
【免费下载链接】caffeineA high performance caching library for Java项目地址: https://gitcode.com/gh_mirrors/ca/caffeine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考