Java 8老系统代码阅读:先建结构化事实,再让AI解释项目

为什么要写这个系列

做Java后端十年,我接触过不少企业的核心系统。

金融、电商、政务------行业不同,但底层的现状惊人地相似:生产系统还在Java 8,框架停在Spring Boot 2.x甚至更早,代码跑了很多年,没人敢轻易动。

去年开始,几乎每个项目都在谈接AI。

但真正动手的时候,团队就卡住了。

不是因为不懂大模型,而是老系统本身接不住。JDK版本不够,Spring AI引不进来;依赖树牵一发动全身,升级一个包怕带崩一片;生产流量压着,不敢拿主流程赌一个AI试点。

更危险的是硬塞。我见过团队在老系统的Service层直接new一个HttpClient调模型API,Prompt拼在业务代码里,超时没配、降级没有。模型响应慢的时候,老系统的订单查询线程池被占满,主流程跟着卡住。还有团队把用户手机号、身份证原样送进Prompt,过了两个月才被安全审计发现。

这种事见多了,我就开始想一个问题:老系统不具备直接接入AI的条件,这是不是大多数企业的常态?

答案是肯定的。而且这不应该成为接不了AI的理由。

核心思路其实就三条:

text 复制代码
老系统少改 ------ 不升级 JDK,不引入 Spring AI 依赖
AI 能力旁路 ------ 独立部署,老系统通过 HTTP 或 MQ 调用
企业边界先行 ------ 脱敏、审计、降级、幂等比模型调用更重要

这个系列就是把这三条线展开成10讲。

从AI Gateway到MCP工具中心,从SQL Agent到RAG知识库,从工单Agent到多Agent研发团队------每一讲都围绕同一个前提:

你的老系统还在跑Java 8,你不能为了AI去赌它的稳定性。

每讲配套一个可运行的Maven Lab,不讲空架构,不写Hello World。你跑得通Demo,看得到边界,拿得到代码。

这就是我做这个系列的原因。


Java 8老系统代码阅读:先建结构化事实,再让AI解释项目

前三讲,我们一直站在"AI如何接入老系统"的视角看问题:

text 复制代码
第 1 讲:老系统不升级,通过 AI Gateway 旁路接入 AI
第 2 讲:老系统 API 不能裸奔,要包装成受控工具
第 3 讲:AI 生成 SQL 只能作为候选,必须经过安全引擎

这三个场景有一个共同点:AI要和老系统发生关系。

但这里有一个更底层的问题:如果团队自己都没有读懂老系统的结构,AI又怎么可能稳定地接入它?

很多Java 8老项目已经跑了七八年甚至十几年。写代码的人离职了,文档只剩一半,Controller里可能夹着业务逻辑,Service里可能有历史兼容分支,Mapper里还保留着旧字段。新人不敢改,老员工也只敢动自己熟悉的那几块。

这时候直接问AI:

text 复制代码
帮我解释这个类
帮我改这个方法
帮我接一个新能力

很容易得到一段"看起来很合理"的回答。问题是,它可能只理解了局部代码,却没有理解这段代码在整个项目里的位置。

所以第4讲先不讲RAG,也不讲工作流。这一讲要补上一个基础能力:

在让AI解释老项目之前,先把入口、分层、调用链、异常分支和数据边界这些结构化事实整理出来。

后面无论是做代码评审、知识库问答、工单分析,还是更复杂的研发Agent,都离不开这一步。

本讲要解决什么

这一讲的目标不是让AI直接改代码。目标是先生成一张"老项目阅读地图":

text 复制代码
给一个 Java 8 老项目
        ↓
扫描源码文件
        ↓
识别 Controller / Service / Mapper
        ↓
找到入口方法和调用链
        ↓
标出业务规则、兼容分支、异常处理、数据落库边界
        ↓
生成一条新人能看懂的阅读路线

核心观点:

text 复制代码
结构化事实在前,AI 解释在后。

不要让模型一上来就凭感觉猜项目结构。先让代码扫描器告诉它:项目里有哪些类、请求从哪里进、业务规则在哪一层、数据最终写到哪里。然后AI再基于这些事实去解释、总结、问答,可信度才会高很多。

本讲Demo

代码目录:

text 复制代码
code/spring-ai-enterprise-lab/labs/chapter04-code-reading

运行:

powershell 复制代码
.\compile-and-run.ps1

当前Demo不依赖真实模型,也不需要API Key。它先跑通"事实层":

text 复制代码
legacy-codebase-sample
        ↓
JavaProjectScanner
        ↓
LayerDetector
        ↓
CallChainAnalyzer
        ↓
ReadingPathGenerator

这一步是AI读代码之前必须准备的上下文。当前Demo负责生成模型可以依赖的结构化事实;后续接入真实模型时,模型再负责把这些事实转成更自然的解释、摘要和问答。

换句话说:这一讲跑通事实层,后续再让模型基于事实做表达层。

样例老项目

本讲内置了一个极简但带典型老系统特征的Java 8老订单项目:

text 复制代码
legacy-codebase-sample
└── src/main/java/com/ynzz/legacy/order
    ├── OrderController.java
    ├── OrderService.java
    └── OrderMapper.java

入口层:历史客户端版本

OrderController带上了历史客户端版本:

java 复制代码
public String createOrder(String userId, String productId, int quantity, int clientVersion) {
    return orderService.createOrder(userId, productId, quantity, clientVersion);
}

业务层:三个老系统常见点

OrderService#createOrder里有:

java 复制代码
if (quantity <= 0) {
    throw new IllegalArgumentException("quantity must be positive");
}

if (clientVersion == 1) {
    normalizedProductId = "LEGACY-" + productId;
}

catch (RuntimeException ex) {
    return "CREATE_ORDER_FAILED";
}

至少有三件事值得读:

text 复制代码
quantity <= 0          业务校验
clientVersion == 1     历史兼容分支
catch RuntimeException  异常兜底

持久化层:历史字段痕迹

OrderMapper#insert里保留了历史字段痕迹:

java 复制代码
// legacy_channel: 2018 mobile client only, old SQL mappings still reference this field.
// String legacyChannel = "APP_V1";
System.out.println("[Mapper] INSERT INTO order_tab(...)");

老项目最麻烦的地方,往往不是主流程看不懂,而是:

text 复制代码
为什么这里有个兼容分支?
这个 catch 是业务兜底还是临时补丁?
这个旧字段还能不能删?
Mapper 里有没有隐藏的数据副作用?

如果AI没有先看到这些结构事实,就很容易给出过于乐观的改造建议。

假设你只把OrderService#createOrder丢给模型。模型大概率会告诉你:

text 复制代码
这个方法负责创建订单。
它校验 quantity。
它生成 orderId。
它调用 Mapper 插入订单。

这段解释没错,但还不够。因为它没有回答:

text 复制代码
clientVersion 从哪里来?
clientVersion == 1 是不是还在线上使用?
CREATE_ORDER_FAILED 返回给谁?
Controller 有没有继续包装这个返回值?
Mapper 里的 legacy_channel 是否仍影响旧 SQL?

这就是"局部解释"和"项目理解"的差别。局部解释只看一个方法,项目理解要看入口、调用链、分层职责和数据边界。

所以第4讲要先生成结构化阅读路线,再让AI基于这条路线解释项目。

第一步:扫描项目文件

JavaProjectScanner做的事情很朴素:

text 复制代码
找到项目目录下的 .java 文件
读取类名、路径和源码文本
组装成 JavaProject

它不理解业务,也不调用模型。但它给后续分析提供了事实来源。

你不能默认模型已经知道项目结构。你要先告诉它:

text 复制代码
项目里有哪些类
每个类在哪个路径
源码文件之间可能有什么关系

这一步越清楚,后面的AI总结越不容易跑偏。

第二步:识别代码分层

LayerDetector用类名规则识别分层:

text 复制代码
OrderController -> controller
OrderService    -> service
OrderMapper     -> mapper

不同层的代码,阅读目的不一样:

  • Controller:重点看入口、参数、返回。
  • Service:重点看业务规则、异常分支、状态变化。
  • Mapper:重点看数据边界、字段映射、持久化副作用。

如果AI不知道这些层次,它可能会把Mapper里的历史字段当成业务规则,也可能忽略Service里的兼容分支。

第三步:生成调用链

CallChainAnalyzer从入口方法开始,生成一条最小调用链:

text 复制代码
OrderController#createOrder
        ↓
OrderService#createOrder
        ↓
OrderMapper#insert

调用链的价值不在于它有多长,而在于它回答了一个企业开发最常见的问题:

text 复制代码
这个业务动作到底经过哪些地方?

你要改创建订单的校验规则,就不能只看Service。你还要知道入口参数从哪里来、异常结果回到哪里、数据最终有没有写库。

第四步:生成阅读路线

有了分层和调用链之后,ReadingPathGenerator输出阅读路线:

json 复制代码
{
  "projectName": "legacy-order",
  "entrypoints": ["OrderController#createOrder"],
  "layers": ["controller", "service", "mapper"],
  "callChain": [
    "OrderController#createOrder",
    "OrderService#createOrder",
    "OrderMapper#insert"
  ],
  "readingPath": [
    "先读 OrderController#createOrder,理解请求入口和参数。",
    "再读 OrderService#createOrder,理解核心业务规则和异常分支。",
    "最后读 OrderMapper#insert,理解数据落库和持久化边界。"
  ]
}

这就是一张很朴素的代码阅读地图。它不替你下结论,只把"先看哪里、再看哪里、每一步确认什么"讲清楚。

规则扫描和AI应该怎么分工

这讲最重要的设计是分工:

text 复制代码
规则扫描负责事实
AI 负责表达和归纳
人负责确认和决策

规则扫描适合做确定性工作:

text 复制代码
类名、路径、入口方法、调用链、分层
可见的异常分支、可见的字段痕迹

AI适合做表达性工作:

text 复制代码
把阅读路线讲清楚
总结业务含义
提示可能风险
生成新人可读的解释
围绕代码事实做问答

负责最后判断:

text 复制代码
这个兼容分支还能不能删?
这个兜底返回是否合理?
这个字段是否仍被线上报表依赖?
这次需求到底应该改哪一层?

这样分工之后,AI才更像一个可靠助手,而不是一个凭空猜项目的回答机器。

这类能力能解决哪些问题

第一,新人接手老项目。 以前新人拿到项目,只能靠同事口头讲。如果系统能自动生成阅读路线,新人至少有一个起点。

第二,需求改造前做影响分析。 比如要改创建订单的参数校验。阅读路线会提醒你:入口参数在Controller、quantity校验在Service、clientVersion兼容分支也在Service、落库动作在Mapper。这能减少"只改一处,以为完了"的风险。

第三,为后续AI能力建立上下文。 后面做代码评审、RAG问答、工单分析,不能只给AI一个孤立类。更稳的是先给它:入口、调用链、分层、关键规则、异常分支、数据边界。这样AI的回答才更像"基于项目事实的建议",而不是"基于代码片段的猜测"。

从Demo到落地,还差什么

本讲Demo只跑通了"扫描→分层→调用链→阅读路线"的事实层,但真实项目落地还差几件事:

大项目增量扫描:Demo是全量扫描,企业项目往往有几千个Java文件,每次改动全量扫描不现实。需要增量扫描------只处理改过的文件,用缓存记录历史结构事实。

跨模块调用链:Demo只处理了单链路(Controller→Service→Mapper),真实项目有跨Service调用、异步事件、AOP拦截、Spring Bean注入。需要把这些关系也纳入调用链分析。

静态分析升级到字节码层:类名规则能区分Controller/Service/Mapper,但覆盖不了接口实现类、抽象方法、多态调用。生产级方案需要基于ASM或Javassist做字节码分析。

真实模型接入做表达层:当前Demo的ReadingPathGenerator输出的是结构化JSON。后续需要接入真实模型,把这份JSON翻译成自然语言的新人引导、风险提示和问答响应。

如果你正在推进老系统的AI辅助阅读能力,这几件事是绕不开的。完整方案会在后续更新中逐步展开。

企业避坑

  1. 不要把完整源码一次性丢给模型 --- 大项目代码量太大,模型上下文有限,安全风险也更高。更稳的方式是先扫描、切分、摘要,再让模型基于摘要解释。
  2. 不要让模型凭空判断调用链 --- 调用链、类名、方法名、入口方法这些结构化事实,应该尽量由规则或静态分析生成。
  3. 不要只解释单个类 --- 老系统最难的是链路,不是单个类。AI输出应该围绕"业务动作经过哪些地方"来组织。
  4. 不要忽略异常分支和历史兼容 --- 很多生产问题不是主流程错了,而是兼容逻辑、兜底逻辑、历史字段没有读懂。
  5. 不要让AI直接给改造结论 --- 更好的顺序是:先生成阅读路线 → 再确认代码事实 → 再让AI总结风险 → 最后由人决定怎么改。

小结

第4讲是这个系列里的一个转折点。

前3讲讲的是AI如何安全接入老系统。第4讲开始补另一个基础:AI和团队如何先读懂老系统。

这一讲的Demo只跑事实层,不跑真实模型。这不是退一步,而是把地基先打牢。

因为后续所有更复杂的AI能力,都应该建立在可验证的项目事实上。

后续讲次 和本讲的关系
第5讲 代码评审 评审前先用阅读路线确认入口、调用链和影响范围
第6讲 RAG知识库 文档切分和索引要依赖项目结构,而不是随便按文件切
第7讲 工单Agent 工单定位问题前,要知道业务动作对应哪条代码链路
第8-9讲 Browser Agent / Workflow Agent执行动作前,必须先理解代码上下文和系统边界
第10讲 多Agent研发团队 多Agent协作需要共享同一份结构化项目事实

不要让AI先猜。先给AI一张结构化的代码地图。只有解释有依据,AI才能真正进入企业老系统的研发流程。