
我最近在整理一个迭代了三年的Java微服务仓库时发现真正卡住我的不是代码量而是“没有一张能跟着代码一起演进的架构图”。文档里的架构视图停在两个大版本之前新同事按图排查问题找过去的服务早就拆成了两个。后来我拿 Archify代码地图做了完整验证结论很直接它把“画架构图”这件事从手工维护变成了解析仓库后直接生成而且第二次生成速度真的可以用“秒”来形容。这篇文章我会把整个落地过程、配置文件、调参思路和踩过的问题都写出来给同样在维护复杂仓库的人一个参考。1. 为什么“架构图过期”比没有架构图更麻烦1.1 代码一直在变架构图却停在上一次重构很多团队不是不想画架构图而是画完就再也养不起。我见过不少仓库的“总架构图”是两年前人肉画的当时业务域划分和今天完全不是一回事。这种图放在 Wiki 里比没有图更危险它给了人一种“我已经了解了系统”的错觉结果照着图去做变更改错服务、找错依赖最后背锅的还是自己。问题本质在于代码和架构图的演进速度完全不一致。代码每次提交都在变化而架构图需要有人手动同步。除非把“更新架构图”加入 Definition of Done否则过了两个迭代图就开始失真。Archify 的思路和我之前用的绘图工具不一样——它不要求你去维护图而是要求你把源码仓库当作图的唯一数据源。你要做的只是让仓库结构和依赖关系保持清晰图自然会跟着变。1.2 直接生成和手工绘制之间的选择差价我过去用过三种方案直接用 IDE 生成 class diagram。优点是准确缺点是一放大几十个类糊成一团根本没法做架构评审。用 Draw.io / Excalidraw 手绘分层架构图。优点是美观可控缺点是要人肉维护一个服务拆分后所有依赖边都要重画。用 Archify 这类代码地图工具。优点是图永远跟着代码走支持聚合和过滤缺点是第一次生成结果往往“太诚实”会把代码里真实存在的混乱全部暴露出来。这三条路线不矛盾。我的实践结论是Archify 用于持续演进和版本对比手绘用于给老板和客户看的“概念图”。两者不冲突但如果你只能留一份一定留代码即时生成的这份因为它是活的。表格式对比会更直观方案准确性维护成本适合场景IDE 类图高低但不聚合单模块类关系排查手绘架构图低容易过期高对外汇报、方案设计Archify 代码地图高跟随仓库低需配置规则架构评审、依赖治理、文档自动化2. Archify 的工作原理从源码到一张可读架构图2.1 第一步不是画图是构建语言无关的符号依赖模型我曾经以为这类工具是靠正则表达式抓 import 语句后来看 Archify 的输出才发现不是。它做的是正儿八经的静态分析每个源文件被解析成 AST抽象语法树然后从语法树里提取类型声明、方法调用、字段引用、继承关系、接口实现等符号信息。在此基础上再把符号之间的调用和引用转换成一张有向图。这里有个很关键的细节它不只是看“A 文件 import 了 B 文件”而是精确到“A 类内部实际使用了 B 类的哪些方法”。比如一个 Service 里 new 了一个 Mapper 但没用它的方法这种边在图里可以被识别成低价值依赖。过度依赖 import 级别会造成很多假边而符号级别能过滤掉一部分让架构图更接近真实运行时的协作关系。对于多语言仓库Archify 的处理方式也不是把所有语言混在一起硬画而是为每种语言提供独立解析器最后统一转换到同一个内部模型。我在同一个仓库里同时有 Java 服务端和 TypeScript 前端它至少能分别生成两套图不会因为语法差异导致解析中断。这一点对于前端大仓尤其重要因为 TypeScript 的 re-export 和路径别名特别容易让工具分析出错。2.2 从类级依赖聚合到服务级边如果把几万个类之间的依赖全画出来架构图毫无意义。Archify 的可读性来自“聚合”这一步它会先建立类级依赖图然后根据聚合规则把节点向上归并。聚合的单位可以是包、命名空间、目录、Maven/Gradle 模块也可以是独立的部署单元。聚合的底层逻辑很像地图缩放街景级别太密就要切换到城市级别。Archify 在默认情况下会把 Java 包的依赖关系汇总成“模块到模块”的边同时把两点之间的多条依赖压成一条记录权重。比如user-service有 37 个类依赖order-service那它们在架构图上的连线就是一条很粗、很明显的边而不是 37 条乱线。这种聚合策略还会影响后续的架构评估。Archify 内部保存的是“原始依赖图 聚合层级结构”两份数据所以可以随时用命令行参数切换聚合粒度。我通常先用“模块级”做整体评估发现某个模块依赖异常复杂时再下钻到“包级”甚至“类级”。这种层级缩放体验和用在线地图看城市路网的感觉非常像这也是它被称为“代码地图”而不是“代码依赖图工具”的原因。2.3 布局与渲染为什么不能只靠“自动排列”架构图最难的不是生成节点和连线而是布局。如果只是把依赖图丢给通用图布局算法结果大概率是一坨交叉线。Archify 用的是分层布局思路先通过拓扑排序确定节点的横向层级同一层的节点尽量平铺跨层的边方向保持一致这样图读起来会有清晰的“从左到右”或“从上到下”的依赖流。但现实项目存在循环依赖拓扑排序不可能完全成功。Archify 在处理循环依赖时有两种表现要么在图上用特殊颜色标出环要么允许你配置break-cycles让它可以强行分层但会把环上的某条边标记为反向。这个设计很务实它没有假装代码里不存在循环而是把问题显性化。渲染环节还有一个容易被忽略的点节点大小和边粗细。如果节点大小不反映实际代码规模/类数量读者很难判断哪个模块是核心。Archify 默认用节点包含的类数量决定节点面积用依赖数目决定边宽。我第一次生成时看不懂图后来发现其实就是把“代码规模”可视化了越大的框代表越重的模块越粗的线代表越强的耦合。看懂了这一层读图效率会高很多。3. 实操从安装到生成第一张架构图3.1 安装与初始化以及我建议的目录约定Archify 的安装方式很简单我可以直接给出一条命令。以 macOS 环境为例brew install archify archify versionLinux 或者 CI 环境可以用二进制包或者直接拉 Docker 镜像运行。有一点要提醒Archify 在扫描仓库时并不要求你安装对应语言 SDK因为它做的是静态解析但某些语言如果要精确解析需要下载语言自身的解析器插件。Java 项目一般开箱即用Python、TypeScript 也还好Go 需要在初始化时确认一下 GOPATH 环境。首次使用建议先初始化初始化会在仓库根目录生成archify.yamlarchify init --repo .我遇到的第一个“坑”是它默认把.git、node_modules、target、build都排除了但没有排除我项目里的vendor目录。初次扫描结果里有大量第三方源码图根本没法看。所以初始化后第一件事是检查 exclude 配置把所有依赖目录和生成目录都排除掉。我自己还会把docs/、scripts/、deploy/这类非业务源码目录也排除它们对架构图没有贡献。3.2 一个能直接跑的最小配置这是我实际使用的最小配置后续所有调优都是在这个基础上加参数project: name: mall-admin-backend language: java scan: entry-points: - services/*/src/main/java - common-lib/src/main/java exclude: - **/generated/** - **/target/** - **/src/test/** cluster-by: package render: layout: layered node-labels: auto show-edge-weight: true output: svgentry-points是告诉 Archify 从哪些目录开始构建依赖图而不是让它猜。如果你有多个模块的源码分散在不同目录最好显式列出来。exclude里的src/test我一开始没有加结果测试代码里大量 Mock 依赖污染了架构图加完后清爽很多。cluster-by: package表示首先按包聚合。对纯后端项目来说包聚合已经能看出分层是否合理。但如果是微服务仓库我后面会把cluster-by改成module或者service这个参数是控制“缩放级别”的核心。3.3 生成命令和真正的“秒生”体验最小配置写好后第一次生成我用了这条命令archify scan --repo . --format svg --output docs/arch/mall-services.svg --cluster-by module第一步全量扫描大约花了 40 多秒对一个接近 20 万行代码的仓库来说可以接受。第一次跑完Archify 会把解析结果缓存到.archify/cache目录第二次运行时扫描明显变快。我在同一个仓库里改了一行代码后重新生成耗时不到 2 秒这就是“秒生”的真实状态它秒的不是首次全量分析而是增量缓存后的重新生成。如果你要嵌入文档或 Wiki建议同时导出 JSON 版本。SVG 适合人看JSON 适合后续做 diff 和 CI 判断。我的命令一般是archify scan --repo . --format svg --format json \ --output docs/arch/mall-services.svg \ --output-diff docs/arch/mall-services.json这样架构图既保持了可视化也能参与版本化管理。3.4 第一次生成的图为什么不能直接用翻车复盘我第一次生成的图说实话非常“真实”真实得让人尴尬。图上能看到服务边界已经乱了common-lib里居然有模块反向依赖业务服务order-service和user-service之间存在大量双向调用整个图呈现为中间一团毛线四周散落着和主架构无关的内容。这里要强调一个心态代码地图工具的价值不是把烂架构变成漂亮图而是把烂架构暴露出来。如果你生成的图很乱大概率不是工具的问题而是代码依赖关系本身需要治理。我当时做的第一件事不是急着调过滤参数而是把这张图发给团队做架构评审。正因为图足够准确大家才意识到两个服务之间互相调用的现象已经到了需要干预的程度。当然有些“乱”是过滤参数不对导致的。比如测试代码、代码生成器和工具类没有排除干净。我建议第一次生成后先做“减法”把明显不参与业务架构的节点和边从配置里排除等图整体可读后再考虑加min-edge-weight这类权重过滤。这个顺序很重要否则你会在一张包含噪声数据的图里反复横跳。4. 让架构图真正适用于微服务仓库的调优实践4.1 用 cluster-by 先聚合出服务边界微服务仓库跟单模块仓库最大的区别是“服务边界”比“包边界”更接近架构语义。如果按包聚合一个服务内部的所有包会散落在图上看不出服务是谁。因此我在微服务仓库里几乎不用默认配置而是把聚合级别提到服务模块cluster-by: moduleArchify 会识别 Maven/Gradle 模块或目录结构把每个微服务当作一个节点服务之间的 HTTP 调用、RPC 调用、数据库共享、消息队列生产和消费关系会变成节点之间的边。这一步做完图的规模立刻从几千个节点降到几十个节点架构评审才能聊起来。节点变小之后需要看服务内部依赖时再单独跑一次archify scan --cluster-by package --subtree order-service只展开单个服务。这种由粗到细的方式比一张全量图吃遍所有场景要合理得多。我甚至会在同一个项目里维护三个视图服务全局图、关键服务内部图、核心类依赖图三张图都由同一份源码生成从不同粒度回答不同问题。4.2 通过 min-edge-weight 过滤低频噪声服务数量少的时候权重过滤不重要服务数量超过二三十个低频依赖就会变成噪声。比如notification-service只因为一个工具类依赖了common-lib图上也会画一条线。所有服务都和common-lib连线后整张图看起来就是一个“海星”没法区分哪些服务是真正的高耦合。我的做法是设置最小边权重archify scan --repo . --min-edge-weight 3 --cluster-by modulemin-edge-weight的含义是两个节点之间的聚合依赖边权重小于 3 就不画出来。这里的权重和类数量有关一般“一个类调用另一个类的方法”算权重 1。min-edge-weight3意味着只有至少三个类共同产生依赖时才会显示连线。设置后低频偶然依赖被过滤图上留下来的基本都是核心关系。但要小心权重过滤也可能把重要的“非典型依赖”隐藏掉。例如一个服务通过一个硬编码 Feign Client 调用另一个服务权重只有 1但它可能是架构规范不允许的跨层调用。为了兼顾精度我会控制在图上隐藏权重小于阈值的边但在 JSON 输出里保留完整依赖再用 CI 规则去检查那些“合法但低频”的边是否违反架构约定。4.3 处理跨服务调用与 HTTP 端点识别生成服务间架构图时Archify 能不能识别 HTTP/RPC 调用决定了图的业务准确性。以 Java Spring Boot 仓库为例它会解析FeignClient、GetMapping、PostMapping这类注解结合 RestTemplate/OpenFeign 的调用点把两个服务之间的线上调用关系画出来。这就是“代码地图”和纯静态类图不同的地方——它会尝试理解你实际对外暴露的接口语义。不过自动识别总有边界。我遇到过user-service通过动态构造 URL 调用order-service代码里没有声明式 Feign Client而是直接把服务名字拼进 URLArchify 只能看到字符串常量无法可靠判断目标是谁。这种情况我不会骂工具因为它本来就不该靠猜。解决方式是继续人工维护一个覆盖文件overrides.yaml在配置里显式声明这两个服务之间的调用关系overrides: - from: user-service to: order-service kind: http note: 通过注册中心动态调用无法静态解析Archify 会把 override 边合并进最终架构图。虽然多了一步人工维护但需要维护的只是那些“动态到无法自动识别的边”数量通常很少比维护整张架构图成本低得多。5. 不局限于“一张图”把 Archify 接入 CI 与文档5.1 架构漂移检测当代码地图与预设规则发生冲突架构图如果只用来“看”价值会大打折扣。真正让 Archify 进入日常流程的是它的 diff/check 能力。第一次扫描后我会导出一份 JSON baseline提交到仓库里。之后每次代码变更都可以让 Archify 对比当前依赖图和 baseline识别出新增加的依赖边或消失的模块。这比人工 code review 找架构问题可靠得多。我建立了一套简单的规范新增边本身不报错只有新增边中的“反向依赖”和“跨层调用”会报错。比如我们规定 controller 层不能直接依赖其他服务的 repository 实现当有人提交了这样的调用时CI 阶段会输出类似这样的信息[Archify] New dependency edge found: order-service.controller.checkout - payment-service.repository.AccountRepository [Rule] violation: controller should not access repository of other service这种机制把架构评审从“靠经验、靠记忆”变成“靠规则、靠工具”。团队越来越多人愿意提交架构图相关的 MR因为检查是自动化的不需要架构师逐行盯着。5.2 在 GitLab CI 里实现自动检查和实况图更新下面是我在 GitLab CI 里实际运行的简化配置stages: - arch arch-check: stage: arch image: archify/archify-ci:latest script: - archify scan --repo . --format json --output arch-current.json --cluster-by module - archify check --baseline baseline.json --current arch-current.json --rules archify-rules.yaml only: - merge_requests这个任务每次 MR 都会运行不满足规则时会让 Pipeline 失败。另外还有一个定时任务每天凌晨重新生成全量架构图并提交到文档仓库保证 Wiki 里的“系统架构图”始终和主干代码一致。这里我想特别强调 baseline 的维护流程。不是每次架构变动都要重新生成 baseline那样等于把检查变成了摆设。我会只在架构评审确认“这次调整是预期的且规则已经同步更新”之后才手动刷新 baseline。其他时候Archify check 的任务就是证明“代码没有偏离预期架构”。这种“预期架构”长期稳定的前提是团队愿意维护架构边界而不只是命令工具闭嘴。5.3 将 SVG 嵌入 README 与内部知识库很多架构图工具生成的是 PNG放大容易糊而且无法被搜索引擎索引。Archify 默认支持 SVG 输出这个细节我特别看重。SVG 可以直接嵌入 Markdown 文档点击后还能无限缩放也方便浏览器搜索节点文本。README 里嵌入代码地图的做法## 系统架构 当前架构图由源码自动生成请勿手工编辑。 更新时间每个 MR 合并后自动更新。 内部知识库如果支持 HTML还可以直接加载 SVG并添加节点跳转链接。我给order-service节点加过 wiki 链接点击节点就能跳到服务专属文档。这个体验对新人很友好他们从全局架构图开始沿着节点进入服务详情再展开类级依赖图基本不需要人肉讲解就能掌握系统全貌。“代码地图”不只是给架构师看的它的目标用户应该是所有需要阅读代码的人。低频使用者需要一张地图快速定位自己要找的东西在哪个区域高频使用者也需要一张图来理解变更影响面。6. 常见认知误区、实际坑位与我的建议6.1 扫描慢不等于工具弱增量缓存是正解很多人第一次跑 Archify 时看到全量扫描几十秒甚至几十秒以上会觉得“秒生”是吹牛。这里有个认知偏差所谓的“秒生”是指增量构建不是冷启动全量解析。我现在的使用习惯是大型仓库第一次 scan 放在本地或 CI 定时任务里生成缓存后后续所有交互式操作都是秒级响应。缓存目录可以考虑提交到公司内部共享存储而不是每个人本地重新生成。比如我让 CI 每次跑完把.archify/cache传到制品库本地开发时再拉下来。第二次扫描直接命中增量缓存速度体感接近即时。当然如果仓库里每天有大量文件变更缓存命中率会下降这时候不要纠结速度全量扫描本来就有它的价值稳定优先于快。6.2 循环依赖图上一团毛线时先修依赖还是先调图循环依赖在代码地图上的表现很讽刺如果图布局算法是“分层”的遇到循环就会出现反向连线整张图就像有人把橡皮筋缠在了一起。我看到很多团队会用过滤参数把循环依赖隐藏掉让图变“好看”。我不推荐这么干因为循环依赖是架构质量的预警信号隐藏它等于埋雷。正确做法是先用 Archify 找出所有环再按环的严重程度逐个修复。Archify 有专门列出环的命令可以输出环上所有节点和边。我在一个老仓库里找到了一个隐藏很深的循环user-service-auth-service-common-security-user-service表面看没有直接循环但三个服务之间存在反向依赖。这种环如果不靠工具人工很难一眼发现。如果一时没法修至少要在 CI 规则里加一条“禁止新增参与循环的依赖”防止环扩大。图可以暂时接受乱但架构恶化趋势必须可视化并限制增量变化。6.3 动态语言和反射调用识别不了时怎么办Archify 对 Java、Kotlin、C# 这类静态类型语言的解析质量比较高但对 Python、JavaScript 这种动态语言识别精度会打折扣。尤其是 Python 里常见的importlib.import_module(...)或 Django 的魔法字符串关联静态分析基本无能为力。同样Java 里如果大量使用反射加载类Archify 能看到字符串常量但不会自动推断它指向哪个类。对应的方案依然是用 overrides 机制手工补充。我还会结合另外一个习惯把必须手工维护的override文件视作架构文档的一部分里面每一条都要写清楚为什么静态解析不了。这样即使工具本身失效人也能通过 override 文件看到“这些是约定不是代码事实”。对于纯动态项目Archify 更适合用来展示模块结构而不是展示细粒度调用关系。6.4 我对 Archify 适用边界的总结工具再好也有边界。Archify 解决的问题是“代码仓库当前状态的逻辑视图”它回答的是“系统由哪些模块组成、模块之间实际存在什么依赖”。它回答不了“系统运行时有多少实例、请求链路的性能瓶颈在哪里”这类运维和运行时问题。使用时不要指望一张图替代掉 APM、链路追踪和部署架构图。我个人使用下来的判断是单体仓库、微服务仓库、前端大仓都适合用但它最擅长的是有稳定语言生态、有明确模块边界的仓库。如果仓库本身没有分层所有类堆在一个目录里Archify 生成的图会很扁平价值有限。这种情况下工具给最大的帮助是“让你看到没有边界的仓库长什么样”然后你反而应该先去补架构设计而不是继续堆功能。最后再分享一个小技巧我每次做重构前会在重构分支上跑一次 Archify diff对比重构前后的架构图变化这样可以把“重构是否让依赖更清晰”这件事量化。比如把某次重构前后全仓库的反向依赖数量从 12 条降到 3 条评审时把这个数据贴出来比任何架构设计文档都有说服力。代码地图真正有意思的地方不在那张图而在于它能不断提醒你代码每天都在回答你它到底长成了什么样子。