Archunit Architecture Guard

Other

扫描既有 Java/Kotlin 工程,识别其架构模式(分层/MVC、DDD、六边形/端口适配器、整洁架构、按功能分包/模块化单体、或 Android MVVM),报告架构坏味道并给出具体修复指导,再生成 ArchUnit 守护测试把"应有的架构"固化下来,使后续(通常是 AI 生成的)代码无法让它腐烂。当用户想要"守护/约束/保护/测试"某个 Java/Kotlin 工程的架构、提到 ArchUnit、架构适应度函数(architecture fitness function)、依赖规则、越层、包循环、架构腐烂(architecture erosion/rot),或想阻止 AI 生成的代码破坏既有设计时,使用本 Skill。当用户问"这个工程用的什么架构""我的架构干净吗",或想给一个被 AI 反复修改的代码库加护栏时,也应触发。

Install

openclaw skills install archunit-architecture-guard

ArchUnit 架构守护(Architecture Guard)

何时使用本 Skill

说明:真正生效的触发条件写在文件顶部 frontmatter 的 description 字段里(那是 Claude 判断是否加载本 Skill 的唯一依据)。本小节是给维护者看的人类可读版,二者应保持一致;改了触发条件请同步更新 description

适用:

  • 用户想给某个 Java/Kotlin 工程的架构加"护栏"——守护、约束、保护、测试架构。
  • 用户提到 ArchUnit、架构适应度函数(architecture fitness function)、依赖规则、越层、包循环、架构腐烂(architecture erosion/rot)。
  • 用户想阻止 AI 生成的代码破坏既有架构设计,或想给一个被 AI 反复修改的代码库加约束。
  • 用户问"这个工程用的什么架构""我的架构干净吗""帮我看看依赖有没有乱"。
  • 用户想生成 / 编写架构测试、固化分层或模块边界。

不适用:

  • 纯粹的功能性单元测试 / 集成测试(本 Skill 只管架构约束,不管业务逻辑正确性)。
  • 运行时性能、安全漏洞扫描这类与"结构依赖"无关的问题。
  • 非 JVM 工程(ArchUnit 只分析 Java 字节码;它能覆盖 Kotlin、Scala 等编译到 JVM 的语言,但不适用于 Go、Python、前端 JS/TS 等)。

目的与核心思想

AI 生成的代码会漂移。每一次单独的改动看起来都合理,但累积许多次之后,controller 开始直接调 repository,领域模型导入了 Spring,包之间冒出了循环。代码库就这样一次一个"看似合理的 diff"地腐烂下去。

本 Skill 把工程应有的架构翻译成可执行测试,使用 ArchUnit,让新代码一旦破坏规则,构建立即失败。工作流分四个阶段:

  1. 扫描 真实的包结构与依赖结构。
  2. 识别 架构模式(基于证据)。
  3. 诊断 坏味道并给出修复指导。
  4. 生成 ArchUnit 守护测试(默认:冻结存量违规,只拦新增)。

锚定原则——守护的是"回归",不是"历史"。 用户几乎从不希望构建在第一天就因为历史债务而失败。ArchUnit 的 freeze(...) 会把当前违规记成基线,只对新增违规报错。这正是"阻止 AI 把架构改得更差"这个场景的命门。默认采用冻结;把严格模式作为可选项提供。

两种规则推导方式——必须明确说明你在用哪种:

  • 描述性(descriptive) 规则:固化现状的架构(适合冻结 + 锁定团队已经在遵循的约定)。
  • 规定性(prescriptive) 规则:固化应有的架构(所识别模式的标准形态)。对依赖方向、隔离这类规则用规定性,然后 freeze 它们,使存量例外不阻塞构建、但不允许任何新增。

务必告诉用户哪些规则是描述性、哪些是规定性,让他们清楚自己在约束什么。

输出语言

所有面向用户的分析内容(架构报告、坏味道发现、修复指导)使用用户在对话中所用的语言。Java 标识符、包名、代码注释保持英文。生成的测试代码本身与语言无关。


阶段一 —— 扫描结构

目标:在做任何判断之前先建立一份客观的工程地图。不要凭单个类去猜,要收集证据。

  1. 定位源码根目录与构建文件。 找到 pom.xml / build.gradle(.kts) / settings.gradle,以及 src/main/java(和 src/main/kotlin)。注意是否为多模块结构。
  2. 探测构建工具与测试框架(这决定生成的测试形态):
    • 从构建文件判断 Maven 还是 Gradle。
    • 从既有测试依赖判断 JUnit 5(org.junit.jupiter)还是 JUnit 4(junit:junit)。
    • 是否存在 Spring Boot / Spring、JPA/Hibernate、Android(com.android.*AndroidManifest.xml)。
  3. 盘点包结构。 列出每个源码根下的包树。识别根包/基础包(公共前缀)。记录其下的顶层子包。
  4. 构建包依赖图。 对每个包,统计它导入了哪些内部包。这是最重要的产物——它揭示依赖方向、循环和分层。用扫描脚本可靠地生成,而不是肉眼看:运行 python references/scan_packages.py <源码根> --base <基础包>(输出格式见该脚本头部说明)。若脚本无法运行,再退而用 grep 抓 import 语句推导同样的依赖图,但优先用脚本。
  5. 探测角色信号:按包名和注解识别角色。统计包名中出现的角色关键字频率:controllerwebapiresourceendpointserviceusecaseapplicationdomainmodelentityaggregatevovalueobjectrepositorydaopersistencemapperinfrastructureinfraadapterportconfigdtoclientgatewayviewmodelview。同时统计注解:@RestController/@Controller@Service@Repository@Component@Entity@Configuration

把这些都记成客观事实,由阶段二来解读。


阶段二 —— 识别架构模式

把证据匹配到模式上。输出一个带排名的识别结果,附置信度和具体证据——绝不只给一个光秃秃的标签。报出混合不一致的结论很常见也完全合理(例如"分层为主,其中两个模块正向按功能分包漂移")。

完整的信号表和判定指南见 references/pattern-detection.md。主要模式概要:

  • 分层 / MVC —— 按技术角色分包(controllerservicerepository),有 Spring 构造型注解,自上而下。Java 后端最常见的形态。
  • DDD(战术)+ 分层 —— domain 包含 aggregate/entity/valueobject/domainservice,外加 applicationinfrastructure。领域层意在保持无框架依赖。
  • 六边形 / 端口与适配器 —— domain/application 核心,外加 port(接口)和 adapter(实现:web、persistence、messaging)。适配器向内依赖。
  • 整洁架构 —— 同心圆:entities ⊂ usecases ⊂ interface-adapters ⊂ frameworks;依赖只能向内。
  • 按功能分包 / 模块化单体 —— 顶层包是业务能力(ordersbillingcatalog),每个功能内部有自己的分层;功能之间不应依赖彼此的内部实现。
  • MVVM(Android/客户端) —— viewviewmodelmodel/repository,配合 LiveData/StateFlow/DataBinding。View ↔ ViewModel ↔ Model;View 不得直接碰 Model,ViewModel 不得引用 Android 的 View/Activity 等 UI 类型。提醒用户 MVVM 主要是客户端(Android)模式——若在服务端看到它,要再三确认。

证据混杂时直说,并选取主导模式作为守护规则的基础,把偏离项列为阶段三的候选坏味道。


阶段三 —— 诊断坏味道并给出修复指导

每条发现都报告:是什么(坏味道)、在哪(包/类)、为何要紧(具体风险)、怎么修(具体重构)。按严重度排序。完整目录和修复模式见 references/smell-catalog.md。高价值检查项:

  • 包循环 —— 几乎总值得标记;它阻断独立推理与测试。
  • 越层 / 依赖方向违规 —— controller → repository 跳过 service;domain → infrastructure;内层依赖外层。
  • 框架泄漏进领域层 —— domain 内部出现 Spring/JPA 注解或导入(破坏 DDD/六边形的隔离,把业务规则耦合到框架)。
  • 持久化实体泄漏到 Web 层 —— JPA @Entity 类型被直接当成 API 的请求/响应体,而非用 DTO。
  • 放置/命名不一致 —— 一部分 controller 在 web、另一部分在 controller;这正是 AI 改动造成的漂移,也是引入守护测试最有力的理由。
  • 上帝包 —— 一个所有东西都依赖的包(超出合理的共享 common 范畴)。

要诚实对待置信度:某个"违规"可能是有意的例外。对边界情况,表述为"先确认意图,再决定修复还是冻结"。


阶段四 —— 生成 ArchUnit 守护测试

这是最终交付物。可直接复制的规则配方、依赖坐标、JUnit 4/5 模板、冻结/存储配置见 references/archunit-cookbook.md

流程:

  1. 配置依赖:按探测到的构建工具与 JUnit 版本(手册里 Maven/Gradle 都有)。提醒用户加上,不要假设已存在。
  2. 为主导模式选规则集
    • 分层/MVC → 用探测到的层调用 layeredArchitecture()
    • DDD/六边形/整洁 → onionArchitecture()(ArchUnit 内置),或用显式的 noClasses().that().resideInAPackage("..domain..").should().dependOnClassesThat()... 规则约束"只能向内"的依赖方向。
    • 按功能分包 → 用 slices().matching(...) 禁止跨功能访问内部实现。
    • MVVM → view/viewmodel/model 之间的方向规则,外加"viewmodel 不准出现 Android UI 类型"。
  3. 无论什么模式都加上这些
    • 无包循环:slices().matching("<base>.(*)..").should().beFreeOfCycles()
    • 与现状匹配的命名约定(例如 ..controller.. 里的类以 Controller 结尾)——这能极低成本地阻止放错位置。
    • 若团队明显有此约定,再禁用特定通用 API(如 java.util.logging、字段注入)。
  4. 默认冻结。 把依赖方向、循环、隔离这类规则包进 freeze(...),使存量违规变成基线、只有新增才失败。生成 archunit.properties 并解释存储文件(要提交进版本库)。同时提供清晰标注的"严格模式"变体(去掉 freeze(...)),供想对所有违规都报错的团队使用。冻结细节见手册的冻结小节。
  5. 放置测试src/test/java/<base>/architecture/ArchitectureTest.java(或按模块各放一个)。用 @AnalyzeClasses(packages = "<base>", importOptions = ImportOption.DoNotIncludeTests.class)
  6. 让每条规则可追溯。 在每个 @ArchTest 规则上方加注释,写明它守护的是哪条坏味道/模式约束,以及它是描述性还是规定性。这样将来某次构建失败时(人或 AI)能看懂为什么失败。
  7. 告诉用户怎么跑mvn test / gradle test)以及首次运行的预期(冻结会记录基线;要提交存储文件)。

当用户的工程在磁盘上时,把测试作为真实文件写出来;否则以内联代码/工件形式给出,让用户直接放进工程。


汇总 —— 报告结构

按以下顺序交付,让用户先看到推理、再看到代码:

  1. 识别到的架构 —— 带排名的模式、置信度、证据(一小段 + 一张角色关键字计数小表即可)。
  2. 架构地图 —— 分层/功能依赖图以及任何循环,紧凑描述。
  3. 坏味道与修复 —— 按严重度排序,每条含 是什么/在哪/为何/怎么修。
  4. 守护测试 —— 依赖配置、ArchitectureTest 类、archunit.properties、冻结 vs 严格的说明、运行方式。
  5. 它拦住了什么 —— 一两句话把规则映射回"AI 腐烂"这个关切,让用户明白护栏确实在起作用。

行文要紧凑。代码和发现才是价值所在,不要围着它们堆评论。

常见陷阱(要避免)

  • 不要为了"整齐"而硬编一个模式。"不一致 / 无明确模式"是一个有效且有用的结论,反而更能论证守护测试的必要性。
  • 除非用户明确要严格模式,否则不要生成会让构建在历史代码上立刻失败的规则——那样测试第二天就会被删掉。要冻结。
  • 不要硬编 com.example。处处使用真实探测到的基础包。
  • 若有网络,到 Maven Central 核对 ArchUnit 版本;手册里的版本号可能已过时。提醒用户确认最新版。
  • 多模块工程要按模块限定 @AnalyzeClasses 范围,或用该模块的基础包;在聚合 pom 上跑单个测试往往会导入错误的类路径。