大家好,我是人月聊IT。今天继续聊源代码逆向工程。最近我对自己使用的源代码逆向工程提示词做了第二次比较大的调整。
第一次整理这套提示词时,我更关心的是“能不能从一个陌生项目里把东西找出来”。比如系统有哪些模块、有哪些接口、有哪些对象、用了哪些数据库表、采用了什么框架。做到这一步以后,文档看起来已经很多了,目录也很完整,但真正拿它去理解项目、分析需求或者准备修改代码时,还是会发现一些问题。
最明显的问题是:东西虽然都找到了,关系却没有真正串起来。
我可能知道系统里有一个传输配置页面,也知道有一个保存接口,还知道存在传输主表、属性表和目标关系表。但当我想回答“用户点击保存之后,究竟经过了哪些业务判断、调用了哪些类、最后改了哪几张表”时,仍然需要在好几份文档之间来回翻。页面说明在一处,接口在一处,Service 在一处,数据库表又在另一处。信息是全的,理解却仍然是断开的。
这次迭代,我主要就是围绕这些断点重新思考。

我一直强调“穿透”这个词。也可以把它理解为调用链,但又不完全等同于通常意义上的代码调用链。
现在有不少代码分析工具可以生成 Code Graph 或知识图谱。它们很擅长识别类之间的依赖、方法之间的调用、包与模块之间的引用关系。这些信息当然有价值,尤其是在分析代码耦合、影响范围和循环依赖时。但从业务系统理解的角度看,只看到类和方法之间的关系还不够。
业务人员不会从某个 Controller 方法开始理解系统,开发人员接到需求时通常也不是先问“这个类调用了谁”,而是先问:这是哪个菜单下的什么功能?用户进行了什么操作?这个操作改变了什么业务状态?最后数据落到了哪里?
所以我需要的穿透,起点必须是业务场景,而不是代码符号。
一条真正有用的链路,应该类似这样:
菜单入口
-> 页面或自动触发入口
-> 业务功能点
-> 页面事件或触发条件
-> 接口
-> 业务处理类和方法
-> 核心对象与外部依赖对象
-> 关键业务规则
-> 数据库表 R/C/U/D
-> 最终业务结果和状态变化
这里面任何一段断开,都会影响我们对功能的理解。
比如“保存传输配置”这个动作,不能只写成“页面调用保存接口,接口调用 Service,Service 调用 DAO”。这种描述在技术上没错,但对理解业务几乎没有帮助。我更关心的是:保存时如何判断新增还是修改?新增是否校验名称唯一?初始状态是什么?修改后是否需要重新部署?属性数据是差量更新还是先删除再重建?目标关系表是否同步修改?任一步骤失败时是否整体回滚?
最后还要明确回答:这个动作读取了哪些表,创建了哪些表数据,更新了哪些字段,删除了哪些旧数据。
这也是我这次进一步明确的一个原则:数据库不能只作为系统末端的一个附录存在,它必须进入每个功能点的主链。
过去写逆向文档时,很容易用一句“该功能涉及以下数据库表”结束。但“涉及”这个词太模糊了。
一个功能读取端点信息,和一个功能修改传输主记录,虽然都叫“涉及数据库”,性质完全不同。为了真正理解功能,我认为至少要区分几类数据对象:
仍然以传输配置为例。保存动作的核心数据不是一张表,而是传输主表、属性表和目标关系表。端点表和机器集群表虽然也会被读取,但它们属于外部依赖数据,当前功能没有权利修改。部署成功以后更新传输状态,这是核心状态变化;运行日志则属于部署之后的传输执行功能,不应该被误写成配置保存的直接数据落点。
把这些角色区分开以后,功能边界才会变得清楚。
这次迭代中,我还重新调整了功能发现的粒度。
一个菜单不等于一个功能,一个页面也不等于一个功能。一个页面里可能同时存在初始化、查询、详情、校验、保存、删除、提交和部署等多个独立动作。它们有不同的业务结果、不同的权限边界,也可能使用完全不同的数据表。
页面初始化也不能被忽略。很多页面进入以后会自动加载下拉数据、默认配置、用户权限和状态信息。这些行为可能调用多个接口,也可能决定后续页面能不能正常工作。如果只逆向页面上的按钮,初始化链路就会成为盲区。
同样,系统里还有大量没有菜单入口的功能。定时调度、消息消费、事件监听、回调、批处理、轮询、补偿任务和启动初始化,都可能产生真实的业务结果。菜单只是人工入口,不是功能清单的边界。
所以我现在更倾向于从四条路径交叉发现功能:从 UI 动作找,从服务接口找,从自动触发入口找,再从数据库写入路径反查。只有四条路径能够互相对上,功能清单才比较可信。
业务功能的穿透链很重要,但它不能替代对整体技术架构的理解。
逆向一个系统时,我仍然需要单独说明它采用什么架构方式,模块如何划分,部署单元有哪些,同步和异步边界在哪里,事务和安全边界在哪里。这些内容回答的是“系统整体如何组织”,不是某一个按钮如何执行。
在架构之下,还需要继续识别技术组件真正提供了什么能力。只写“系统使用了 Spring、MyBatis、消息队列”意义有限。更有价值的是说明这些组件在系统里具体提供了什么能力接口,哪些功能正在消费这些能力,失败以后影响什么。
例如数据库访问、集群节点发现、远程部署、文件协议适配、任务调度,这些都可以看成技术能力。业务功能文档里要说明自己依赖了哪些能力,但组件的提供方式、配置和消费者清单仍然应该单独整理。
安全认证、菜单权限、数据权限、流程引擎、通知、消息、文件、字典和审计,我更愿意把它们定义为公共能力。它们通常跨越多个业务模块,也有自己的对象、接口和数据表。如果把它们散落在各个功能说明里,很难看清系统对这些能力的整体依赖;如果完全独立出去,又容易失去与业务功能的关系。
比较合适的方式是双向记录:公共能力文档列出所有消费者,每个业务功能也明确说明自己使用了哪些公共能力,以及这些能力失败时会产生什么业务影响。
我之前容易把“全量逆向”和“详细逆向”混在一起。项目一大,就觉得不可能把所有功能都分析得很细,于是先挑几个核心功能展开,其他功能只保留一句话。结果是前两个功能链路很完整,后面的功能越来越粗,最后虽然文档很多,却无法确认系统到底有没有遗漏。
后来我逐渐意识到,覆盖完整度和分析深度应该分开。
第一层是全局基线。它不要求把每个方法里的所有分支都展开,但必须完整枚举范围内的模块、入口、功能、接口、任务、事件、对象、数据表、字段、组件和配置。每个业务功能至少要有一份需求面板和一条完整主链,不能只给“代表性功能”。
第二层才是单功能深挖。用户真正关心哪个菜单、哪个任务或者哪个业务功能,再沿着已有主链继续展开页面字段、子调用、规则分支、事务、异常、重试和字段级数据变化。
这样的好处是,项目再大也可以分批处理,但全局状态仍然知道哪里完成、哪里只是建立了主链、哪里还存在断链。不会因为没有时间细化所有功能,就把未覆盖误写成已完成。
这次迭代里,我认为最关键的变化之一,是把单功能逆向明确拆成两种模式。
第一种是业务和技术实现全集逆向。
它的目标是帮助开发人员真正理解现有项目,方便后续修改、排错和影响分析。因此每一个页面事件或功能点,都需要继续追踪到前端文件、接口、入口类、业务实现类、数据访问类、子调用、业务规则和数据库。规则还要尽量绑定到具体执行位置,数据库操作也要绑定到真实业务步骤。
这种文档回答的是“当前系统到底是怎么做的”。
第二种是技术实现无关的需求逆向。
它不关心当前项目用了什么框架,也不关心某个 Controller、Service 或 DAO 叫什么。它更像是从已有系统中重新提炼一份完整需求规格:用户看到什么页面,可以执行哪些功能,每个功能遵循哪些业务规则,会改变什么状态,最终创建、读取、更新或删除哪些业务数据。
这种文档回答的是“这个系统到底需要做什么”。
两种文档看上去相似,但用途完全不同。技术全集适合接手旧项目、分析变更和定位代码;需求规格适合理解业务、重构系统,或者使用另一种语言和框架重新实现。
从 MDA 的角度看,后一种做法有点像从已有实现中反向提取一个与语言和框架无关的模型。它不是简单地删除类名和接口,而是要把技术行为重新翻译成业务规则、状态变化和数据约束。
这里还有一个容易忽略的问题:现有代码行为不一定等于正确需求。逆向时如果发现修改名称没有校验唯一性,不能直接把“修改允许重名”写成目标需求。更合理的做法是分别记录现状行为、需求风险和待确认的目标规则。
单功能逆向需要说明 UI,但我不希望它演变成一项很重的原型设计工作。
截图受运行环境限制,图片不方便版本比较,重新制作高保真原型成本又太高。对逆向文档来说,ASCII 页面原型反而是一个比较实用的选择。
它可以表达页面区域、字段、按钮、列表、Tab、向导步骤、弹窗和状态反馈,也可以直接在控件上标注功能点编号:
+--------------------------------------------------------------+
| 传输名称 [____________] [F04 校验名称] |
| 源端点 [请选择 v] [F03 查看详情] |
| |
| [F06 保存] [F07 保存并部署] |
+--------------------------------------------------------------+
这样 ASCII 图就不只是“页面长什么样”,而是整个功能穿透的导航入口。点击、初始化、查询、保存等每个编号,都应该在后面对应一份完整的功能说明。
如果页面包含多个 Tab,每个 Tab 都应该有独立布局;如果是向导,每一步都要说明上一步、下一步和最终提交;重要弹窗、空数据、无权限和错误状态也不能只靠文字带过。
当然,在缺少前端源码时,ASCII 原型只能是推断。这时关键不是假装还原得很准确,而是明确哪些字段来自请求对象,哪些动作来自接口,哪些布局和控件类型只是根据业务行为推测出来的。

在实际构建提示词时,我越来越觉得,列一份输出目录远远不够。
如果只要求输出页面、接口、对象和数据库,模型很容易分别生成四份看起来完整的清单,但不会主动保证它们之间能够互相追踪。所以提示词中需要明确关系约束。
例如 ASCII 图上的每个控件必须映射到稳定的功能 ID;每个功能 ID 必须同时存在需求面板和实现链;每条写操作必须落到具体物理表;每张表要能够反查主写功能和读取功能;每个技术组件和公共能力要列出消费者。
在详细功能文档里,我现在更倾向于以功能点为单位纵向组织,而不是按前端、后端、规则、数据库横向分章。也就是说,“保存”这一章内部就应该同时出现页面位置、输入、接口、类文件、业务规则、表操作、事务和最终结果。读者不需要自己到第六章找实现、到第十章找数据库、再到第十四章找业务规则。
另外,证据等级也很重要。源代码能够直接确认的内容写事实,多处线索一致但没有闭环的写推断,只有命名依据的写假设。缺少 DDL 时不能编造字段长度和索引,缺少前端代码时也不能把推断的按钮文案写成事实。
文档之间没有死链,并不等于逆向完整。
如果一开始就漏掉了一个定时任务,那么所有已生成文档仍然可能链接正常。只检查文档内部关系,永远发现不了这个遗漏。
所以我给第二次迭代增加了更独立的交叉验证思路:重新从源码扫描入口、接口、任务、DAO、模型和数据库候选,再与已经生成的资产清单对账;同时从数据库表反查写入功能,从接口反查消费者,从公共能力反查业务依赖。
对单功能文档也要单独检查:ASCII 图上的每个动作是否都有功能卡片;技术全集是否真的追踪到了类、规则和数据库;需求规格是否有规则、数据落点和验收场景;两种模式有没有相互串味。
逆向工程最怕的不是写得少,而是看起来很完整,却不知道自己漏了什么。
这次调整以后,我对源代码逆向的理解又发生了一些变化。
它不应该只是把代码换一种方式描述,也不应该只是自动生成一批接口和表结构文档。真正有价值的逆向,是重新建立业务、实现和数据之间的关系,并且允许我们根据不同目的切换观察角度。
我要理解和修改旧系统时,需要的是从页面动作一直穿透到类、方法和数据库的技术全集;我要提炼业务、重构系统或者跨语言复刻时,需要的是摆脱当前框架约束的需求模型。
这两者不能互相替代,但可以共享同一套功能边界、业务规则和数据事实。
第二次迭代并没有让逆向过程变得更轻,反而增加了不少约束。但这些约束解决的是我实际使用文档时遇到的问题:功能不能只列名字,链路不能停在 Service,数据库不能只写“相关表”,页面也不能只有一张没有功能映射的示意图。
对我来说,逆向工程最终不是为了得到更多文档,而是为了让一个陌生系统逐渐变成一个可以解释、可以验证、可以修改,也可以重新实现的系统。