用Archify生成代码地图:让架构图随代码实时演进
2026/9/24 18:31:53 网站建设 项目流程

我最近在整理一个迭代了三年的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 version

Linux 或者 CI 环境可以用二进制包,或者直接拉 Docker 镜像运行。有一点要提醒:Archify 在扫描仓库时并不要求你安装对应语言 SDK,因为它做的是静态解析,但某些语言如果要精确解析需要下载语言自身的解析器插件。Java 项目一般开箱即用,Python、TypeScript 也还好,Go 需要在初始化时确认一下 GOPATH 环境。

首次使用建议先初始化,初始化会在仓库根目录生成archify.yaml

archify init --repo .

我遇到的第一个“坑”是它默认把.gitnode_modulestargetbuild都排除了,但没有排除我项目里的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: svg

entry-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-serviceuser-service之间存在大量双向调用,整个图呈现为中间一团毛线,四周散落着和主架构无关的内容。

这里要强调一个心态:代码地图工具的价值不是把烂架构变成漂亮图,而是把烂架构暴露出来。如果你生成的图很乱,大概率不是工具的问题,而是代码依赖关系本身需要治理。我当时做的第一件事不是急着调过滤参数,而是把这张图发给团队做架构评审。正因为图足够准确,大家才意识到两个服务之间互相调用的现象已经到了需要干预的程度。

当然,有些“乱”是过滤参数不对导致的。比如测试代码、代码生成器和工具类没有排除干净。我建议第一次生成后先做“减法”:把明显不参与业务架构的节点和边从配置里排除,等图整体可读后,再考虑加min-edge-weight这类权重过滤。这个顺序很重要,否则你会在一张包含噪声数据的图里反复横跳。

4. 让架构图真正适用于微服务仓库的调优实践

4.1 用 cluster-by 先聚合出服务边界

微服务仓库跟单模块仓库最大的区别是“服务边界”比“包边界”更接近架构语义。如果按包聚合,一个服务内部的所有包会散落在图上,看不出服务是谁。因此我在微服务仓库里几乎不用默认配置,而是把聚合级别提到服务模块:

cluster-by: module

Archify 会识别 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 module

min-edge-weight的含义是:两个节点之间的聚合依赖边权重小于 3 就不画出来。这里的权重和类数量有关,一般“一个类调用另一个类的方法”算权重 1。min-edge-weight=3意味着只有至少三个类共同产生依赖时,才会显示连线。设置后,低频偶然依赖被过滤,图上留下来的基本都是核心关系。

但要小心:权重过滤也可能把重要的“非典型依赖”隐藏掉。例如一个服务通过一个硬编码 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,而是直接把服务名字拼进 URL,Archify 只能看到字符串常量,无法可靠判断目标是谁。这种情况我不会骂工具,因为它本来就不该靠猜。解决方式是继续人工维护一个覆盖文件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 合并后自动更新。 ![系统架构图](docs/arch/mall-services.svg)

内部知识库如果支持 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 条,评审时把这个数据贴出来,比任何架构设计文档都有说服力。代码地图真正有意思的地方不在那张图,而在于它能不断提醒你:代码每天都在回答你,它到底长成了什么样子。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询