Martin Fowler

The ArchaeologistвҖҷs Copilot

6.9内容质量
The ArchaeologistвҖҷs Copilot

TL;DR · AI 摘要

The Archaeologist’s Copilot The Archaeologist’s Copilot Restoration of a 20-year-old Java “Big Ball of Mud” using...

核心要点

  • 主题聚焦:The ArchaeologistвҖҷs Copilot
  • 来源:Martin Fowler,建议结合原文判断细节。
  • AI 分析暂不可用,本条为保底评分与摘要。
#AI#编程#后端#云计算#安全
打开原文

考古学家的副驾驶

考古学家的副驾驶

使用 AI 和 Docker 恢复一个 20 年前的 Java “大泥球”

本文介绍了我用于现代化 Java 1.5 代码库的方法,该代码库在现代设备上已无法稳定构建。我早期使用 LLMs 时得到了看似合理的答案,但这些答案在代码库中并不成立。当我在证据基础上建立流程时,进展才开始出现:利用 AI 支持分析,在稳定的 Docker 环境中进行验证,并通过测试保护渐进式重构。主要收获是实用性的:当 AI 被证据、明确角色和逐步现代化策略所约束时,其效果最为显著。

2026 年 7 月 16 日

Nik Malykhin

Nik 是一名以色列软件开发人员,Thoughtworker 公司成员。除了对将 AI 应用于软件开发的兴趣外,他还认为在 AI 时代,纪律与人类协作的结合正变得越来越重要。

目录

  • “游客”陷阱 能力的幻觉 教训
  • 第一阶段:分析 考古学家提示 发现 1:碳测定文物 发现 2:“转录”陷阱 发现 3:说谎的测试 决策:隔离优于修复
  • 第二阶段:封装 任务:棕地修复 转向:“时间胶囊”策略 “湿”测试:扭曲现实
  • 第三阶段:提升(揭开文物) 硬件原理 “Java 17 陷阱” 执行:映射遗留结构 “说谎测试”发现 强化基线 AI 编译器反馈循环
  • 第四阶段:重构 掌握技艺 TestContainers 陷阱
  • 最终清理:并发与压力测试
  • 结论:交接 清理环境 项目路线图:README.md 最后思考:增强型考古学家

“游客”陷阱

我们组织中都有“那个”仓库。2005 年编写,使用 Ant 构建,基于 Java 1.5。自奥巴马政府时期以来从未编译过,当然也无法在你的新款 Apple Silicon MacBook 上运行。

当我继承这个“棕地”项目时,诱惑是将生成式 AI 视为万能翻译器。我将代码粘贴到 LLM 中,问出世界上最自然的问题:“我该如何运行它?”

我称之为“游客提示”。就像参观古迹的游客,我要求的是导览和纪念品商店的纪念品。我想要一条顺利的路径。

在我们的实验中,我确实尝试了这一点。我与标准 LLM 开启对话并询问:

“你好,我需要开始使用这个库。请帮我充当高级开发人员,指导我入门?阅读仓库并给我一个高层次的摘要……以及一个简单的‘Hello World’代码示例。”

能力的幻觉

AI 像一个礼貌且乐于助人的导游。它扫描了 README.txt,无视了数十年的尘埃,自信地生成了一个现代“入门套件”。它给了我一份干净的 build.gradle 文件和一个整洁的 HelloBlobStore.java。它精确地告诉我如何连接数据库。表面上,这看起来像是奇迹。

AI 生成的 build.gradle

code
plugins {
   id 'java'
}

group = 'com.legacycorp.blobstore'
version = '1.1'

sourceCompatibility = '1.8'
targetCompatibility = '1.8'

repositories {
   mavenCentral()
}

dependencies {
   implementation 'org.apache.commons:commons-pool2:2.11.1'
   implementation 'log4j:log4j:1.2.17'
   testImplementation 'junit:junit:4.13.2'
}

test { useJUnit() }

code

结果是一个谎言——更确切地说,是一个结构性的谎言。通过生成现代构建文件,AI只是在斑驳的结构墙外涂上了一层新漆。

首先,它凭空捏造了依赖项,当遗留代码实际依赖于org.apache.commons.pool(v1.x)时,却建议使用commons-pool2(v2.x)。由于这些库的API完全不同,盲目运行AI生成的代码会导致构建失败,出现“类未找到”错误,让我陷入调试“现代”代码的沮丧困境,而这些代码从未打算被现代化。

接下来是结构性的误导。AI自信地假设了标准的Maven布局src/main/java,完全忽视了非标准的Ant结构java/com/legacycorp...。它描述的是自己想看到的现实,而非实际存在的结构。

最后,它掩盖了底层的腐朽。其完美的“Hello World”示例中包含了PooledBlobStoreImpl,却隐瞒了核心实现SimpleBlobStoreImpl甚至不是线程安全的事实,错误处理代码经常吞掉异常,所谓的“单元测试”实际上是需要运行在实时MySQL数据库上的集成测试。

### 教训

AI默认采取乐观态度。当我问“如何运行这个?”时,它假设我能够运行。在修复工程中,乐观主义是致命的。

如果我遵循“游客”路径,会立即开始重构——将List改为ArrayList<>或添加泛型——很可能会破坏我尚未完全理解的隐藏行为。我会在未明确系统当前状态的情况下,对脆弱系统进行盲目的修改。

要真正修复遗留项目,我需要停止扮演游客,转而成为考古学家。

## 第一阶段:分析

在“游客”提示因提供无法存在的现代构建方案而失败后,我意识到这里需要的不是导游,而是建筑检查员。我将思维模式从“如何运行这个?”转变为“为什么失败?”。我重置了与AI的对话上下文,要求它以批判性而非帮助性的方式进行分析。

### 考古学家提示

我设计了一个去除乐观主义的提示。我为AI分配了特定角色:资深遗留系统架构师。我明确禁止它总结README(在遗留项目中README通常是谎言),并要求进行“法医代码审计”。

我正在对这个遗留Java仓库进行技术尽职调查:https://github.com/nikmalykhin/java-blobstore。

请扮演资深遗留系统架构师。你的目标不是告诉我代码“做了什么”,而是评估其结构健康状况和“年龄”。

不要总结README。相反,请进行“法医代码审计”,重点关注以下四个支柱:

  1. 碳测年(时代):
  • 根据语法(如原始类型与泛型、注解)、导入和构建工具(Ant与Maven),估算具体Java版本(如1.4、1.5、6)以及代码可能编写的时间。
  • 引用具体代码行作为“法医证据”。
  1. 架构完整性(结构):
  • 它是否遵循标准的职责分离(传输 vs 协议 vs 逻辑),还是“一团乱麻”?
  • 识别任何“上帝类”(God Classes)——即承担过多职责的类。
code

3.  **数据流与类型(“字符串化”陷阱):**
    * 分析数据传递方式。是否使用了正确的领域对象,还是依赖于“字符串化”映射(Maps)和原始数组?
    * 寻找“泄露的抽象”现象,即协议细节渗透到业务逻辑中。

4.  **“安全性”检查(错误处理与线程):**
    * 查找错误处理中的反模式(吞异常、返回 null)。
    * 分析线程模型。`SimpleBlobStoreImpl` 是否线程安全?

将你的发现以结构化“风险评估报告”的形式呈现,供决定是否重构或重写系统的利益相关者参考。

AI 立即撕下了礼貌的伪装。它没有给我“入门套件”,而是直接递给我一份风险评估报告,给出残酷的结论:“强烈建议彻底重写”。

发现1:对文物进行碳测定

AI 像考古学家一样分析语法,通过证据而非猜测揭示历史地层。首先查看历史记录,发现了一个没有 pom.xml 文件的 build.xml,将项目明确置于 2010 年前的“Ant 时代”。接着,它标记了关键语法特征,发现使用了遗留的 org.apache.commons.pool.ObjectPool(1.x 版本)和原始类型如 Map(而非 Map<String, String>)。这直接得出结论:这段代码是 2005-2008 年间用 Java 1.5 编写的,完全早于现代泛型、try-with-resources 和标准目录结构。

发现2:“转录”陷阱

最令人震惊的发现是,这实际上是伪装成 Java 的 Perl 代码;原始作者只是将一个过程式 Perl 脚本强行套用 Java 语法。这种过程式思维直接体现在 SimpleBlobStoreImpl 中,这是一个巨大的单体“上帝类”,试图处理从底层套接字连接和协议解析到核心业务逻辑的所有内容。此外,代码库极度“字符串化”:没有使用 DeviceFile 等正确领域对象,而是不断传递原始 Map<String, String> 对象,并手动构造原始协议字符串来打开文件。这引入了巨大的操作风险:一个字符串键的拼写错误,如 get("fiel_id"),会导致灾难性运行时崩溃,而非在编译时安全捕获。

发现3:虚假的测试

“考古学家”揭示了所谓的测试覆盖率不过是一种危险的幻觉。整个测试套件严重依赖 LocalFileBlobStoreImpl,这是一个完全重新实现的存储系统,直接写入本地磁盘而非通过网络。虽然这些测试成功证明了这个本地模拟器在隔离环境中完美运行,但这种表面的成功掩盖了一个令人震惊的现实:实际的网络代码、线程不安全的连接池和脆弱的协议解析器——系统中最不稳定的部分——被完全绕过了。

决策:隔离优于修复

这份报告拯救了我免于灾难。如果我盲目听从乐观的“游客”建议,立即重构 SimpleBlobStoreImpl,我会毫无头绪地引入泛型并破坏脆弱的解析逻辑。测试仍然会通过——多亏了那个欺骗性的本地模拟器——但实际生产代码将完全无法运行。

意识到这段代码已成为绝对的负担,过于脆弱以至于不敢触碰,过于晦涩以至于无法信任。我做出了战略决策,暂停所有正在进行的更改。我拒绝修复漏洞、更新依赖项,甚至不重新格式化空白字符。相反,我直接进入完全隔离阶段,将遗留代码封装在隔离的标准化 Docker 环境中,然后再进一步分析。

第二阶段:封装

完成审计并贴上“关键遗留”标签后,我的目标发生了转变。我不再想修改代码,只想运行它。如果能让现有测试通过,我就能获得可验证的基准线。为实现这一目标,我将 AI 角色从架构师切换为高级 DevOps 工程师。

任务:棕色场地修复

在这一阶段的棕色场地修复中,核心任务是建立标准化环境。为引导 AI 并主动防止隐秘的“现代化蔓延”,我制定了严格的几条核心指令。首先,我们必须保持时代特征,即绝对不更新遗留构建工具或 Java 版本——我们严格复现 2008 年的环境。其次,我优先考虑隔离而非现代化,保留原始的 Ant build.xml 文件,并在隔离的 Docker 容器内运行整个流程,以避免污染主机系统。最后,绝对不允许任何代码修改;我拒绝为修复可见性漏洞而给类添加 public 修饰符。如果 2008 年能运行,现在在正确的容器中也必须能运行。

然而,尽管制定了这些严格规则,我的第一反应仍被游客心态秘密污染——这是典型的现代化者傲慢。我发现自己在想:“好吧,我不能修改 Java 代码,但肯定可以将这个古老的 Ant 构建替换为 Gradle 8,对吧?”受这种冲动驱使,我要求 AI 执行快速的“迁移”,直接将原始 Java 1.5 源文件抓取并放入现代 Gradle 8 容器中。

结果是灾难性的崩溃。构建失败并非因为遗留代码本身存在缺陷,而是因为软件环境的基础规则在二十年间发生了根本性变化。遗留代码经常依赖于从完全不同的包中访问包私有类(例如 TestBackend 访问 Backend)。2008 年时,Ant 和 Eclipse 对这些结构违规非常宽容;到 2026 年,Gradle 8 和现代 JDK 已经成为封装规则的严格执行者。

构建失败日志

code
/src/test/java/com/legacycorp/blobstore/test/TestBackend.java:12:
error: Backend is not public in com.legacycorp.blobstore; cannot be accessed from outside package
        Backend backend = new Backend(trackers, true);
        ^

面对这个障碍,AI 的即时建议可以预见:“只要给类添加 public 修饰符就行。”但我拒绝了。这样做将直接违反我零代码修改的核心指令。仅为了取悦现代构建工具而修改生产源代码是危险的滑坡,我不会踏上这条道路。

转折:时间胶囊策略

意识到无法在现代环境中稳定该构件后,我转向了“时间胶囊”策略。若想捕获该系统,必须构建一个严格复现2008年标准的隔离环境。我借助Docker重现代码诞生时的精确环境,寻找包含Java 6和Ant 1.5的旧版镜像。

但立即遭遇了硬件现实的阻碍。唯一可用的Java 6 Docker镜像均针对x86架构(linux/amd64)编译,而我却试图在现代苹果硅芯片(ARM64)笔记本上运行构建。虽然Rosetta或QEMU等模拟层在理论上可行,但它们会向本就脆弱的流程引入危险且不可预测的变量。如果构建失败,如何判断是代码本身的缺陷,还是模拟层无法处理二十年前的二进制文件?

为彻底消除该变量,我改变了环境。放弃笔记本电脑,转而使用搭载现代i9处理器的原生Intel机器。这里的教训显而易见:有时软件考古需要合适的工具。只有当我停止与主机架构对抗,直接在构件的原生环境中推进时,才取得进展。

“湿”测试:扭曲现实

当编译器在Intel平台运行就绪(完成“干”胶囊阶段)后,我面临最后的结构挑战:一个顽固的集成测试TestBlobStore.java。这个“湿”测试是其时代的纯粹产物,充斥着与原始开发者本地机器直接绑定的硬编码假设。具体而言,它寻找一个魔法主机,试图连接qbert.legacycorp.com:7001,并依赖一个位于~/Projects/blobstore/…的魔法文件路径。在标准的现代重构中,我会直接删除这些代码。但由于严格处于隔离模式,修改测试文件被禁止。我无法改变代码以适应现代现实,只能改变现实以适应代码。

解决方案在于通过Docker Compose实现环境模拟。我让AI扮演网络工程师,协助我实现一些基础设施幻象。首先,我们执行了一些网络把戏:启动一个现代BlobStore容器,并使用Docker网络别名欺骗测试运行器,使其相信该容器实际上是失散多年的qbert.legacycorp.com。接下来是文件系统把戏,我配置Docker卷将我们实时的本地源代码目录挂载到容器内的精确路径,该路径与工程师在2005年使用的路径完全一致。

这个环境魔术体现在我的docker-compose.yml文件中:

网络配置部分:

code
services:
    blobstore:
      image: hrchu/blobstore-all-in-one:latest
      networks:
        default:
          aliases:
- qbert.legacycorp.com
builder:
      image: blobstore-legacy-builder
      volumes:
- .:~/Projects/blobstore/java/com/legacycorp/blobstore/
command: ant test

配置了特定网络别名并将本地目录映射到预期的遗留卷路径。

这场精心策划的幻象实现了完全稳定。当我执行docker-compose up时,遗留测试套件启动并完美运行。它查找qbert.legacycorp.com并无缝路由到我的本地Docker容器;它尝试访问工程师的旧硬编码路径,却发现了我们的实时卷挂载。

构建成功了。在不改动一行历史源代码的前提下,我成功恢复了这款二十年前应用程序的全部功能。环境稳定了,代码终于可以验证了,我终于可以开始思考如何将其推向未来。

第三阶段:迁移(解封文物)

当文物被安全地封装在 Docker、Java 6 和 Ant 构成的“时间胶囊”中后,我终于拥有了一个可验证的基准线。现在我有了确凿证据证明代码在其原生环境中是完全可用的,这意味着此后所有失败都将是主动现代化改造工作的直接结果,而不是原有代码腐化的表现。有了这个安全网的保障,我开始进行迁移,将项目推进到十五年后的未来,最终目标是达到 Java 8 和 Gradle。

硬件选择的逻辑

选择 Java 8 并非出于审美考虑,而是由硬件限制驱动的务实选择。我需要在 Apple Silicon(ARM64)上原生运行该项目,但这一目标撞上了双向技术壁垒。一方面,现代 JDK(Java 17+)已完全停止对编译遗留 Java 1.5 源代码的支持,直接拒绝使用旧版 -source 1.5 参数。另一方面,像 Java 6 这样的古老 JDK 无法在 ARM64 架构上原生运行,迫使你陷入有缺陷的模拟层。

因此我转向了 Java 8,这是唯一能同时满足时间线两端需求的特定版本。由于它既是支持 Java 1.5 目标编译的最后一个版本,又是最早能在现代 Mac 硬件上原生安装的版本之一,它成为了我们完美的架构入口点。

“Java 17 陷阱”

在选择工具版本时,我首次遭遇了硬性技术壁垒。我的直觉是使用最新版本 Gradle 8.5,但这个选择立刻撞上了障碍:Gradle 8 需要 Java 17 才能运行其内部守护进程,而正如我们之前提到的,Java 17 完全无法编译遗留的 Java 1.5 源代码。

为了解决这个瓶颈,我决定转向 Gradle 7.6。这是最后一个仍能运行在 Java 8 JVM 上的现代 Gradle 版本,使我能够建立完美的环境兼容链:

Apple Silicon -> Java 8 JVM -> Gradle 7.6 -> Java 1.5 源代码

执行:映射遗留结构

我不仅简单地封装了旧版 build.xml。意识到 Ant 脚本正在主动掩盖底层逻辑后,我配置 Gradle 直接映射到遗留目录结构。为了实现原生编译,我覆盖了现代默认设置,明确指示 Gradle 在 srcDirs = ['java'] 中查找源代码,而不是期待标准的 src/main/java 布局。

接下来我需要处理遗留的测试运行器。由于历史测试以旧式 main() 方法而非现代 JUnit 套件构建,标准的 gradle test 命令无法找到它们。为绕过这个限制,我创建了一个自定义 JavaExec 任务 runLegacyTest 来手动执行这些测试入口点。

将 Gradle 映射到遗留布局

code
java {
      sourceCompatibility = JavaVersion.VERSION_1_5
      targetCompatibility = JavaVersion.VERSION_1_5
  }
  
  sourceSets {
      main {
          java {
              srcDirs = ['java'] 
          }
      }
  }
  
  tasks.register('runLegacyTest', JavaExec) {
      mainClass.set(project.findProperty('mainClass'))
      classpath = sourceSets.main.runtimeClasspath
  }

配置 Gradle 以兼容 Java 1.5,将源代码目录映射到传统布局,并注册一个用于传统测试的执行任务。

"虚假测试"的发现

在构建系统升级到 Gradle 后,runLegacyTest 任务成功执行。但测试运行得异常迅速。当我审查 TestBlobStore.java 的源码以查明原因时,发现了一个经典的遗留反模式:静默吞食异常。代码主动捕获了失败并将其压制,防止其向运行时环境传播:

遗留代码模式

code
public static void main(String[] args) {
      try {
          BlobStore bs = new PooledBlobStoreImpl(...);
          bs.storeFile("test_file", ...);
          System.out.println("Success!");
      } catch (Exception e) {
          System.out.println("Failed: " + e.getMessage());
          e.printStackTrace();
      }
  }

catch 块虽然记录了异常,但将其吞没,导致错误未被察觉。

虽然人工查看控制台输出会轻易识别这是明显的失败,但自动化构建工具却有完全不同的视角。由于异常被捕获并在内部处理而没有继续抛出或终止程序,进程以完美的退出码 0 结束。这些测试完全具有误导性;后端连接可能完全失败,但我们的现代流水线仍会自信地报告绿色通过。

强化基线

为消除这种虚假的安全感,我启动了有意识的强化过程。我指示 AI 重构旧的测试框架,使其明确地将异常一直抛到执行栈顶层。这标志着我对遗留代码库进行的第一个结构性修改,且只有一个目的:迫使我的可验证基线变得完全诚实。我不再用错误压制的毯子包裹操作,而是完全移除了 try-catch 块,强制应用程序在出错时自然崩溃:

强化模式

code
public static void main(String[] args)
throws Exception
{ 
      BlobStore bs = new PooledBlobStoreImpl(...);
      bs.storeFile("test_file", ...); 
  }

没有 catch 块,任何异常都会导致应用程序崩溃。

突然,构建状态变为亮红色。这远非失败,而是巨大的叙事胜利——红色构建意味着我终于看到了系统的真实面貌。我在接下来的一小时里追踪并修复了损坏的连接配置,直到构建流水线最终恢复为绿色。但这一次,它是诚实的绿色。

AI 编译器反馈循环

当测试变得"诚实"且构建转为绿色后,我面临着一座技术债务的高山。构建虽然成功,但编译器却在尖叫:

code
注意:某些输入文件使用了已弃用的 API 或覆盖了弃用的 API。
注意:使用 -Xlint:deprecation 重新编译以获取详细信息。
注意:某些输入文件使用了未检查或不安全的操作。
注意:使用 -Xlint:unchecked 重新编译以获取详细信息。

为了系统性地清理这些问题,我摒弃了通用的重构方式,建立了紧密的、迭代的AI编译器反馈循环。我没有简单地让AI模糊地"修复代码库",而是直接以编译器本身作为最终的驱动者。

首先,我在Gradle构建配置中显式启用了-Xlint:unchecked标志,强制编译器揭示触发违规的具体源代码行。每当构建运行并捕获到特定警告块(例如对原始类型列表的不安全调用时),我会将这些精确的错误日志直接传递给AI,并通过高度针对性的提示指令,要求它仅重构这些特定行代码,使用现代Java泛型来解决警告。

这种高度本地化的策略在消除历史运行时风险方面非常有效。例如,原始代码库使用了原始的Java 1.5风格的泛型集合,编译器根本不知道这些集合中实际包含什么对象,迫使开发者依赖盲目且危险的类型转换:

之前:原始类型风险(Java 1.4风格)

java
public class Backend {
      private List hosts;
      private Map deadHosts;
  
      public void reload(List trackers, boolean connectNow) {
          this.hosts = trackers;
          this.deadHosts = new HashMap();
      }
InetSocketAddress host = (InetSocketAddress) hosts.get(index);
}

编译器无法识别hosts和deadHosts中包含的内容。开发者必须盲目地进行类型转换,如果hosts中包含的是字符串而非地址,高亮显示的代码行将在运行时引发ClassCastException。

通过将这段精确的代码片段及其伴随的警告日志传递给AI,它迅速将架构更新为正确的Java 8类型安全标准,将验证的负担从运行时猜测转移到编译时强制执行:

之后:类型安全标准(Java 8风格)

java
public class Backend {
      private List<InetSocketAddress> hosts;
      private Map<InetSocketAddress, Long> deadHosts;
  
      public void reload(List<InetSocketAddress> trackers, boolean connectNow) {
          this.hosts = trackers;
          this.deadHosts = new HashMap<>();
      }
      
      InetSocketAddress host = hosts.get(index);
  }

编译器保证hosts中只包含InetSocketAddresses。无需类型转换。零运行时风险。

通过在每个文件中保持这种严谨的重复循环,我最终实现了构建成功且完全没有警告的目标。这个历史遗留的代码库不仅功能正常,而且正式达到了标准化要求。

第四阶段:重构

虽然我成功解封了这个历史遗留项目,但其本质仍然杂乱无章。核心代码库仍然困在复杂的、非标准的java/com/...文件夹结构中,其测试套件仍然由原始的独立main()脚本组成。更糟糕的是,源代码本身完全充斥着原始类型——这是Java 1.5时代的遗留产物,迫使开发者盲目地进行类型转换,并持续暴露系统于不可预测的运行时崩溃风险。但随着现代构建链的顺利运行和安全防护网的最终建立,我终于具备了从严格管控转向全面架构改造的条件。

掌握技艺

在动手修改生产代码之前,我必须先修复工作区。我首先进行了一项亟需的清理工作,将源文件从过时的 java/ 根目录移动到行业标准的 src/main/java 结构中。这次结构调整使我能够彻底删除之前自定义的 Gradle 目录变通方案;通过最终接受标准惯例,构建工具直接开箱即用。

在理清项目骨架后,我着手进行了全面的 JUnit 5 迁移,将原始的遗留 main() 脚本(如 TestBackend 和 TestBlobStore)转化为真正的单元测试。在实现过程中,我系统性地将老旧的 System.out.println("Error") 报错方式替换为规范的 Assertions.assertEquals() 断言。这种改进立即带来了令人满意的成果:我获得了细粒度的自动化测试报告,从此彻底摆脱了手动审查无数文本日志的繁琐工作,取而代之的是标准明确的绿色对勾提示。

TestContainers 陷阱

我一度雄心勃勃,考虑用 TestContainers 替代手动的 docker-compose 配置,让测试真正实现自包含,但这次尝试很快以失败告终。迁移过程迅速退化为一团乱麻的“大爆炸”式重构——我发现自己同时试图改造测试运行器、网络拓扑和启动逻辑,同时还需应对 ARM 架构下复杂的 Docker-in-Docker 网络问题。

这种摩擦让我领悟到一个重要的工程教训:动量就是氧气。当我意识到自己正把所有精力都耗费在与工具链的对抗上,而非修复实际代码时,我果断决定终止这次实验。我欣然接受了“外部附属模式”——手动运行 docker-compose up——因为这种方案可靠且有效,我刻意选择脚踏实地的务实方案,而非过度设计的完美主义。

最终收尾:并发与压力测试

我已成功解封该组件并加固其核心,但在能自信宣布项目恢复完成之前,仍有两个最后的遗留问题需要处理。首先是被遗忘的兄弟文件:LocalFileBlobStoreImpl.java。这个遗留的模拟实现迫切需要更新,以适配我们全新基于泛型的 BlobStore 接口。其次,我必须处理验证架构的终极证明:StoreALot.java,这是一个深埋在历史仓库中的多线程负载测试工具。

这些文件至关重要,因为它们掌握着验证并发规则的钥匙。如果 PooledBlobStoreImpl 中的池化逻辑出现哪怕轻微偏差,StoreALot 会立即因 ConcurrentModificationException 崩溃,或陷入无声的竞态条件。为了证明我的现代化改造确实具备线程安全性,我必须彻底重构这些文件,并将它们推向极限测试。

为了执行这个最终的性能工程阶段,我让我的AI助手以高级性能工程师的身份进行协作。我们系统地对老旧的负载测试脚本进行了现代化改造,在保持其可执行性的同时,用泛型和现代日志工具清理了原始语法。我指导AI配置测试运行器,通过PooledBlobStoreImpl指向我们的后端服务,直接访问Docker容器别名qbert.legacycorp.com:7001。最后,我们将原始的手动线程替换为现代的ExecutorService,确保系统能够优雅地处理并行负载而不会因并发异常崩溃。

我们已经完成了核心API的现代化改造。现在必须验证线程安全性。

  • 现代化负载测试:重构StoreALot.java。当前它是一个主程序脚本;保持其可执行性,但用泛型和现代日志工具清理语法。
  • 指向后端服务:确保使用PooledBlobStoreImpl访问Docker容器别名(qbert.legacycorp.com:7001)。
  • 并发验证:使用ExecutorService替代手动线程。在不抛出ConcurrentModificationException的前提下处理并行负载。

这种严谨的协同工作为我提供了所需的实证结果。我启动了压力测试,直接向Docker容器中的BlobStore后端发起100次迭代、10个并发线程的请求。结果清晰可见:应用程序的线程安全架构成功依赖于PooledBlobStoreImpl——通过Apache Commons Pool实现——为每个活跃线程无缝分配独立的后端实例。通过在高强度模拟真实场景下验证这一行为,我确认我们深入的现代化改造(包括泛型、JUnit迁移和结构化集合替换)并未破坏核心历史逻辑。

我终于完成了这项工作。我将一段完全无法编译、无法测试且已损坏的二十年前的代码遗存,成功转化为现代的、线程安全的、完全容器化的Java 8库。

结论:交接

软件修复任务永远不会因为某个遗存物突然变得可用就真正完成;只有当它达到明确且不可动摇的"完成"定义时才算真正结束。对于这个遗留项目而言,这个里程碑不是追求理论上的完美,而是将系统带入一个可验证的特定状态:代码在现代硬件上完全可运行、可测试且可预测。通过达到这个精确的标准,我成功将仓库从一个晦涩的考古谜题转变为更熟悉且可控的标准技术债务。

环境清理

为了确保下一位开发者不必重复我那繁琐的考古挖掘工作,我最后一次切换了我的AI角色,以担任仓库维护负责人。带着这个最终目标,我识别并系统性地清除了所有属于过去的遗留物。首先删除了build.xml,这个几十年来主导仓库规则的旧版Ant脚本。接着,我清空了旧的lib/文件夹——永久丢弃了一堆未经版本控制的硬编码JAR文件——并清理了.classpath.project,这些不过是来自被遗忘IDE配置的废弃遗迹。运行rm build.xml标志着现代化的终极仪式;它正式切断了我们与古老Ant时代的脆弱联系,并永久迫使仓库依赖我的现代Gradle引擎。

项目路线图:README.md

我留下的不只是一个干净的仓库,更是一张地图。与AI协作,我生成了一份全面的README.md文件,完美反映了这个新标准化的现实。文件不再是一个未记录的迷宫,而是明确指出了通向生产力的无摩擦路径,列出了Docker和Java 8+等基础前提条件,并提供了一个极其简单的快速入门指南,只需执行./gradlew build即可构建项目。现在测试整个基础设施也变得同样简单,只需运行docker-compose up -d启动后端依赖,然后执行标准的./gradlew test。这份单一文档彻底改变了项目,将原本令人望而生畏的神秘黑箱转变为可预测的标准Java库,永久将下一位工程师的体验从痛苦的法医调查转变为常规的标准化入职流程。

转变:前后对比

| 功能 | 第0天(档案) | 第N天(产品) | |--------------|----------------|----------------| | 构建系统 | Ant | Gradle 8 | | 编译器 | Java 1.5 | Java 8 | | 环境 | “在我机器上能运行” | Docker | | 测试 | 手动脚本 | JUnit 5 | | 安全性 | 运行时风险 | 编译时安全 | | 确定性 | 被吞没的异常 | 强化测试 | | 入职 | “祝你好运” | README.md |

最后思考:增强的考古学家

这次实验最重要的教训本质上是关于人类主动性的。当我最初依赖无助的“游客提示”——模糊地要求机器“替我修复”——整个尝试崩溃了,因为AI缺乏对环境和过去严格约束的基础理解。只有当我流畅切换思维模式来指导执行时,成功才到来:首先作为考古学家识别真正的架构腐朽,然后作为DevOps工程师设计容器化的时间胶囊,最后作为架构师定义严格的重构政策。

AI并未自行神奇地恢复这个历史系统;相反,我通过将技术作为强大的力量倍增器来恢复它。它处理了那些繁琐的重复性翻译层——从Ant到Gradle的转换、Dockerfile的编写以及系统性地消除50个不同的编译器警告——而我则专注于高层次战略。由于这种积极的合作,代码库不再是一个令人望而生畏的纠缠黑箱。它现在完全可运行、可测试且可预测——完全准备好应对未来十年的挑战,并完美定位以迎接任何未来的重构。

致谢

感谢Matteo Vaccari撰写的关于AI辅助现代化的一系列文章。这些文章为使用AI驱动的重构来改造真正老旧的软件(如这段Java 1.5代码)提供了关键灵感。

非常感谢Martin Fowler在整个写作过程中提供的反馈和指导。他的帮助使本文更加清晰,也更易于阅读。

本文使用了AI技术。我首先使用Gemini来突出实验中的关键时刻,并将我的笔记转化为大纲。随后利用AI根据该大纲撰写各部分内容,我再手动进行审阅、评论和修改。最后使用AI对文章的逻辑流程和语法进行润色。所有实验内容、结论以及最终措辞均由我和GitHub Copilot共同完成审阅和编辑。

重大修订记录

2026年7月16日:发布