接手这个活儿的时候,我本来没抱太大期望。一个2022年落地的Java老项目,代码量不大不小,刚好两万来行,涉及订单同步、库存扣减和对账几个模块,属于典型的“能跑就行”的内部系统。让我意外的是,把代码丢给AI做审查,竟然在几轮对话里挖出20个值得讨论的坑。更让我意外的是,团队里几个老炮一起复核之后,大家一致认可的问题只有15个,另外5个属于典型的“AI看到了代码,但没看懂业务”。
这篇文章就围绕这15:20的比例展开,说说我是怎么用AI做Java老项目代码审查的,AI的哪些判断能信、哪些不能信,资深工程师的价值为什么恰恰体现在过滤AI输出这件事上。如果你手里也有一堆写于两三年前的Java代码,想系统清理技术债又不想投入太多人力,这篇内容应该能给你一条可行的路径。
1. 为什么拿2022年的老项目开刀
1.1 老项目的“技术债”比新代码更值得交给AI
新项目的代码通常还保持着良好的纪律性,约束文档、规范插件、Code Review流程都在场,即使有问题也是零星散落。而2022年的老项目,经历过需求快速迭代、人员流动、临时hotfix的反复捶打,代码里沉积的往往是“当时赶工没来得及想清楚”的产物。
这种项目有个特点:问题分布极不均匀。一个模块可能写得很讲究,另一个模块则整个是“屎山”。让资深工程师从头到尾逐个方法去读,精力的投入产出比很低;而AI没有疲倦感,可以把所有文件扫完,把可疑点全部列出来。我这次就是把全部Java源码压缩成项目结构索引,分批喂给AI,让它按模块逐层审查,效率比人工通读高出几个量级。
1.2 AI审查和传统静态扫描的根本差异
做Java的人多数用过SonarQube、PMD或者FindBugs,它们靠规则库匹配代码模式,能准确但笨拙地找出“魔法数字”“未捕获异常”这类标准问题。AI审查则完全不是同一套逻辑。
我用的是对话式大语言模型。它不匹配规则,而是像一个读过大量代码的工程师,带着上下文去理解类与类之间的关系、方法的职责边界、事务的传播行为,再从代码气味和逻辑漏洞两个维度给出判断。换句话说,传统静态扫描是“查字典”,AI审查是“读文章”。
两种方式各有用处,但面对老项目,AI的灵活性明显更有价值,因为老项目最大的问题不是单个错误,而是设计层面的不协调——比如两个模块用了两套并发控制方案、事务被拉得太长、缓存和数据库的更新顺序不统一。这些问题靠规则库永远发现不了,AI却能识别出来。
1.3 适合用AI审查的项目特征
经过这次实践,我总结出适合AI审查的Java项目画像:
| 特征 | 说明 |
|---|---|
| 代码量适中 | 1万到10万行最好,太少AI没上下文,太多则输出质量问题 |
| 有一定技术债 | 如果全是新代码且严格遵守规范,AI的价值会大打折扣 |
| 属于内部业务系统 | 不涉及特别敏感的加密逻辑,可以安全地交给AI分析 |
| 团队能人工复核 | AI只是筛子,真正的判断必须由懂业务的人来做 |
如果你手里的项目符合这张表的特征,那接下来的实战流程可以直接照搬。
2. 审查准备:给AI搭好“舞台”
2.1 上下文打包的三个要点
AI审查的效果很大程度上取决于你怎么把项目交给它。我一开始图省事,直接丢了一个核心Service的代码给它,结果AI给出的反馈全是对单文件风格的点评,完全没有涉及类之间交互层面的问题。后来调整了做法,分三步走。
第一步,生成项目全貌的结构索引。把各模块的包名、类名、关键方法名整理出来,让AI先知道整个项目的骨架。第二步,按业务链路切分代码块。比如“订单创建链路”涉及OrderService、StockService、PaymentClient三个类,就把这三个类连同对应的Mapper接口一起喂进去,保持业务语义的完整性。第三步,在喂代码前用一句话说明这个模块的业务背景和预期行为,AI就不会把一些正常流程误判成问题。
这里最容易被忽略的是POJO和工具类。它们看起来不重要,但AI在判断“这个字段有没有被并发修改”“这个对象能不能安全发布”时,需要看到完整的定义。缺了这些,AI的判断依据不完整,误报率会明显上升。
2.2 环境问题的前置处理
我这次遇到一个典型的环境坑:项目里某个模块使用的JDK版本不统一,有的地方用了Java 11语法,有的地方却在pom.xml里配置了Java 8的编译目标。这类问题在编译时往往只表现为一句警告,比如“源发行版 17 需要目标发行版 17”这种提示,老项目里特别常见。
严格来说,这不算AI审查的范畴,但它会影响AI理解代码。如果连项目用的是什么Java版本都没确定,AI对“这个写法是否可以简化”的判断就会失真。所以我先把pom.xml和build.gradle都整理好,统一下JDK版本信息,再开始审查工作。
如果你也有这个警告,先在IDE里做两件事:检查Project Structure里的SDK设置,再检查Maven或Gradle的编译参数,确保source和target一致。这个问题不解决,AI审查过程中会一直被无关的环境噪音干扰。
2.3 审查会话的分层策略
一次把所有代码塞给同一个AI会话,输出质量会迅速下降。我的做法是分层开三个独立会话:
- 第一层:静态资源会话,只审查Controller、Service、Mapper的骨架和接口定义,关注API设计和分层合理性。
- 第二层:核心逻辑会话,挑出涉及金额计算、库存扣减、状态流转的代码重点审查。
- 第三层:并发与数据一致性会话,把所有出现synchronized、Lock、ThreadLocal、事务注解的地方集中分析。
三个会话各查各的,最后把结论汇总到一张表里。这个分法让每个AI会话的上下文窗口都塞满了相关信息,输出的问题列表质量高很多。如果你用的是上下文窗口较大的模型,可以适当合并,但分层审查的思路一定要保留。
3. AI扫描出的20个坑:按类拆解
3.1 线程与并发隐患
老项目的并发问题是最多的一类,AI在这一块的表现相当突出。它能敏锐地识别出“这个HashMap被多个线程写”“这个SimpleDateFormat是共享的”“这个static变量没有加volatile”等经典问题。
印象最深的一个坑:项目里有个缓存工具类,用了双重检查锁来初始化一个Map,但Map本身用的还是HashMap,没有用ConcurrentHashMap。AI直接指出,即使初始化过程被锁保护,后续的读写仍然是并发不安全的。这种判断需要理解“锁保护的是初始化,而不是后续操作”,AI做到了。
还有一处涉及线程池的坑。老代码里对每次请求都执行Executors.newFixedThreadPool(5),导致频繁创建线程池。AI指出这既浪费资源,也容易在请求量上来时造成4个线程的空转消耗。这两个问题我只认了第一个,第二个修复成本不高,但属于性能优化而非正确性问题,优先级排后。
3.2 数据一致性与事务边界
这个模块是AI输出的重灾区,因为它最容易“看到一半就下结论”。比如某个Service方法里,先更新订单状态,再调用远程接口通知物流系统。代码里没有事务注解。AI立刻标注“缺少事务控制”,但业务逻辑本身要求“订单状态更新成功后必须通知物流,如果通知失败需要重试”,这实际上是个最终一致性的场景,强行加事务反而会拉长数据库锁时间。
我们复核后把这类问题归为“需要人工判断”。AI看到了“没有@Transactional”的代码事实,却没有理解“这里本就不该有分布式事务”的业务语义。这就是20个坑里最典型的伪命题。
但AI也抓到了真问题。比如在库存扣减方法里,更新库存的SQL先执行,然后捕获异常试图回滚扣减操作,但同一个Service方法被类内部调用时,@Transactional注解根本不会生效,因为Spring的代理机制只对跨类调用生效。这个坑很隐蔽,AI不仅标了出来,还解释了“自调用”的成因,和我们核实后的结论完全一致。
3.3 代码风格与技术债
老项目的技术栈往往停留在两三年前的“标准写法”上。AI对这类问题的识别非常稳定,因为它读过海量的不同代际代码,能一眼看出“这行的写法应该升级了”。
举几个典型的:
- 大量使用
new SimpleDateFormat(),每次格式化都重新创建对象。AI建议用DateTimeFormatter替换,并且是线程安全的。 - 笨拙的字符串拼接,
s += item循环了几千次,性能损失明显。AI建议改用StringBuilder。 - 类型判断用
if (obj instanceof String)后强转,AI建议用Java 16的instanceof模式匹配优化。 - 数据拷贝用
BeanUtils.copyProperties做深拷贝,AI指出这个工具本质是浅拷贝,对嵌套对象无能为力。
第4点值得多说一句。团队里有几个年轻人觉得“AI连这个都要管,不是小题大做吗”,但老炮一致认为这是值得改的,因为项目里已经出现过一次因为浅拷贝导致两个对象共享同一个内部List,进而互相污染数据的线上事故。IO问题总在周末爆发,深拷贝的问题也一样。
3.4 安全与异常处理
AI在安全检查上的表现让人惊喜。它发现了一个存储型XSS入口,一个SQL参数拼接,还有一个文件上传路径穿越问题。这些都是实打实的安全风险,虽然这个老项目只在内网使用,但如果未来暴露到公网,就是致命漏洞。
异常处理方面的问题则要辩证看待。AI对“catch了Exception但什么都没做”的代码非常敏感,十次有九次会标出来。但老项目里大量这种空catch块,有些十确确实实是“吞异常”的坏味道,需要至少打一行日志;另一些则是“这里出错不影响主流程”的刻意设计。我复核时给每条都补上了注释,但并没建议全部修掉。
最终AI列出的20个问题分布是这样的:
| 类型 | 数量 | AI认为严重度 |
|---|---|---|
| 并发与线程安全 | 6 | 高 |
| 事务与数据一致性 | 4 | 高 |
| 外部接口调用与异常吞没 | 3 | 中 |
| 性能低效写法 | 3 | 中 |
| 安全风险 | 2 | 高 |
| 代码可读性与维护性 | 2 | 低 |
4. 老炮只认15个:人工复核的艺术
4.1 5个“假坑”是怎么被排除的
团队一起过AI报告时,争论最多的是5个被标记为高严重度的“事务缺失”和“空循环等待”。这5个之所以被老炮枪毙,原因各不相同,但核心逻辑是一致的:AI缺少业务上下文。
举一个典型例子。AI在某段库存同步代码里看到了一段while (queue.size() == 0) { Thread.sleep(100); },立刻标注“空转占用CPU,可能造成死循环”。但我们知道这段代码的上游是一个定时任务,每5分钟才向queue里写一批数据,这个循环的最大空转时间不会超过5分钟,而且任务本身被管理在独立的线程中,对主流程无影响。
再比如一个“循环里调用远程接口且没有超时设置”的问题。AI建议加超时配置,但实际调用的是内网的一个常驻服务,历史上从未出现过超过2秒的响应,而且服务调用的失败会由外层补偿任务兜底。老炮的判断是:这个循环是合理等待,不是缺陷。
这类差异是资深工程师和AI最本质的区别。看得懂代码结构的工具很多,看得懂业务意图的人很少。AI从代码出发,老炮从系统和业务的目标出发,两者结合才是完整的审查。
4.2 15个问题按优先级重新排序
排除5个误报后,我们按“线上风险 > 技术债偿还 > 可读性提升”三个维度重新排了优先级:
- P0(本周修):库存扣减自调用导致事务失效、存储型XSS入口、线程池每次创建、登录接口的口令哈希算法过旧。
- P1(本月修):SimpleDateFormat共享读写、深拷贝污染、SQL参数拼接、共享HashMap并发读写、空catch块补日志。
- P2(下季度修):字符串拼接、重复的魔法数字、过长方法拆分、DTO拷贝歧义、命名风格统一。
P0这5个问题里,有两个是线上出现过故障但定位为“偶发”的,另一个是安全审计发现的“低风险”漏洞。AI把它们一股脑翻出来,给了我们一次性解决的机会。以前这些分散在文档、故障复盘、审计报告里的内容,从来没有被系统地汇总过。
4.3 我的筛选规则
经过这轮磨合,我总结了一套自己的AI审查结果分级法,分享出来供大家参考:
- 信任AI的判断,当它说的是“事实性”问题时:例如一个共享对象被多线程读写、一个不存在的导入、一个数组越界,这类有明确正误答案的问题,AI基本不会错。
- 参考AI的判断,当它说的是“改进型”问题时:例如“建议用StringBuilder”“建议拆分方法”,这些方向没错,但改不改要看你项目当前阶段的稳定性需求。
- 核实AI的判断,当它说的是“设计型”问题时:例如“缺少事务”“循环等待”“没加缓存”。这些往往是业务约束的结果,必须结合需求文档判断。
4.4 一个被AI发现却被我们低估的问题
这里多说一句:最初老炮们把“外部接口响应未校验”排在了P2级别,理由是“内网服务很稳定”。但AI的表述措辞是“该接口返回结果未进行空值判断,若上游变更协议将直接导致未解包的NullPointerException”,这句“若上游变更协议”点醒了我们。
实际上,这个外部接口就是团队另外一个项目提供的,对方在三个月前就调整过字段命名,当时因为是新增字段所以没暴露问题,但如果未来对方把某个必填字段标记为可空,我们这边直接炸。我们把这个项升到了P1,这个决策不能算AI的功劳,也不能算老炮的功劳,而是两者讨论过程中撞出来的。
5. 实操过程中的问题与排查技巧
5.1 喂代码时的隐私与切分问题
如果你手头的是公司内部老项目,使用外部AI审查服务时务必注意数据脱敏和审查范围。我的做法是先把代码里的数据库连接地址、密钥、内部服务域名全部替换成占位符,再交给AI。同时只审查业务代码,不审查基础设施配置和密钥文件。
切分上还有一个容易踩的坑:同一个类里的多个方法被分到不同会话里审查时,AI对上下文的理解会割裂。比如UserService里有validateUser和updateUser两个方法,单独看updateUser,AI看不出任何问题;但把两个方法放在一起,AI才能发现“validateUser的结果根本没被updateUser使用”这一逻辑断点。切分时尽量保持方法组完整。
5.2 老项目编译环境与JDK版本导致的干扰
前面提到过JDK版本不统一的问题。这次审查期间,pom.xml的编译参数是Java 8,但某台开发机上装了Java 17,代码里有些地方用了Java 11才支持的API。AI看到源码后本能地用较新的API规范去套老代码,结果产生了几条误导性建议。
解决办法:审查前先统一项目编译信息,把它作为Context告诉AI,“本项目的目标编译版本是Java 8,因此基于Java 9以上的API新特性不建议引入”。这样AI的建议会更贴合项目的实际情况。如果你想让AI给你的Java代码提建议,这个步骤一定要做,否则它会一直拿最新规范来批评几年前的代码。
5.3 如何避免AI“睁眼说瞎话”
AI在我这次审查中也出现了两次硬编造:一次声称某处调用了项目里不存在的方法,另一次指认“某类中定义了static变量但从未使用”,实际上那个变量是在父类里定义的。
这类情况的应对思路是:要求AI给出修复建议时顺带说明代码定位。我在会话里固定追加了这样一句话:所有结论必须附上类名、方法名和行号,或者整段代码引用,没有定位信息的问题条目直接丢弃。加了这条约束后,AI的输出严谨了很多。
5.4 20个坑里的“伪修复”教训
团队里有人按AI的建议修了一个“共享HashMap改为ConcurrentHashMap”的问题,看似完美,结果在压测时发现性能下降得很明显。原因是这个Map的读操作远高于写操作,而ConcurrentHashMap的读路径在竞争激烈时也有同步开销。老炮把实现方式换成了读写锁 + LinkedHashMap,性能才恢复正常。
这个案例说明了一个道理:AI给的建议方向是对的,但实现方式的权衡需要人工决定。老项目里尤其如此,因为老代码往往是“某一次性能压测后形成的妥协产物”,任何改动都可能导致性能回归。
我整理了一个简易的选择表:
| 场景 | 推荐做法 |
|---|---|
| 读多写少且热点集中的Map | 读写锁 + LinkedHashMap |
| 读写均衡且并发量高 | ConcurrentHashMap |
| 数据量小且允许丢失 | CopyOnWriteArrayList 或直接用锁块 |
| 有顺序要求的并发Map | ConcurrentSkipListMap |
5.5 高效的复核会议怎么开
最后一点实操建议:不要拿着AI报告直接开全员评审会,那样大家只会盯着报告逐条过,效率极低。我建议两轮会议:
第一轮是“老炮闭门会”,只有3名核心开发参加,先把AI报告的20个问题筛一遍。对每个问题回答三个问题——这个会不会引发线上事故?修的成本高不高?有没有其它地方依赖当前行为?得到“修/不修/缓修”的结论。
第二轮是把筛选后的15个问题同步给全员,但会上只讨论P0和P1,P2放到日常迭代里消化。会议时间控制在一个半小时以内,问题讨论基于代码示例和线上故障记录进行,气氛会务实很多。
6. 复盘:AI审查的价值与边界
这轮AI审查的实际收益,超出了我最初的预期。20个坑里,我们最后确认值得处理的15个中,有5个是过去一年线上故障的间接原因,6个会在未来某次流量突增时引爆,4个属于纯粹的技术债。如果没有AI,这些不显眼的坑很可能继续潜伏。
更重要的是,AI改变了我对老项目的维护思路。以前对于两年前的项目,团队的态度是“能不动就不动”,怕修出一个新问题。现在AI先把代码扫过一遍,把改动影响面缩小到可预测的范围,修复的恐惧感下降了一大截。技术债依然是债务,但债务要还的前提是——你至少得知道债主是谁。
再分享一个过程中的小体会:AI给出问题报告以后,最先认可的往往不是团队里最年轻的成员,而是工作经验最久的那位。老炮看到“自调用导致事务失效”这类问题时,会立刻联想起自己当年踩过的坑,而新人对这些坑完全没有直觉。所以,AI真正作用的是放大有经验者的判断半径,而不是替代没有经验者的成长过程。
7. 后续还能往哪扩展
这套AI审查流程并不是只做一次就完结的。我目前正在做两件延伸的事情。
第一件事是把筛选后的15个坑固化成审查模板。给AI配置一份持续使用的审查提示词,明确要求它检查“事务自调用、并发容器选择、外部调用结果校验、异常吞没、深拷贝、日志规范”这几类老项目高发问题。下次任何同事接手老项目时,都可以直接复用这套提示词做初筛。
第二件事是建立代码审查的基线档案。把这次审查的代码版本、AI输出报告、人工复核意见、修复记录全部归档,作为以后Review同类项目的对照标准。间隔半年后再跑一次AI审查,对比新旧报告,就能量化出“质量到底提升了多少”。
如果你手上也有一个2022年甚至是更早的Java项目,可以按我上面的流程跑一遍。不用追求审查出多少个坑,关键是让AI先帮你把“可能有问题的地方”圈出来,再让最懂业务的人去判断哪些是真坑、哪些只是看起来像坑。这个过程本身,就是一次投入产出比极高的技术债盘点。