The Archaeologist’s Copilot考古学家的副驾驶

Restoration of a 20-year-old Java “Big Ball of Mud” using AI and Docker利用 AI 和 Docker 修复 20 年前的 Java “泥潭代码”

This article explains the approach I used to modernize a Java 1.5 codebase that no longer built reliably on modern machines. My early use of LLMs gave me plausible answers that did not hold up in the codebase. Progress came when I grounded the process in evidence, using AI to support analysis, validation in a stable Docker environment, and gradual refactoring protected by tests. The main takeaway is practical: AI was most useful when constrained by evidence, clear roles, and a step-by-step modernization strategy. 本文阐述了我如何对一个已无法在现代机器上稳定构建的 Java 1.5 代码库进行现代化改造。起初,我使用 LLM(大语言模型)得到的答案看似合理,却无法在代码库中实际运行。直到我将整个过程建立在证据之上,利用 AI 辅助分析、在稳定的 Docker 环境中进行验证,并辅以测试保护下的渐进式重构,进展才得以实现。核心经验很务实:只有当 AI 受到证据、明确角色和分步现代化策略的约束时,它才最有用。

16 July 20262026 年 7 月 16 日


Photo of Nik Malykhin

Nik is an Israeli software developer and Thoughtworker. In addition to his interest in applying AI to software development, he believes that the combination of discipline and human collaboration is becoming even more important in the AI era.

Contents目录


The “Tourist” Trap“游客”陷阱

We all have “that” repository in our organization. The one written in 2005, built with Ant, using Java 1.5. It hasn't been compiled since the Obama administration, and it certainly doesn't run on your new Apple Silicon MacBook. 我们每个人的组织里都有“那个”代码库。它是 2005 年写的,用 Ant 构建,运行在 Java 1.5 上。自奥巴马执政以来,它就没被编译过,当然也无法在你的新款 Apple Silicon MacBook 上运行。

When I inherited this “Brownfield” project, the temptation is to treat Generative AI as a universal translator. I paste the code into an LLM and ask the most natural question in the world: “How do I run this?” 当我接手这个“棕地”项目时,很容易产生一种诱惑,即把生成式 AI 当作万能翻译器。我把代码粘贴到 LLM 中,问了一个最自然不过的问题:“我该如何运行它?”

This is what I call the “Tourist Prompt.” Like a tourist visiting ancient ruins, I am asking for a guided tour and a gift shop souvenir. I want a happy path. 这就是我所说的“游客式提示”。就像参观古遗迹的游客一样,我要求的是导游服务和纪念品。我想要的是一条平坦的通途。

In our experiment, I tried exactly this. I opened a chat with a standard LLM and asked: 在实验中,我正是这样做的。我打开与标准 LLM 的对话框,问道:

“Hi, I need to start working with this library. Can you please act as a Senior Developer and help me get started? Read the repo and give me a high-level summary... and a simple 'Hello World' code example.”“嗨,我需要开始使用这个库。你能扮演高级开发人员并帮我入门吗?阅读一下仓库,给我一个高层级的总结……再给一个简单的‘Hello World’代码示例。”

The Hallucination of Competence

The AI acted like a polite, eager-to-please tour guide. It scanned the README.txt, ignored the decades of dust, and confidently generated a modern “Starter Kit.” It gave me a pristine build.gradle file and a clean HelloBlobStore.java. It told me exactly how to connect to the database. On the surface, it looked like a miracle. AI 表现得像一位礼貌且急于讨好的导游。它扫描了 README.txt,忽略了数十年的灰尘,自信地生成了一个现代化的“入门套件”。它给了我一个完美的 build.gradle 文件和一个整洁的 HelloBlobStore.java。它准确地告诉了我如何连接数据库。从表面上看,这看起来像是个奇迹。

The AI-generated build.gradleAI 生成的 build.gradle

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()
}

The result was a lie — and a structural lie at that. By generating a modern build file, the AI merely painted a fresh coat of paint over a crumbling structural wall.结果是一个谎言——而且是一个结构性的谎言。通过生成一个现代化的构建文件,AI 只是在摇摇欲坠的结构墙上刷了一层新漆。

First, it hallucinated dependencies by suggesting commons-pool2 (v2.x) when the legacy code actually relied on org.apache.commons.pool (v1.x). Because these libraries have completely different APIs, blindly running the AI's code would have crashed the build with “Class Not Found” errors, sending me down a frustrating rabbit hole of debugging “modern” code that was never meant to be modern.首先,它通过建议使用 commons-pool2 (v2.x) 产生了幻觉依赖,而遗留代码实际上依赖于 org.apache.commons.pool (v1.x)。由于这些库的 API 完全不同,盲目运行 AI 生成的代码会导致构建因“找不到类”错误而崩溃,从而让我陷入调试“现代”代码的挫败感中,而这些代码本就不该是现代化的。

Next came the structural gaslighting. The AI confidently assumed a standard Maven layout src/main/java, completely ignoring the reality of a non-standard Ant structure java/com/legacycorp.... It was describing the reality it wanted to see, not the one that actually existed.接下来是结构性的煤气灯效应。AI 自信地假设了一个标准的 Maven 布局 src/main/java,完全忽略了非标准 Ant 结构 java/com/legacycorp.... 的现实。它描述的是它想看到的现实,而不是实际存在的现实。

Finally, it hid the underlying rot. Its pristine “Hello World” example featured PooledBlobStoreImpl, omitting the fact that the core implementation SimpleBlobStoreImpl wasn't even thread-safe, the error-handling code routinely swallowed exceptions, and the so-called “Unit Tests” were actually integration tests that required a live MySQL database to run.最后,它掩盖了底层的腐烂。它那完美的“Hello World”示例中使用了 PooledBlobStoreImpl,却忽略了核心实现 SimpleBlobStoreImpl 甚至不是线程安全的,错误处理代码习惯性地吞掉了异常,而所谓的“单元测试”实际上是需要实时 MySQL 数据库才能运行的集成测试。

The Lesson

AI defaults to optimism. When I ask “How do I run this?”, it assumes I can run it. In a restoration mission, optimism is fatal. AI 默认倾向于乐观。当我问“我该如何运行它?”时,它假设我能运行它。但在修复任务中,乐观是致命的。

If I had followed the Tourist path, I would have started refactoring immediately—changing List to ArrayList<> or adding Generics—likely breaking hidden behaviors I didn't fully understand. I would have been making blind changes to a fragile system without establishing its current state. 如果我遵循“游客”路径,我会立即开始重构——将 List 改为 ArrayList<> 或添加泛型——这很可能会破坏我尚未完全理解的隐藏行为。我会在没有确定当前状态的情况下,对一个脆弱的系统进行盲目的修改。

To truly restore a brownfield project, I need to stop acting like Tourists and start acting like Archaeologists. 要真正修复一个棕地项目,我需要停止表现得像游客,开始像考古学家一样行事。

Phase I: The Analysis 第一阶段:分析

After the “Tourist” prompt failed by offering a modern build that couldn't exist, I realized that what was needed here was not a tour guide, but a construction inspector. I shifted my mental model from “How do I run this?” to “Why did this fail?”. I reset the context with the AI, asking it to be critical rather than helpful.在“游客”提示词因提供了一个不存在的现代构建而失败后,我意识到这里需要的不是导游,而是建筑检查员。我将思维模型从“我该如何运行它?”转变为“为什么它会失败?”。我重置了与 AI 的上下文,要求它进行批判性思考而非提供帮助。

The Archaeologist Prompt

I crafted a prompt designed to strip away the optimism. I assigned the AI a specific persona: Senior Legacy Systems Architect. I explicitly forbade it from summarizing the README (which is often a lie in legacy projects) and ordered a “Forensic Code Audit”.我精心设计了一个旨在消除乐观情绪的提示词。我为 AI 指定了一个特定角色:高级遗留系统架构师。我明确禁止它总结 README(这在遗留项目中往往是谎言),并要求进行“取证式代码审计”。

I am conducting a technical due diligence assessment on this legacy Java
repository: https://github.com/nikmalykhin/java-blobstore.

Act as a Senior Legacy Systems Architect. Your goal is not to tell me what the
code "does," but to evaluate its structural health and "age."

Do not summarize the README. Instead, perform a "Forensic Code Audit" focusing
on these four pillars:

1.  **Carbon Dating (The Era):**
    * Based on syntax (e.g., raw types vs. generics, annotations), imports, and
    build tools (Ant vs. Maven), estimate the specific Java version (e.g., 1.4,
    1.5, 6) and the year this code was likely written.
    * Cite specific lines of code as "forensic evidence."

2.  **Architectural Integrity (The Structure):**
    * Does it follow standard separation of concerns (Transport vs. Protocol vs.
    Logic), or is it a "Big Ball of Mud"?
    * Identify any "God Classes" that are doing too much.

3.  **Data Flow & Typing (The "Stringly" Trap):**
    * Analyze how data is passed. Is it using proper Domain Objects, or is it
    relying on "Stringly-typed" Maps and raw arrays?
    * Look for "Leaky Abstractions" where protocol details leak into business logic.

4.  **The "Safety" Check (Error Handling & Threading):**
    * Look for anti-patterns in error handling (swallowed exceptions, returning null).
    * Analyze the threading model. Is `SimpleBlobStoreImpl` thread-safe?

Output your findings as a structured "Risk Assessment Report" for a stakeholder
deciding whether to refactor or rewrite.

The AI dropped the polite facade immediately. Instead of a “Starter Kit,” it handed me a Risk Assessment Report with a brutal verdict: “critical rewrite recommended“.AI 立即卸下了礼貌的面具。它没有给我“入门套件”,而是递给我一份风险评估报告,并给出了一个残酷的结论:“建议进行彻底重写”。

Finding 1: Carbon Dating the Artifact

The AI analyzed the syntax like an archaeologist uncovering historical strata, citing evidence rather than guessing. Looking first at the historical record, it found a build.xml file without any sign of a pom.xml, placing the project squarely in the pre-2010 “Ant Era.” Next, it flagged key syntax markers, spotting the legacy use of org.apache.commons.pool.ObjectPool (Version 1.x) alongside raw types like Map instead of Map<String, String>. This led to an unmistakable verdict: this was Java 1.5 code written during the transition era of 2005–2008, completely predating modern generics, try-with-resources, and standard directory layouts.AI 像考古学家揭示历史地层一样分析语法,引用证据而非猜测。首先查看历史记录,它发现了一个 build.xml 文件,没有任何 pom.xml 的迹象,这使该项目完全属于 2010 年之前的“Ant 时代”。接下来,它标记了关键语法标记,发现了遗留代码中 org.apache.commons.pool.ObjectPool (1.x 版本) 的使用,以及原始类型(如 Map 而非 Map<String, String>)。这得出了一个明确的结论:这是 2005-2008 年过渡时期编写的 Java 1.5 代码,完全早于现代泛型、try-with-resources 和标准目录布局。

Finding 2: The “Transliteration” Trap

The most damning insight was that this was actually Perl code masquerading as Java; the original author had simply taken a procedural Perl script and forced it into Java syntax. This procedural mindset manifested directly in SimpleBlobStoreImpl, a massive monolithic god class that tried to handle everything from low-level socket connections and protocol parsing to core business logic. Furthermore, the codebase was aggressively “stringly-typed”. Instead of utilizing proper domain objects like Device or File, the code constantly passed around raw Map<String, String> objects and manually constructed raw protocol strings just to open a file. This introduced an immense operational risk: a single typo in a string key, such as get(“fiel_id”), would trigger a catastrophic runtime crash instead of being caught safely at compile time.最令人震惊的洞察是,这实际上是伪装成 Java 的 Perl 代码;原始作者只是将过程化的 Perl 脚本强行塞进了 Java 语法中。这种过程化思维直接体现在 SimpleBlobStoreImpl 中,这是一个庞大的单体上帝类,试图处理从底层 socket 连接和协议解析到核心业务逻辑的所有事情。此外,代码库严重“字符串化”。代码没有使用 Device 或 File 等适当的领域对象,而是不断传递原始的 Map<String, String> 对象,并手动构建原始协议字符串来打开文件。这引入了巨大的操作风险:字符串键中的一个拼写错误(例如 get(“fiel_id”))会导致灾难性的运行时崩溃,而不是在编译时安全捕获。

Finding 3: Lying Tests

The Archaeologist revealed that the perceived test coverage was nothing more than a dangerous illusion. The entire test suite relied heavily on LocalFileBlobStoreImpl, a complete re-implementation of the storage system that wrote directly to the local disk instead of traversing the network. While these tests successfully proved that this local mock worked flawlessly in isolation, that superficial success masked an alarming reality: the actual networking code, the thread-unsafe pooling, and the fragile protocol parser—the absolute most volatile parts of the system—were being completely bypassed.考古学家揭示,所谓的测试覆盖率不过是一种危险的幻觉。整个测试套件严重依赖 LocalFileBlobStoreImpl,这是存储系统的完全重实现,直接写入本地磁盘而不是遍历网络。虽然这些测试成功证明了该本地模拟在隔离状态下运行完美,但这种表面的成功掩盖了一个令人担忧的现实:实际的网络代码、线程不安全的池化和脆弱的协议解析器——系统中最不稳定的部分——完全被绕过了。

The Decision: Containment over Repair

The report saved me from disaster. Had I followed the optimistic “Tourist” advice to refactor SimpleBlobStoreImpl immediately, I would have blindly introduced generics and broken the fragile parsing logic. The tests would have still passed—thanks to that deceptive local mock —but the actual production code would have been completely non-functional.这份报告让我免于灾难。如果我遵循“游客”的乐观建议立即重构 SimpleBlobStoreImpl,我会盲目地引入泛型并破坏脆弱的解析逻辑。测试仍然会通过——多亏了那个欺骗性的本地模拟——但实际的生产代码将完全无法运行。

Realizing the code was an absolute liability, too fragile to touch and too opaque to trust. I made the strategic decision to halt all active changes. I refused to fix the bugs, update the dependencies, or even reformat the whitespace. Instead, I transitioned directly into a phase of complete containment, wrapping the legacy code inside an isolated, standardized Docker environment before trying to analyze it any further.意识到代码是一个绝对的负债,太脆弱以至于不敢触碰,太不透明以至于无法信任。我做出了停止所有主动修改的战略决定。我拒绝修复 bug、更新依赖项,甚至拒绝重新格式化空白。相反,我直接进入了完全隔离阶段,在尝试进一步分析之前,将遗留代码包装在一个隔离的、标准化的 Docker 环境中。

Phase II: The Wrap 第二阶段:包装

With the audit complete and the “Critical Legacy” label applied, my goal shifted. I didn't want to change the code; I just wanted to run it. If I could get the existing tests to pass, I would have a verifiable baseline. To achieve this, I switched AI persona from Architect to Senior DevOps Engineer.审计完成并贴上“关键遗留代码”标签后,我的目标发生了转变。我不想修改代码;我只想运行它。如果我能让现有的测试通过,我就能拥有一个可验证的基准。为了实现这一点,我将 AI 的角色从架构师切换为高级 DevOps 工程师。

The Mission: Brownfield Restoration

For this phase of brownfield restoration, the core mission was to establish a standardized environment. To guide the AI and actively prevent insidious “modernization creep”, I established a strict set of prime directives. First, we had to preserve the era, meaning absolutely no updates to the legacy build tools or the Java version—we were strictly mimicking the year 2008. Second, I prioritized containment over modernization, keeping the original Ant build.xml file and running the entire process inside an isolated Docker container to avoid polluting my host machine. Finally, there were to be absolutely no code changes; I refused to slap public modifiers onto classes just to fix visibility bugs. If it worked in 2008, it had to work now inside the right container.对于棕地修复的这一阶段,核心任务是建立一个标准化的环境。为了引导 AI 并积极防止隐蔽的“现代化蔓延”,我制定了一套严格的准则。首先,我们必须保留时代特征,这意味着绝对不更新遗留构建工具或 Java 版本——我们严格模拟 2008 年。其次,我将隔离置于现代化之上,保留原始的 Ant build.xml 文件,并在隔离的 Docker 容器中运行整个过程,以避免污染宿主机。最后,绝对不允许修改代码;我拒绝为了修复可见性 bug 而给类添加 public 修饰符。如果它在 2008 年能运行,那么在正确的容器里它现在也必须能运行。

Yet, despite these strict rules, my first instinct was still secretly tainted by the tourist mindset—a classic case of modernizer's hubris. I caught myself thinking, “Okay, I can't change the Java code, but surely I can swap out this ancient Ant build for Gradle 8, right?” Acting on that impulse, I asked the AI to perform a swift “lift and shift,” grabbing the raw Java 1.5 source files and dropping them directly into a modern Gradle 8 container.然而,尽管有这些严格的规则,我的第一直觉仍然暗中受到游客心态的影响——这是现代化者傲慢的典型案例。我发现自己在想:“好吧,我不能修改 Java 代码,但我肯定能把这个古老的 Ant 构建换成 Gradle 8,对吧?”出于这种冲动,我要求 AI 执行一次快速的“直接迁移”,将原始的 Java 1.5 源代码文件直接丢进一个现代的 Gradle 8 容器中。

The result was a spectacular crash. The build failed not because the legacy code itself was buggy, but because the foundational rules of the software environment had radically shifted over two decades. The legacy code routinely relied on accessing package-private classes from entirely separate packages (such as TestBackend reaching into Backend). Back in 2008, Ant and Eclipse were incredibly permissive about these structural violations; by 2026, Gradle 8 and modern JDKs had become strict, unyielding enforcers of encapsulation.结果是一场壮观的崩溃。构建失败不是因为遗留代码本身有 bug,而是因为软件环境的基础规则在过去二十年里发生了根本性的转变。遗留代码习惯性地依赖于访问完全独立包中的包私有类(例如 TestBackend 访问 Backend)。在 2008 年,Ant 和 Eclipse 对这些结构违规行为非常宽容;到了 2026 年,Gradle 8 和现代 JDK 已成为封装的严格执行者。

The Build Failure Log构建失败日志

/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);
        ^

Faced with this roadblock, the AI's immediate suggestion was predictable: “Just add public to the class.” But I refused. Doing so would directly violate one of my prime directive of making zero code changes. Modifying production source code solely to appease a modern build tool is a slippery slope, and I wasn't going to step onto it.面对这个路障,AI 的建议是可以预见的:“给类加上 public 就行了。”但我拒绝了。这样做将直接违反我不修改任何代码的准则。仅仅为了迎合现代构建工具而修改生产源代码是一条滑坡,我不会踏上去。

The Pivot: The “Time Capsule” strategy

Realizing that I couldn't stabilize the artifact in a modern environment, I pivoted to a “Time Capsule” strategy. If I wanted to capture this system, I had to build a containment zone that strictly mirrored the standards of 2008. I turned to Docker to recreate the exact environment the code was born in, searching for an old image that bundled Java 6 and Ant 1.5 together.意识到无法在现代环境中稳定该工件,我转向了“时间胶囊”策略。如果我想捕获这个系统,我就必须建立一个严格镜像 2008 年标准的隔离区。我求助于 Docker 来重建代码诞生的精确环境,寻找一个同时捆绑 Java 6 和 Ant 1.5 的旧镜像。

But I hit an immediate hardware reality check. The only available Java 6 Docker images were compiled for x86 (linux/amd64), while I was attempting to run the build on a modern Apple Silicon (ARM64) laptop. While emulation layers like Rosetta or QEMU are theoretically possible, they introduce a dangerous, unpredictable variable into an already fragile process. If the build fails, how do you know whether it's an inherent code defect or just the emulation layer choking on twenty-year-old binaries?但我立即遇到了硬件现实的阻碍。唯一可用的 Java 6 Docker 镜像都是为 x86 (linux/amd64) 编译的,而我正试图在现代 Apple Silicon (ARM64) 笔记本电脑上运行构建。虽然 Rosetta 或 QEMU 等模拟层在理论上是可能的,但它们在一个本已脆弱的过程中引入了一个危险、不可预测的变量。如果构建失败,你怎么知道是固有的代码缺陷,还是模拟层在处理二十年前的二进制文件时卡住了?

To eliminate that variable entirely, I changed my environment. I abandoned the laptop and switched to a native Intel machine powered by a modern i9 processor. The lesson here was clear: sometimes software archaeology requires the right shovel. I only made progress when I stopped fighting the host architecture and moved directly onto the native ground of the artifact.为了彻底消除该变量,我更换了环境。我放弃了笔记本电脑,转而使用由现代 i9 处理器驱动的原生 Intel 机器。这里的经验很明确:有时软件考古学需要合适的铲子。只有当我停止与宿主架构对抗,直接进入工件的原生环境时,我才取得了进展。

The “Wet” Test: Bending Reality

Once I had the compiler working on Intel—completing the “dry” capsule—I faced the final structural challenge: a stubborn integration test named TestBlobStore.java. This “wet” test was a pure artifact of its time, littered with hardcoded assumptions tied directly to the original developer's local machine. Specifically, it looked for a magic host, trying to connect to qbert.legacycorp.com:7001, and relied on a magic file path located at ~/Projects/blobstore/…. In a standard modern refactor, I would have simply deleted these lines. But because I was strictly in containment mode, touching the test file was off the table. Instead of changing the code to fit modern reality, I had to change reality to fit the code.一旦我在 Intel 上让编译器正常工作——完成了“干”胶囊——我就面临最终的结构挑战:一个名为 TestBlobStore.java 的顽固集成测试。这个“湿”测试是其时代的纯粹产物,充斥着直接与原始开发者本地机器绑定的硬编码假设。具体来说,它寻找一个魔法主机,试图连接到 qbert.legacycorp.com:7001,并依赖于位于 ~/Projects/blobstore/… 的魔法文件路径。在标准的现代重构中,我会简单地删除这些行。但因为我处于严格的隔离模式,触碰测试文件是不可能的。我没有修改代码来适应现代现实,而是必须改变现实来适应代码。

The solution lay in environment emulation via Docker Compose. I prompted the AI to act as a network engineer to help me pull off some infrastructure illusions. First, we executed some network trickery: I spun up a modern BlobStore container and used a Docker network alias to trick the test runner into believing this container was actually the long-lost qbert.legacycorp.com. Next came the filesystem trickery, where I configured Docker volumes to mount our live, local source code directory inside the container at the exact, identical path engineer had used back in 2005.解决方案在于通过 Docker Compose 进行环境模拟。我提示 AI 扮演网络工程师,帮我实现一些基础设施幻觉。首先,我们执行了一些网络欺骗:我启动了一个现代化的 BlobStore 容器,并使用 Docker 网络别名让测试运行器相信该容器实际上就是失踪已久的 qbert.legacycorp.com。接下来是文件系统欺骗,我配置 Docker 卷将我们实时的本地源代码目录挂载到容器中,路径与 2005 年工程师使用的路径完全相同。

This environment trick materialized in my docker-compose.yml file:这种环境欺骗体现在我的 docker-compose.yml 文件中:

The network configuration. 网络配置。

  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

Configures the specific network alias and maps the local directory to the expected legacy volume path.配置特定的网络别名,并将本地目录映射到预期的遗留卷路径。

This orchestrated illusion brought about complete stabilization. When I executed docker-compose up, the legacy test suite fired up and ran flawlessly. It looked up qbert.legacycorp.com and seamlessly routed straight to my local Docker container; it reached out for engineer's old hardcoded path and found our live volume mount instead.这种精心策划的幻觉带来了彻底的稳定。当我执行 docker-compose up 时,遗留测试套件启动并完美运行。它查找 qbert.legacycorp.com 并无缝路由到我的本地 Docker 容器;它寻找工程师旧有的硬编码路径,却找到了我们实时的卷挂载。

The build succeeded. Without changing a single byte of historical source code, I had successfully restored full functionality to a twenty-year-old application. The environment was stable, the code was finally verifiable, and I could at long last think about moving it into the future.构建成功了。在没有更改历史源代码的一个字节的情况下,我成功地恢复了一个二十年前应用程序的全部功能。环境稳定了,代码终于可验证了,我终于可以考虑将其带入未来。

Phase III: The Lift (Unwrapping the Artifact)第三阶段:提升(拆解工件)

With the artifact safely stabilized inside the “Time Capsule” of Docker, Java 6, and Ant, I finally possessed a verifiable baseline. I now had concrete proof that the code was fully functional in its native environment, meaning any failures from this point forward would be the direct result of our active modernization efforts, not pre-existing rot. With this safety net firmly established, I began the transition, launching the project fifteen years into the future with the ultimate goal of reaching Java 8 and Gradle.随着工件安全地稳定在 Docker、Java 6 和 Ant 的“时间胶囊”中,我终于拥有了一个可验证的基准。我现在有了确凿的证据证明该代码在其原生环境中功能完备,这意味着从此时起的任何失败都将是我们主动现代化努力的直接结果,而不是预先存在的腐烂。有了这个安全网,我开始了过渡,将项目向未来推进了十五年,最终目标是达到 Java 8 和 Gradle。

The Hardware Rationale

The choice of Java 8 was not aesthetic; it was a pragmatic necessity driven by my hardware constraints. I needed to run the project natively on Apple Silicon (ARM64), but that goal crashed into a double-ended technical wall. On one side of the timeline, modern JDKs (Java 17+) have dropped support for compiling legacy Java 1.5 source code entirely, rejecting the old -source 1.5 flag. On the other side, ancient JDKs like Java 6 refuse to run natively on ARM64 architecture, trapping you in buggy emulation layers.选择 Java 8 并非出于美观,而是由我的硬件限制所驱动的务实需求。我需要在 Apple Silicon (ARM64) 上原生运行该项目,但该目标撞上了一堵双向的技术墙。在时间线的一端,现代 JDK (Java 17+) 已完全放弃对编译遗留 Java 1.5 源代码的支持,拒绝使用旧的 -source 1.5 标志。在另一端,像 Java 6 这样古老的 JDK 拒绝在 ARM64 架构上原生运行,将你困在 buggy 的模拟层中。

So I turned to Java 8, the single, specific version capable of satisfying both ends of the timeline. Because it stands as the absolute last version to support the compilation of Java 1.5 targets and one of the earliest versions that can be installed natively on modern Mac hardware, it became our perfect architectural entry point.所以我转向了 Java 8,这是唯一能够满足时间线两端的特定版本。因为它作为支持编译 Java 1.5 目标的最后一个版本,同时也是可以在现代 Mac 硬件上原生安装的最早版本之一,它成为了我们完美的架构入口点。

The “Java 17 Trap”

I hit the first hard technical wall when choosing the tool version. My instinct was to use the latest release, Gradle 8.5, but this choice immediately crashed into a wall: Gradle 8 requires Java 17 just to run its internal daemon, and as we already noted, Java 17 is incapable of compiling legacy Java 1.5 source code.在选择工具版本时,我撞上了第一堵硬技术墙。我的直觉是使用最新版本 Gradle 8.5,但这个选择立即撞上了一堵墙:Gradle 8 仅运行其内部守护进程就需要 Java 17,而正如我们已经指出的,Java 17 无法编译遗留的 Java 1.5 源代码。

To resolve this bottleneck, I settled on a pivot to Gradle 7.6. This stands as the absolute last modern-ish Gradle version that can still execute on a Java 8 JVM, allowing me to establish a perfect chain of environmental compatibility:为了解决这个瓶颈,我决定转向 Gradle 7.6。这是最后一个仍然可以在 Java 8 JVM 上执行的现代 Gradle 版本,使我能够建立一个完美的环境兼容链:

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

The Execution: Mapping the Legacy Structure

I didn't just wrap the old build.xml. Realizing the Ant script was actively obscuring the underlying logic, I configured Gradle to map directly to the legacy directory structure. To achieve native compilation, I overrode the modern defaults and explicitly instructed Gradle to look for the source code in srcDirs = ['java'] instead of expecting the standard src/main/java layout.我不仅仅是包装了旧的 build.xml。意识到 Ant 脚本正在积极掩盖底层逻辑,我配置 Gradle 直接映射到遗留目录结构。为了实现原生编译,我覆盖了现代默认设置,并明确指示 Gradle 在 srcDirs = ['java'] 中查找源代码,而不是期望标准的 src/main/java 布局。

Next, I had to tackle the legacy test runner. Because the historical tests were structured as old-school main() methods rather than a modern JUnit suite, the standard out-of-the-box gradle test command couldn't find them. To bypass this limitation, I wired up a custom JavaExec task named runLegacyTest to execute those test entry points manually.接下来,我必须处理遗留测试运行器。由于历史测试被构建为旧式的 main() 方法而不是现代 JUnit 套件,标准的开箱即用 gradle test 命令无法找到它们。为了绕过这个限制,我编写了一个名为 runLegacyTest 的自定义 JavaExec 任务来手动执行这些测试入口点。

Mapping Gradle to the Legacy Layout 将 Gradle 映射到遗留布局

  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
  }

Configures Gradle for Java 1.5 compatibility, maps the source directories to the legacy layout, and registers a execution task for legacy tests.配置 Gradle 以实现 Java 1.5 兼容性,将源代码目录映射到遗留布局,并注册遗留测试的执行任务。

The “Lying Tests” Discovery

With the build modernized to Gradle, the runLegacyTest task executed successfully. But the tests ran suspiciously fast. When I audited the source of TestBlobStore.java to find out why, I discovered a classic legacy anti-pattern: the silent swallow. The code was actively capturing failures and smothering them before they could bubble up to the runtime environment:随着构建现代化为 Gradle,runLegacyTest 任务成功执行。但测试运行得异常快。当我审计 TestBlobStore.java 的源代码以找出原因时,我发现了一个经典的遗留反模式:静默吞噬。代码在主动捕获失败,并在它们冒泡到运行时环境之前将其扼杀:

Legacy Code Pattern 遗留代码模式

  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();
      }
  }

The catch block logs the exception but swallows it, so the error isn't noticed.catch 块记录了异常但将其吞掉,因此错误未被察觉。

While a human reading the console outputs would easily recognize this as a blatant failure, an automated build tool sees it very differently. Because the exception is caught and handled internally without throwing it further or exiting the program, the process finishes with a perfect exit code 0. These tests were completely misleading; the backend connection could fail entirely, yet our modern pipeline would still confidently report a green pass.虽然阅读控制台输出的人很容易将其识别为明显的失败,但自动构建工具的看法却截然不同。由于异常在内部被捕获和处理,而没有进一步抛出或退出程序,该过程以完美的退出代码 0 结束。这些测试完全具有误导性;后端连接可能完全失败,但我们的现代流水线仍会自信地报告绿色通过。

Hardening the Baseline

To strip away this false security, I initiated a process of deliberate hardening. I instructed the AI to refactor the old test harness so that it would explicitly throw exceptions all the way up the execution stack. This marked my very first structural change to the legacy codebase, and it was done with a singular purpose: to force my verifiable baseline to become completely honest. Instead of wrapping the operations in an error-smothering blanket, I stripped out the try-catch block entirely and forced the application to crash naturally if something went wrong:为了消除这种虚假的安全感,我启动了一个有意的加固过程。我指示 AI 重构旧的测试工具,使其显式地将异常一直抛出到执行堆栈的顶端。这标志着我对遗留代码库的第一次结构性修改,其目的只有一个:迫使我可验证的基准变得完全诚实。我没有用掩盖错误的毯子包裹操作,而是完全去除了 try-catch 块,并迫使应用程序在出错时自然崩溃:

Hardened Pattern 加固模式

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

There's no catch block, so any exception crashes the application.没有 catch 块,因此任何异常都会导致应用程序崩溃。

Suddenly, the build turned bright red. Far from a defeat, this was a massive narrative victory—a red build meant I was finally looking at the unvarnished reality of the system. I spent the next hour tracing down and repairing the broken connection configurations until the build pipeline finally flipped back to green. But this time, it was an honest green. 突然,构建变成了亮红色。这绝非失败,而是一个巨大的叙事胜利——红色的构建意味着我终于看到了系统未加修饰的现实。我花了一个小时追踪并修复了损坏的连接配置,直到构建流水线最终翻转回绿色。但这一次,这是一个诚实的绿色。

The AI-Compiler Feedback Loop

Once the tests were “honest” and the build turned Green, I was faced with a mountain of technical debt. The build was successful, but the compiler was screaming:一旦测试变得“诚实”且构建变为绿色,我就面临着堆积如山的技术债务。构建成功了,但编译器在尖叫:

Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
Note: Some input files use unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

To systematically clean this up, I moved away from general refactoring and established a tight, iterative AI-compiler feedback loop. I didn't just ask the AI to vaguely “fix the codebase”; instead, I used the compiler itself as the ultimate driver.为了系统地清理这些问题,我放弃了通用重构,建立了紧密的、迭代的 AI-编译器反馈循环。我没有只是模糊地要求 AI “修复代码库”;相反,我将编译器本身作为最终的驱动力。

First, I explicitly enabled the -Xlint:unchecked flag inside the Gradle build configuration to force the compiler to reveal the exact source lines triggering the violations. Whenever the build ran and captured a specific warning block—such as an unsafe call to a raw type list—I fed those exact error logs directly to the AI with a highly targeted prompt, instructing it to refactor only those specific lines to resolve the warnings using modern Java generics.首先,我在 Gradle 构建配置中显式启用了 -Xlint:unchecked 标志,以强制编译器揭示触发违规的精确源代码行。每当构建运行并捕获到特定的警告块(例如对原始类型列表的不安全调用)时,我就会将这些精确的错误日志直接喂给 AI,并附带一个高度针对性的提示词,指示它仅重构那些特定的行,以使用现代 Java 泛型解决警告。

This highly localized strategy was incredibly effective at neutralizing historical runtime risks. For example, the original codebase used primitive, Java 1.5-style raw collections where the compiler had no idea what objects actually lived inside them, forcing me to rely on blind, dangerous type casting:这种高度局部化的策略在消除历史运行时风险方面非常有效。例如,原始代码库使用了原始的、Java 1.5 风格的原始集合,编译器根本不知道里面到底住着什么对象,迫使我依赖盲目的、危险的类型转换:

BEFORE: The “Raw Type” Risk (Java 1.4 Style) 之前: “原始类型”风险 (Java 1.4 风格)

  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); 
  }

The compiler has no idea what is inside hosts and deadHosts. Developers must cast objects blindly, if hosts contains a String instead of an address, the highlighted line will cause a ClassCastException at runtime.编译器不知道 hosts 和 deadHosts 里面是什么。开发者必须盲目转换对象,如果 hosts 包含 String 而不是地址,高亮行将在运行时导致 ClassCastException。

By passing this exact snippet and its accompanying warning log to the AI, it swiftly updated the architecture to the proper Java 8 type-safe standard, transferring the burden of validation from runtime guesswork to compile-time enforcement:通过将此精确片段及其附带的警告日志传递给 AI,它迅速将架构更新为适当的 Java 8 类型安全标准,将验证负担从运行时猜测转移到编译时强制执行:

AFTER: The “Type Safe” Standard (Java 8 Style) 之后: “类型安全”标准 (Java 8 风格)

  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);
  }

The compiler guarantees that hosts only contains InetSocketAddresses. No casting required. Zero runtime risk.编译器保证 hosts 只包含 InetSocketAddresses。无需转换。零运行时风险。

By maintaining this disciplined, repetitive cycle across every file, I eventually crossed the finish line with a successful build and absolutely zero warnings. The historic artifact wasn't just functional; it was officially standardized.通过在每个文件中保持这种纪律严明的、重复的循环,我最终以成功的构建和绝对零警告跨过了终点线。历史工件不仅功能完备,而且已正式标准化。

Phase IV: The Refactor 第四阶段:重构

Although I had successfully unwrapped the historical artifact, it remained fundamentally disorganized. The core codebase was still locked in a convoluted, non-standard java/com/... folder structure, and its test suite still consisted of primitive, standalone main() scripts. To make matters worse, the source code itself was completely riddled with raw types—a legacy artifact of the Java 1.5 era that forced developers to cast objects blindly and constantly exposed the system to unpredictable runtime crashes. Yet, with a modern build chain now humming and a hardened safety net finally secured beneath me, I was at long last equipped to transition from strict containment into a full-scale architectural renovation.尽管我已经成功拆解了历史工件,但它在根本上仍然是混乱的。核心代码库仍然锁定在复杂的、非标准的 java/com/... 文件夹结构中,其测试套件仍然由原始的、独立的 main() 脚本组成。更糟糕的是,源代码本身完全充斥着原始类型——Java 1.5 时代的遗留产物,迫使开发者盲目转换对象,并不断将系统暴露于不可预测的运行时崩溃中。然而,随着现代构建链现在平稳运行,且加固的安全网终于稳固地固定在我身下,我终于有能力从严格的隔离过渡到全面的架构翻新。

Mastering the Craft

Before attacking the production code, I had to fix the workbench. I started with a much-needed phase of sanitization, moving the source files out of their archaic java/ root folder and into the industry-standard src/main/java layout. Making this shift allowed me to delete my previous custom Gradle directory workarounds entirely; by finally bowing to standard conventions, the build tool simply worked out of the box.在攻击生产代码之前,我必须修复工作台。我从急需的清理阶段开始,将源文件从其古老的 java/ 根文件夹移出,放入行业标准的 src/main/java 布局中。这一转变使我能够完全删除之前自定义的 Gradle 目录变通方法;通过最终屈服于标准约定,构建工具直接开箱即用了。

With the project's skeleton straightened out, I tackled a comprehensive JUnit 5 migration to convert the primitive legacy main() scripts—such as TestBackend and TestBlobStore—into genuine unit tests. Throughout the implementation, I systematically swapped out the old, crude System.out.println(“Error”) traps for proper Assertions.assertEquals() statements. This immediately paid off with a deeply satisfying result: I gained granular, automated test reporting, permanently freeing me from having to manually audit endless text logs just to check if a test had passed in favor of receiving a standard, unambiguous green checkmark.随着项目骨架的理顺,我进行了全面的 JUnit 5 迁移,将原始的遗留 main() 脚本(如 TestBackend 和 TestBlobStore)转换为真正的单元测试。在整个实现过程中,我系统地将旧的、粗糙的 System.out.println(“Error”) 陷阱替换为适当的 Assertions.assertEquals() 语句。这立即带来了令人深感满足的结果:我获得了细粒度的、自动化的测试报告,永久地将我从手动审计无休止的文本日志中解放出来,转而接收标准的、明确的绿色复选标记。

The TestContainers Trap

I got ambitious and considered replacing the manual docker-compose setup with TestContainers to make the tests truly self-contained, but the attempt quickly collapsed. The migration rapidly degenerated into a messy “Big Bang” refactor—I found myself trying to overhaul the test runner, the network topology, and the startup logic all at once, while simultaneously wrestling with complex Docker-in-Docker networking issues on ARM architecture.我变得野心勃勃,考虑用 TestContainers 替换手动 docker-compose 设置,使测试真正自包含,但该尝试很快崩溃了。迁移迅速退化为混乱的“大爆炸”式重构——我发现自己试图同时彻底检查测试运行器、网络拓扑和启动逻辑,同时还要与 ARM 架构上复杂的 Docker-in-Docker 网络问题作斗争。

This friction taught me a vital engineering lesson: momentum is oxygen. The moment I realized I was spending all my energy fighting the tooling rather than recovering the actual code, I made the conscious decision to abort the experiment. I gladly accepted the “External Sidecar” pattern—running docker-compose up manually—because it was reliable and it worked, deliberately choosing ground-level pragmatism over over-engineered perfection.这种摩擦教会了我一个至关重要的工程经验:动力就是氧气。当我意识到我把所有精力都花在与工具对抗而不是恢复实际代码时,我做出了中止实验的清醒决定。我欣然接受了“外部边车”模式——手动运行 docker-compose up——因为它可靠且有效,故意选择了地面级的务实而非过度设计的完美。

The Final Sweep: Concurrency & Stress Testing

I had successfully unwrapped the artifact and hardened its core, but two final loose ends remained before I could confidently declare the project's restoration complete. First, there was a forgotten sibling: LocalFileBlobStoreImpl.java. This legacy mock implementation desperately needed to be updated to implement our brand-new, generic-based BlobStore interface. Second, I had to address the ultimate proof of our architecture: StoreALot.java, a multi-threaded load-testing tool buried deep within the historical repository.我已经成功拆解了工件并加固了其核心,但在我能自信地宣布项目修复完成之前,还有两个最后的松散环节。首先,有一个被遗忘的兄弟:LocalFileBlobStoreImpl.java。这个遗留的模拟实现迫切需要更新,以实现我们全新的、基于泛型的 BlobStore 接口。其次,我必须解决我们架构的终极证明:StoreALot.java,这是一个埋藏在历史仓库深处的多线程负载测试工具。

These files mattered immensely because they held the keys to verifying our concurrency rules. If the pooling logic inside PooledBlobStoreImpl was even slightly misaligned, StoreALot would immediately crash with a ConcurrentModificationException or succumb to silent race conditions. To prove that my modernizations were actually thread-safe, I needed to overhaul these files and push them to their absolute limits.这些文件非常重要,因为它们掌握着验证我们并发规则的钥匙。如果 PooledBlobStoreImpl 内部的池化逻辑有哪怕一点点偏差,StoreALot 就会立即因 ConcurrentModificationException 而崩溃,或屈服于静默的竞态条件。为了证明我的现代化是线程安全的,我需要彻底检查这些文件并将它们推向极限。

To execute this final performance engineering phase, I prompted my AI copilot to act as a senior performance engineer. Together, we systematically modernized the old load-testing script, cleaning up its raw syntax with generics and modern loggers while ensuring it remained executable. I instructed the AI to configure the test runner to target our backend using PooledBlobStoreImpl to hit the Docker container alias at qbert.legacycorp.com:7001. Finally, we swapped out the primitive, manual threads for a modern ExecutorService to guarantee the system could elegantly handle a parallel load without buckling under concurrent exceptions.为了执行这个最后的性能工程阶段,我提示我的 AI 副驾驶扮演高级性能工程师。我们一起系统地现代化了旧的负载测试脚本,用泛型和现代记录器清理了其原始语法,同时确保它保持可执行状态。我指示 AI 配置测试运行器,以使用 PooledBlobStoreImpl 针对我们的后端,以命中 qbert.legacycorp.com:7001 的 Docker 容器别名。最后,我们将原始的、手动线程交换为现代 ExecutorService,以确保系统能够优雅地处理并行负载,而不会在并发异常下屈服。

We had modernized the core API. Now we must verify thread safety.我们已经现代化了核心 API。现在我们必须验证线程安全性。

  1. Modernize the Load Test: Refactor StoreALot.java. It’s currently a main script; keep it executable but clean up the syntax with Generics and modern Loggers. 现代化负载测试:重构 StoreALot.java。它目前是一个 main 脚本;保持可执行,但用泛型和现代记录器清理语法。
  2. Target the Backend: Ensure it uses PooledBlobStoreImpl to hit the Docker container alias (qbert.legacycorp.com:7001). 定位后端:确保它使用 PooledBlobStoreImpl 来命中 Docker 容器别名 (qbert.legacycorp.com:7001)。
  3. Concurrency Verification: Run with ExecutorService instead of manual threads. Handle parallel load without throwing ConcurrentModificationException.并发验证:使用 ExecutorService 而不是手动线程运行。处理并行负载而不抛出 ConcurrentModificationException。

This rigorous orchestration yielded the definitive empirical proof I needed. I launched the stress test, firing 100 iterations across 10 concurrent threads directly at my Docker-contained BlobStore backend. The results were crystal clear: the application's entire thread-safety architecture successfully relied on PooledBlobStoreImpl—utilizing Apache Commons Pool—to seamlessly provision isolated backend instances to each active thread. By verifying this behavior under intense, simulated real-world conditions, I confirmed that our deep modernizations—the generics, the JUnit migration, and the structural collection swaps—had not destabilized the core historical logic.这种严谨的编排产生了所需的最终经验证明。我启动了压力测试,在 10 个并发线程中对我的 Docker 容器化 BlobStore 后端进行了 100 次迭代。结果非常清楚:应用程序的整个线程安全架构成功依赖于 PooledBlobStoreImpl——利用 Apache Commons Pool——来无缝地为每个活动线程提供隔离的后端实例。通过在激烈的、模拟的真实世界条件下验证这种行为,我确认了我们的深度现代化——泛型、JUnit 迁移和结构性集合交换——并没有破坏核心历史逻辑。

I had finally done it. I took a twenty-year-old piece of code archaeology that was completely uncompilable, untestable, and broken, and transformed it into a modern, thread-safe, and fully containerized Java 8 library.我终于做到了。我拿走了一件二十年前的考古代码,它完全无法编译、无法测试且已损坏,并将其转化为一个现代的、线程安全的、完全容器化的 Java 8 库。

Conclusion: The Handover结语:移交

A software restoration mission is never truly finished just because an artifact suddenly becomes functional; it is only complete when it meets a clear, unyielding definition of what it means to be done. For this legacy project, that milestone wasn't about achieving theoretical perfection, but rather about bringing the system to a specific, verifiable state where the code was completely runnable, testable, and predictable on modern hardware. By clearing that precise bar, I successfully transformed the repository from an opaque archaeological mystery into something much more familiar and manageable: standard technical debt.软件修复任务永远不会仅仅因为工件突然变得功能完备而真正完成;只有当它满足对“完成”的明确、不可动摇的定义时,它才算完成。对于这个遗留项目,那个里程碑不是关于实现理论上的完美,而是关于将系统带到一个特定的、可验证的状态,使代码在现代硬件上完全可运行、可测试且可预测。通过跨越那个精确的门槛,我成功地将仓库从一个不透明的考古谜团转化为更熟悉且易于管理的东西:标准技术债务。

Scrubbing the Environment

To ensure the next developer doesn't have to repeat my tedious archaeological dig, I switched my AI persona one last time to act as a lead repository maintainer. With this final objective in mind, I identified and systematically purged every historical artifact that belonged firmly to the past. First went build.xml, the legacy Ant script that had dictated the repository's rules for decades. Next, I emptied out the old lib/ folder—permanently discarding a loose bag of unversioned, hardcoded JARs—and swept away .classpath and .project, which were nothing more than abandoned artifacts from long-forgotten IDE setups. Running rm build.xml stood as the final, cathartic act of modernization; it officially severed our fragile link to the ancient Ant era and permanently forced the repository to rely on my modern Gradle engine.为了确保下一位开发者不必重复我繁琐的考古挖掘,我最后一次切换了我的 AI 角色,以扮演首席仓库维护者。带着这个最终目标,我识别并系统地清除了所有属于过去的历史工件。首先是 build.xml,这个几十年来一直决定着仓库规则的遗留 Ant 脚本。接下来,我清空了旧的 lib/ 文件夹——永久丢弃了一袋松散的、未版本化的、硬编码的 JAR 包——并扫除了 .classpath 和 .project,它们不过是被遗忘的 IDE 设置的废弃工件。运行 rm build.xml 是现代化的最后一次宣泄行为;它正式切断了我们与古老 Ant 时代的脆弱联系,并永久迫使仓库依赖于我的现代 Gradle 引擎。

The Project Roadmap: README.md

I didn't just leave behind a clean repository; I left a map. Working with the AI, I generated a comprehensive README.md file that perfectly reflects this new, standardized reality. Instead of an undocumented labyrinth, the file outlines a completely frictionless path to productivity, specifying basic prerequisites like Docker and Java 8+, and offering a dead-simple quick start that builds the project with a single ./gradlew build. Testing the entire infrastructure is now just as straightforward, requiring a quick docker-compose up -d to spin up the backend dependencies followed by a standard ./gradlew test. This single document completely transforms the project from an intimidating mystery box into a predictable, standard Java library, permanently shifting the experience for the next engineer from a grueling forensic investigation to a routine, standard onboarding.我留下的不仅仅是一个干净的仓库;我还留下了一张地图。与 AI 合作,我生成了一份全面的 README.md 文件,完美反映了这种新的、标准化的现实。该文件没有成为一个无文档的迷宫,而是概述了一条完全无摩擦的生产力路径,指定了 Docker 和 Java 8+ 等基本先决条件,并提供了一个极其简单的快速入门,只需一个 ./gradlew build 即可构建项目。测试整个基础设施现在同样简单,需要快速 docker-compose up -d 来启动后端依赖项,然后进行标准的 ./gradlew test。这份文档彻底将项目从一个令人生畏的神秘盒子变成了一个可预测的标准 Java 库,永久地将下一位工程师的体验从艰苦的取证调查转变为常规的、标准的入职引导。

The Transformation: Before vs. After转型:之前 vs 之后

FeatureDay 0 (The Archive)Day N (The Product)
Build SystemAntGradle 8
CompilerJava 1.5Java 8
Environment“Works on my machine”Docker
TestingManual ScriptsJUnit 5
SafetyRuntime RiskCompile-time Safety
ConfidenceSwallowed ExceptionsHardened Tests
Onboarding“Good luck figuring it out”README.md

Final Thought: The Augmented Archaeologist

The most important lesson from this experiment was fundamentally about human agency. When I initially leaned on the helpless “Tourist Prompt”—vaguely asking the machine to “fix this for me”—the entire attempt collapsed because the AI lacked a foundational understanding of the environment and the rigid constraints of the past. Success only arrived when I fluidly shifted mindsets to direct the execution: acting first as an archaeologist to identify the true architectural decay, then as a DevOps engineer to design the containerized time capsule, and finally as an architect to define a strict refactoring policy.这个实验最重要的经验本质上是关于人类的主体性。当我最初依赖无助的“游客提示词”——模糊地要求机器“帮我修复这个”——整个尝试崩溃了,因为 AI 缺乏对环境的基础理解和过去严格的约束。只有当我灵活地切换思维模式来指导执行时,成功才到来:首先作为考古学家识别真正的架构衰退,然后作为 DevOps 工程师设计容器化时间胶囊,最后作为架构师定义严格的重构策略。

The AI didn't magically restore this historical system on its own; rather, I restored it by wielding the technology as a powerful force multiplier. It took care of the tedious, repetitive translation layers—churning through the conversion from Ant to Gradle, drafting the Dockerfiles, and systematically squashing fifty distinct compiler warnings—while I focused entirely on high-level strategy. Because of this active partnership, the codebase is no longer an intimidating, tangled black box. It has become entirely runnable, testable, and predictable—fully equipped to endure the next ten years and perfectly positioned for whatever future refactoring awaits it.AI 并没有凭空神奇地修复这个历史系统;相反,我通过将技术作为强大的力量倍增器来修复它。它处理了繁琐的、重复的翻译层——通过从 Ant 到 Gradle 的转换,起草 Dockerfile,并系统地压制了五十个不同的编译器警告——而我则专注于高层战略。由于这种积极的伙伴关系,代码库不再是一个令人生畏的、纠结的黑盒。它已变得完全可运行、可测试且可预测——完全有能力在未来十年内持续存在,并为等待它的任何未来重构做好了完美的定位。


Acknowledgments致谢

Thanks to Matteo Vaccari for the series of articles on AI-assisted modernization. They were a key inspiration for the idea of ​​using AI-powered refactoring for truly old software, such as this Java 1.5 code.感谢 Matteo Vaccari 撰写的关于 AI 辅助现代化的系列文章。正是这些文章启发了我,让我萌生了利用 AI 重构像 Java 1.5 这种真正老旧软件的想法。

Many thanks to Martin Fowler for his feedback and guidance throughout the writing process. His help made this article clearer and, hopefully, more readable.非常感谢 Martin Fowler 在写作过程中给予的反馈与指导。他的帮助使本文表达更清晰,也更易于阅读。

I used AI in this article. I started by using Gemini to highlight the key moments from my experiment and turn my notes into an outline. Then I used it to help draft sections from that outline, which I reviewed, commented on, and revised by hand. Finally I used AI for a pass on flow and grammar. The experiments, conclusions, and final wording were all reviewed and edited by me and GitHub Copilot.我在本文中使用了 AI。首先,我利用 Gemini 提炼出实验中的关键时刻,并将笔记整理成大纲。随后,我用它协助起草大纲中的各个章节,并由我亲自进行审阅、批注和修改。最后,我利用 AI 对文章的流畅度和语法进行了润色。所有的实验、结论以及最终措辞都经过了我和 GitHub Copilot 的审阅与编辑。

Significant Revisions重大修订

16 July 2026: published2026 年 7 月 16 日:发布