仓库里躺着一个叫 REA 的模块没有 README没有需求文档连当初提交代码的人都早已离职。这是今年我在某公司接手遗留系统时遇到的第一道坎。说实话这种三字母缩写项目比完全没文档的项目更磨人因为它的名字本身就有很大的误导空间你连它当初想做什么都得靠猜。这篇文章我想把这几个月的完整经历复盘一遍从看到一个陌生代号到把它变成可维护、可二次开发的代码资产中间踩过的坑、用过的办法都整理出来。如果你也正准备接手一个来路不明的老项目或者正在为历史代码做维护升级这篇应该能给你省下不少时间。先交代一下背景这个模块不是某个知名框架也不是领导随手起的名字而是某公司内部业务系统里一个长期无人维护的遗留模块。它存在的唯一证据就是代码仓库里的一个目录以及日志里偶尔冒出来的 rea 前缀。我最初以为 REA 是什么业务名词的缩写查了一圈资料无果最后只能老老实实从代码本身出发。整个过程分成了六个阶段摸边界、重建上下文、恢复运行、风险体检、谨慎二次开发、沉淀工具。下面按顺序讲每一段都会给出我可以直接抄的步骤和判断依据。1. 拿到 REA 这个代号之后我先做了这三件事1.1 先别急着读代码把项目边界画出来接手陌生代码最忌讳的就是一头扎进源码逐行读。我见过太多人花了一周时间把一个模块的每一行都看了一遍结果问起这个模块对外提供了什么接口、依赖哪些外部服务照样答不上来。这不叫理解项目这叫脑内缓存。我的第一步是只看目录结构不读实现。把 REA 模块的根目录整个列出来标记哪些是源码目录、哪些是资源目录、哪些是构建产物、哪些是测试代码。这个动作的核心目的只有一个搞清楚这个模块的入口在哪里。比如它是一组可以被调用的类库还是一个独立运行的后台任务或者是一个 Web 服务的子模块入口决定了你后续理解它的顺序。第二步是查看构建脚本。无论是 Maven 的 pom、Node 的 package.json还是纯 shell 脚本构建文件本身就是最权威的模块说明书。它会告诉你这个项目在正常情况下应该输出什么产物依赖哪些第三方库打包后部署到哪里。通过构建脚本我在十分钟内就确认了 REA 是一个被上层服务周期性调用的批处理组件而不是一个常驻服务——这个定位直接决定了我后面可以跳过一整类跟网络协议、请求路由相关的分析。第三步是拉起依赖关系图。用工具生成这个模块对内部其他模块的引用列表以及外部系统对它的反向引用。这一步不用很精确目的是形成一张粗糙的上下游地图。知道谁在调用它、它调用谁你就知道这个模块在整个系统里处于哪一层。这一步花不了多少时间但后面分析业务逻辑时它的作用等同于是拿到了一张藏宝图。1.2 从命名机制倒推业务意图边界画完我开始留意命名。代码里的包名、类名、方法名、数据库表名甚至常量名都藏着作者当年对业务的理解。REA 这个名字本身没有给出直接答案但它的内部命名体系会给出大量间接线索。我看到包名大致是 com.example.rea.core、com.example.rea.job、com.example.rea.api 这种结构。这表明模块内部有一个核心逻辑层、一个任务调度层、一个对外接口层。单从这个分层就能推断出它的核心功能应该是围绕数据处理组织的而且存在明确的周期触发机制。继续往下看类名就更有意思了。有一批类名带 Transaction、 有带 Adjustment还有带 Ledger这让我当时就产生了一个假设这个模块很可能跟财务相关的账务处理、交易核对、流水调整有关。这里有个技巧值得单独说一下当你的猜测和一个大概率成立的业务域对上时不要急着高兴要同时准备两个备选假设。比如我当时的备选假设是它可能是某种报表计算引擎两个假设带着去看代码你会发现代码会快速帮你排除掉错误的那一个。我还特意看了日志里打印的关键字。因为日志是运行期产物它记录的是模块真实执行过的动作。一条 rea transaction adjust finished 日志比任何注释都更有说服力。结合这些我心里已经大致有了一个轮廓REA 负责从某个上游抽取交易数据经过清洗和调整再写入某个账务存储。这个轮廓不需要百分之百准确它只是给后续分析搭了一个脚手架后面所有细节往里填。1.3 给代码库做一次资产登记有了初步轮廓我开始做资产登记。这是后面所有工作的重要基础也是容易被忽略的一步。所谓资产登记就是把代码库里有价值的东西全部列成一份清单而不是让这些信息散落在各个文件里。我的登记表包含这几列资产名称、类型配置/接口/核心算法/批处理任务/测试用例、位置、状态活跃/疑似废弃/不可确认、备注。走查了一个小时后我登记出两个核心处理流程、五个对外接口、七个配置项、三个批处理入口、一小撮测试用例以及一个特别可疑的死代码区域——有一段看起来像是在做汇率换算的逻辑但没有找到任何调用它的地方。这个登记过程不能在 IDE 里随便看看就完事要点在于强制自己给每一项标注活跃或疑似废弃。因为老项目最大的特征就是留着不敢删的代码特别多而这类代码会极大地干扰你对模块真实运行状态的理解。把活跃资产和疑似废弃资产分开后续分析效率能提高一大截。2. 没有需求文档时如何从代码本身重建上下文2.1 配置文件是第一个突破口在没有任何需求文档的情况下配置文件是我最信任的信息源。它不像代码那样有复杂的调用关系配置里的每一个键值对几乎都对应着运行环境的某个真实要素可靠性相当高。REA 的配置分散在两个地方一个是 resources 目录下的 properties 文件另一个是部署环境里单独的配置文件。最开始我看 properties 文件时一头雾水因为里面的键名都是缩写比如 src.sys.id、dst.pool我连它指什么都猜不准。后来我把这些键按照数据源、目标、任务参数、运行阈值四类重新归纳了一遍整个逻辑一下清晰起来。比如有一个键专门配置上一次成功处理的时间戳这说明模块内部必须支持断点续跑否则不需要持久化这样的状态。这里给一个非常实用的建议拿到配置文件先把所有键名列成一张表尽量给每个键名补上你推测的含义。遇到拿不准的不要自己瞎猜直接全仓搜索这个键在哪里被读取。搜索引用比看着键名猜含义可靠得多。我用这个办法确认了至少三个配置项的真实用途也发现了有一处配置根本没有代码读取它——也就是遗留的无效配置。无效配置本身不算大事但它意味着这个模块历史上经历过一次或多次调整这对判断它的演进方向有价值。2.2 依赖清单暴露真实技术栈配置看完了接下来通过依赖清单判断技术栈和项目历史。我发现 REA 的主依赖里有两个比较显眼的特征第一使用了某个老版本的 ORM 框架这个版本本身已经相当古老第二引入了一个不常见的数据导出组件专门负责生成某种固定格式的报表文件。单单这两条就能推导出一个合理结论这个模块早期的主要产出可能包括批量账务文件而且是需要按固定文件格式输出的那种。依赖的传递关系也很值得看。因为某些传递依赖会把一些你根本用不到、但历史原因引入的库带进来。这些库经常是安全扫描的重点对象也是升级时最头疼的地方。看清楚传递依赖之后你就能判断升级某个主版本到底会波及哪些库、哪些可能会冲突。这其实比看代码更紧迫因为遗留模块能不能继续生活在现有基础设施里很大程度上取决于它跟当代依赖体系是否兼容。我的习惯是把依赖分成运行必需、编译必需、测试必需、可疑多余四类。运行必需是容器启动或任务执行时真正加载的编译必需是开发环境编译时需要但运行时不打包的测试必需只在测试阶段出现可疑多余则是查不到实际引用、可以尝试移除的。给 REA 做完这个分类之后我发现至少有两个库只出现在构建脚本里没有任何代码引用。移除它们之后构建速度有了可感知的提升虽然不大但对一个老项目来说已经是个好兆头。2.3 用注释和提交信息拼凑演进史代码不会说话但提交记录会。我花了一个下午把所有跟 REA 相关的 git 提交历史翻了一遍提交信息写得参差不齐有的只有 fix 两个字有的倒很认真地写清楚了改动原因。这里有一个技巧不要只看最新的几条提交要重点看首次出现和大规模修改这两个时间点。首次出现的提交决定了这个模块的出生原因而大规模修改则往往伴随着业务转型或技术升级可以帮你理解为什么代码里会同时存在新旧两套写法。提交历史之外注释也值得读但读法有讲究。我会先搜索注释里的 TODO、FIXME、HACK、XXX 这些标记统计它们的密度和文件分布。如果一个文件里有十个 TODO而且集中在某个区域这说明这里作者自己也心虚属于欠债重灾区。接着我会快速浏览核心方法前面的注释不是看它写得好不好而是看它有没有指向某个业务规则。比如 REA 里有一个调整金额的方法注释里写了一句负数代表冲销不改变原流水状态。这句注释直接解答了一个大疑问也让我们后来在二次开发时少走了一大段弯路。注释的情绪也值得品味。老工程师往往会在代码里留下一些半吐槽半解释的话比如此逻辑极其绕改动前请先确认 XX 的口径。这类句子说明这里曾经出过事故是典型的前车之鉴。把这些注释当成最高优先级的警告信号比任何代码评审都靠谱。最终通过提交历史加注释我拼出了 REA 的一段演进史它最初是为一个特定的账单核对场景设计的后来经历了一次大的数据源切换再后来又加上了报表输出能力。这解释了代码里为什么会有大量看似重复的分支逻辑——每一段都是历史留下的脚印。3. 让旧代码重新跑起来环境复原与依赖坑3.1 构建脚本里的隐形约定要把一段你不能运行起来的代码吃透几乎是不可能的。所以我的下一个目标就是先让 REA 在本地跑起来。这个动作看起来简单但老项目的环境复原往往是整个过程里最费时间的一个环节因为构建脚本里藏着大量隐形约定。REA 的构建脚本很简朴就是一行 mvn clean package。但真正执行的时候问题接踵而至。首先它对 JDK 版本有隐性要求脚本里没有明确约束只有旧版本的 JDK 才能编过。其次它假设工作目录是某个固定路径因为构建脚本里有一段相对路径的引用。第三它依赖一个已经不在内部仓库里维护的私有构件这种私有构件在本机恰好存在、新机器上完全找不到的情况在老项目里几乎必现。解决这些问题的思路是逐层逼近。先用 Docker 拉起一个旧版本 JDK 的环境把基础编译跑通再通过错误信息反推缺失部件一个个补最后把成功环境下的版本号、路径等信息固化成一份文档。这里切记不要通过修改业务代码绕过编译错误——老项目里的编译报错常常是环境导致的假信号直接改代码会把问题混进来。先确认环境完全一致再判断代码是否需要改。3.2 依赖版本不一致引发的连锁故障编译过了只是第一步真正跑起来时依赖版本不一致引发的连锁故障才叫人头疼。我把这次遇到的坑记下来因为太典型了。REA 在代码里调用了某个 ORM 框架 3.x 版本才有的 API但实际落地的依赖清单里带着的是 2.x 版本的一个传递依赖。编译期恰好能过因为编译用的本地仓库里有 3.x 的 jar可运行时又从传递依赖里拉到了 2.x。结果就是一启动就报方法不存在。这类问题最有迷惑性的一点在于报错的位置离真正的原因很远。你看到的是某个工具类初始化失败但真正的原因是 jar 包版本冲突。排查手段就是把依赖树完整打印出来逐条核对跟业务直接相关的关键库到底出现了几个版本、谁在依赖谁。我自己的排查顺序是先看最核心的业务包再看序列化、连接池这类容易造成诡异问题的基础包。如果发现同一个库同时有多个版本存在尽量不要只靠调整依赖声明去压版本号先搞清楚为什么会有两个版本。很多时候是两个上游模块各自引入导致的分叉光在 REA 这边声明一个强制版本可能会影响其他模块。正确做法是确认全局依赖仲裁之后再决定锁哪个版本。这个原则不只是在 REA 上适用所有老项目跑不起来的情况基本都能套用。3.3 补齐运行所需的隐藏配置版本问题解决之后REA 进入能启动但没数据的状态。这又引出另一类问题隐藏配置。所谓隐藏配置就是代码里写死、或者依赖运行时环境变量、或者需要外部服务才能正常工作的一些参数。这些参数在文档里根本没有你只能从报错信息里一点点摸出来。我举个例子。REA 启动时需要连接消息队列服务但本地没有这个服务它就一直启动不成功。我没有第一时间去搭一个完整的消息队列而是先看代码里对消息的处理逻辑发现它根本不需要实时消费消息只是在启动时注册了一个消费者。于是我直接把这个连接逻辑做成可配置的开关本地用模拟实现代替。这算是对老项目最小可行改动原则的坚持能不动的代码绝对不动能通过配置解决的坚决不改代码。等到业务数据流程真正跑起来我又发现它需要读取一批全国编码表数据这个数据存在一个远程文件服务器上。本地没有这个文件的授权我就构造了一批最小规模的样例数据保证字段结构和分布跟真实数据一致然后把数据源路径做成配置项。整个过程最重要的收获不是让 REA 跑起来了而是把原先散落在代码里的硬编码逐个找出来列成了一个待外部化管理的清单。这个清单后来成了改造计划的一部分。4. 我不建议直接重构但建议先做一次风险体检4.1 圈复杂度与坏味道的快速扫描让 REA 跑起来之后理论上就可以开始改功能了。但我强烈不建议一上来就动手重构。对一个不了解其业务规则的模块来说重构无异于在蒙着眼拆炸弹。正确的办法是先在代码级别做一次风险体检。我只关注四类风险指标圈复杂度、方法长度、重复代码块、异常吞没点。没有直接用复杂的商业工具就用静态扫描脚本加上手动抽查。圈复杂度偏高的方法集中在那几个核心的账务处理方法上这完全符合预期因为这些方法承载了大量业务分支。我注意到一个特别危险的坏味道有一个方法在处理异常时直接返回了默认值并且注释写着此情况不应出现。这句注释本身就说明异常情况已经出现过只是被吞了。不应出现四个字在老项目里基本等于一定会出现。接着我用一个伪造的异常数据进去测试果然触发了那条路径返回了一个看似正常但完全错误的结果。这种地方就是重构时最需要加防护的靶点。4.2 业务逻辑与技术债的分层扫描出大量高风险代码之后我没有急着开改而是把所有问题先分成三类第一类是乱但有价值这部分逻辑是模块的核心竞争力代码写得难看但规则正确绝不能轻易动第二类是乱且重复同样的规则在好几个地方各写了一遍是重构的主要对象第三类是乱且无用根本没人调用的死代码可以逐步清理。分类的依据有三个被调用次数、是否有测试覆盖、是否被业务规则注释引用。被调用次数可以从调用链分析或者日志频率中估计测试覆盖直接决定了改动的风险上限引用了业务规则的注释说明这段话背后有明确需求。把这三类问题标成红黄绿三色之后整个模块的健康状况一目了然真正危险的是红色区域里的乱但有价值代码既重要又脆弱。我先不去动它而是在它周围建测试护栏。只有先把行为锁定下来后续的重构才有底气。4.3 要不要重写的判断清单风险体检之后自然会产生一个灵魂拷问这代码这么烂要不要推倒重写我的回答是先别急着决定用清单打个分。我给 REA 设计了五个维度每个维度一分到五分总分越高越值得保留修复越低越可以考虑重建。第一个维度是对外契约的稳定性。如果外部系统已经严重依赖这个模块的接口格式和行为重写成本会非常高。REA 的对外接口虽少但被多个上游调用这个维度可以得四分。第二个维度是依赖耦合度。REA 对整个技术栈的依赖非常深很多第三方库都是老版本强行重写意味着整个生态要跟着升级这个维度得两分。第三个维度是测试覆盖。REA 几乎没有像样的测试连个能锁住核心行为的测试都没有重写带来的回归风险无法估量这个维度得一分。第四个维度是业务知识的可迁移性。很多业务规则只存在于代码里没有任何文档。重写时如果没有一个懂业务的人全程陪跑这些规则很容易被丢三落四。这个维度最难量化我给两分。第五个维度是运行稳定性。REA 虽然没人维护但在生产环境里运行了很久稳定性其实尚可说明核心逻辑经过真实业务验证这个维度我给四分。总分算下来刚好卡在中间偏保守的位置。所以最终的结论不是说不能重写而是不值得在现阶段重写。更合理的策略是保留骨架逐步替换高危部位。判断重写与否关键不在于代码写得有多烂而在于你对行为有没有足够深的把握。没有把握的时候换一种写法只会把已知的坑换成一个全新的坑。5. 二次开发时真正要避开的坑5.1 别让新功能绑定旧架构风险体检做完我开始进入真正的二次开发。这个阶段最大的诱惑是为了加一个新功能顺便把周边的旧代码也改头换面。我吃了这个亏才学乖的。当时 REA 需要增加一个新的输出格式我一看旧代码里的报表生成逻辑很乱顺手就想换成更现代的写法。结果新写法引进来之后跟旧逻辑里的状态处理方式冲突一个不起眼的空指针故障折腾了我两天。经验总结成一条原则新功能不要绑定旧架构的改造。你想引入新的设计模式、新的工具库应该先在最外层做防腐层隔离。也就是说新功能自己走一套相对独立的内部实现只在边界处复用旧模块对外提供的稳定接口。这样做的好处是新功能可以快速上线旧架构的改造可以在不影响业务的前提下逐步进行。我给 REA 加的输出格式最终就是用防腐层方式实现的旧代码一行没改新功能正常上线。5.2 给后人留下接入手册在二次开发期间我顺手做了一件非常值的事给 REA 写了一份简短的接入手册。这份手册不追求面面俱到只解决三个关键问题怎么运行、怎么配置、哪些地方是坑。我把它放在模块根目录下起名 README。内容非常朴素包括本地环境复原步骤、配置项对照表、常见的启动报错及处理方法以及三个特意标注的不要动区域。写这份手册的直接原因是我自己刚接手时太痛苦了想让下一个接手的人少走我走过的弯路。但没想到它后来还成了我们内部做交接工作时的标准模板。很多老项目不是没有文档而是文档跟代码脱节。接入手册不一样它只记录当前状态下真实有效的信息代码一变你自然会发现手册需要更新。这份手册你可以在下次接手任何项目时用上尤其是那种历史包袱重的模块文档最大的作用是让新人不至于因为找不到入口而过度焦虑。5.3 逐步替换而不是一次性推翻最后一个避坑建议也是最关键的一条处理老项目永远选择逐步替换别选一次性推翻。我见过太多重构项目死于规划了一个完美的目标架构然后集中力量在短时间内重写全部代码。这种做法的风险不在于重写本身而在于你缺乏足够的时间去验证每一部分的行为。等全部写完再验证一旦出问题你就根本不知道是哪里错了。我采用的是绞杀者模式选定一块最独立、最容易验证的功能先拿新实现替换掉旧实现并且让新旧两套代码并行运行一段时间结果比对一致后再切流量。整个过程不是一口气完成的而是分成了多个独立的上线窗口。第一个窗口我先替换了最外围的报表生成逻辑因为它的输入输出可以精确对比第二个窗口我才替换了中间的数据清洗部分依靠左侧日志比对来确认结果核心的账务调整逻辑只做小范围优化不敢动大手术。这样每一轮改动都是可控的即使出了问题影响面也是一个小圆圈而不是整棵树。6. 代码考古工具箱一份可复用的排查清单整个过程走下来我觉得最有价值的产出不是修好了 REA 这个模块而是沉淀出了一套可以复用的排查清单。这里把它完整列出来。以后不管接手什么项目哪怕是完全不同的技术栈、完全不同的业务这套清单都可以帮你在最短时间内从一脸懵变成心里有谱。第一步资产登记。列出目录树、构建文件、依赖关系区分活跃资产和疑似废弃资产。第二步配置清点。把配置键全部列出来搜索引用位置标注每个配置的真实用途标记失效配置。第三步依赖分析。区分运行必需、编译必需、测试必需、可疑多余四类依赖打印完整依赖树排查冲突。第四步运行环境快照。记录 JDK、中间件版本、私有仓库地址、环境变量、相对路径约定并验证在新环境能够重现构建。第五步代码风险扫描。看圈复杂度、方法长度、重复代码块、异常吞没点把结果汇总成风险地图而不是单个指标。第六步核心链路走查。沿着主流程从入口到出口走一遍抽出五个左右核心方法精读其余代码可以跳过。第七步测试缺口评估。找出哪些核心行为完全没有测试保护针对高危区域优先补测试护栏。第八步重构候选清单。把问题划分为绿黄红三类标记出值得保留修复、可以合并清理、绝对不要随意触碰的区域。这套清单用下来效果立竿见影。我甚至怀疑它才是这次接管 REA 项目的最重要收获。后来每到一个新项目我都会打印一份贴在显示器边上照着一步一步来。它不一定能帮你把所有代码都读懂但一定能帮你把有限的时间和精力放到最关键的部位上。如果你手头也有一个像 REA 这样的无名项目不妨拿这份清单先跑一圈然后再决定下一步怎么走。
阅读完成 · 觉得有帮助?