☰
AI代码审查实战:Java老项目20个坑,老炮只认15个
2026/9/28 15:48:39 网站建设 项目流程

前几天,我把公司一个2022年上线、代码已经堆了近千个类的Java老项目,整体跑了一遍AI代码审查。经过一轮扫描加人工归并,AI一共挑出20个不重样的“坑”,我整理成清单发给组里那个有十一年Java经验的同事,他翻了一晚上只回我一句话:15个是真的,剩下5个别说出去,是AI在讲理论课。

这个反馈我觉得特别有价值。AI代码审查这两年很热,但大家听到的多是“AI帮我发现了几个隐藏Bug”的爽文版本,很少有人聊AI会把哪些“理论上该改、实际上别动”的问题也一板一眼地写进报告。这次实操下来,AI审、人裁、再落地的完整链路,对我来说比单纯抓出几个Bug更重要。如果你手里也有一堆存量的Java项目,或者正在纠结要不要引入AI做代码审查,这篇文章基本能回答你的大部分疑问。

1. 为什么选中这套2022年的Java老项目来“开刀”

1.1 老项目的典型画像与技术负债

先介绍这个项目的基本盘。这是一套2022年春天上线的交易后台,技术栈很标准:Java 8、Spring Boot 2.3、MyBatis连MySQL,模块之间走内部HTTP接口。这种技术栈放到今天不算老到不能碰,但代码上的历史包袱一点都不少:项目前后经历了三四批开发,需求密集的时候一天十几个commit,注释写不写完全看当时的心情。到2022年年底,新进来的同事已经不敢动里面几个核心Service了,改一行都怕出线上事故。

这类老项目的共性是:系统能跑,但代码里的隐患是“慢慢攒”下来的。单点故障不一定发生过,不代表并发大了不会出问题。比如某个公共的日期格式化工具类,配置里写着“不要动”,但没人能说清当初为什么这么写;再比如订单模块里一堆if/else,分支里的数字没人敢改成配置项,因为不知道哪些历史订单依赖它。老项目审查的难点就在这里:你既要发现问题,又要判断这个问题在当前业务场景下会不会真的爆,以及改动它会不会引发另一批潜在问题。

之所以专门挑2022年的项目,是因为它“老得刚刚好”。比起那些Java 6时代的祖传代码,这个项目还在用Java 8的主流写法,AI大模型对这类代码的理解准确率很高,扫描结果有参考价值;比起刚上线的新项目,它又积累了足够多的历史改动、并发场景和业务兼容逻辑,能让AI的边界暴露得比较充分。如果你拿一个刚写完三个月的项目去跑AI审查,大概率只能得到一堆“缺少注释”之类的噪音。

1.2 AI代码审查能解决什么,解决不了什么

传统静态分析工具,像SonarQube、PMD、SpotBugs,规则很全,但对跨方法的语义理解很弱。Sonar能发现“这个变量赋值了但没使用”,却很难判断“这段代码在多个线程并发调用时是不是安全”。AI在这方面的能力要强不少,尤其是Java这种训练语料充足的常见语言,大模型见过足够多的并发、资源泄漏、数据库事务案例,给出的判断往往已经接近一个中级开发者的水平。

但它解决不了另一件事:业务上下文。AI不知道某个字段为什么叫flag,不知道为什么这段代码要兼容三年前的脏数据,也不理解“这个接口虽然没用但老客户还在调”这种历史包袱。它只会从代码结构本身判断是否符合主流最佳实践。这个特点决定了AI审查的结果必须经过一道人工复核,谁复核?只能是那些对业务有完整理解的人。

所以我给自己定的原则很简单:AI负责扫描和提示,人负责裁决和落地。把AI当成一个外行实习生,它能加班加点帮你把代码从头到尾读一遍,给你一堆线索,但最终拍板的必须是有经验的人。想明白这点,后面遇到误报就不会着急上火,而是会把它当成AI给你的一次“定向提醒”。

2. 20个“坑”是怎么被AI挑出来的

2.1 审查环境准备:工具选型与扫描范围的取舍

我没有买专门的商业代码审查平台,整个方案很朴素:项目clone到本地,写一个Python脚本按包路径把核心Java文件切块,调用大模型API做逐段审查。为什么不直接用商业平台?一是这个交易后台涉及内部数据协议,代码不方便上传到外部服务;二是用脚本自己控制提示词,可调整的空间更大。如果你的项目没有这种顾虑,用现成的AI编程助手插件也可以,省事,效果接近。

扫描范围上,我做了明显的取舍。全仓库有900多个Java文件,我没有全部丢进去,而是按风险偏好选了四批:第一批是core包和公共工具类,第二批是订单、支付、库存三个核心Service,第三批是自定义注解和Spring AOP切面,第四批是Mapper接口和对应的XML文件。每一批大约300到500行核心代码,刚好能装进上下文窗口,AI的判断质量最高。

为什么不一次性全量扫描?实测下来,超过1500行代码硬塞进去,AI会出现两类毛病。第一类是捡了芝麻丢西瓜,高风险的并发问题淹没在代码海里,它反而去揪一个无关紧要的命名问题;第二类是开始“编问题”,把不存在的Bug说得有鼻子有眼,仿佛不报几条就不够尽责。控制输入规模,是我这次实操里最朴素的防伪技巧。

2.2 给AI立规矩:审查提示词与分层规则

AI审查的效果,七分靠提示词。我第一轮扫描用的是通用提示词,结果输出的内容偏“教学风”,每段代码都能给你总结出几条改进建议,看着很充实,但不少根本没抓住重点。后来我把提示词改成了下面这版,效果立刻不一样:

你是一名有十年经验的Java架构师。下面是某个Java 8 + Spring Boot 2.x存量项目的一段核心代码。 审查要求: 1. 只报告真实影响线上稳定性、数据一致性、并发安全、资源管理的问题; 2. 忽略命名、注释、缩进等风格问题; 3. 不要建议引入新的框架或升级JDK; 4. 每条问题按以下格式输出: - 问题位置:类名/方法名/行号 - 触发场景:什么条件下会出问题 - 影响分析:可能导致什么后果 - 修复建议:给出可直接替换的代码 5. 如果代码没有可复现的严重问题,明确说“无严重问题”,不要凑数。

最后一条“不要凑数”特别关键。大模型如果没有这条硬约束,为了显得自己专业,经常会给每个片段找出两三条不痛不痒的小毛病,什么“建议使用常量”“建议提取方法”全都冒出来了。加上这一句之后,输出内容立刻干净很多,每条都是冲着要害去的。

另外,不同层级的代码我会微调审查重点。Controller层,关注参数校验够不够、返回码是否合理、有没有把内部异常直接抛给前端;Service层,关注事务边界对不对、RPC调用是不是嵌在锁或事务里;Mapper XML,关注动态SQL拼接有没有注入风险、结果集映射会不会全表扫描;工具类,关注线程安全、缓存设计和日期解析。不同模块给不同的提示词,比一个万能Prompt从头用到尾效果好得多。

2.3 一次完整扫描的现场记录与问题归类

拿core包里的公共类举例,我用脚本扫描了37个文件,AI返回了24条疑似问题。去掉重复项、去掉它自己都标注“可能”的模糊项,剩下12条有效问题。再扫核心Service层,又出现20条。两个批次交叉去重之后,我把所有问题在Excel里建了一张总表,按风险类型归类,最终收敛成20个独立问题。

这20个问题的分布很有意思:线程安全类4个,资源管理类4个,数据库与事务类5个,异常处理类4个,代码质量与可维护类3个。这个分布和很多老Java项目的规律是一致的——线上最容易先出事的,基本集中在并发、资源、数据库这三块。像魔法数字、过时API这类问题虽然也多,但不会一夜之间把系统打挂,属于“慢刀子割肉”。

这里有个经验值得分享:AI扫描结果一定要做“归并”,不要直接拿原始输出去干活。AI在扫描不同文件时,经常会对同一个根因问题描述好几遍,比如“多处使用SimpleDateFormat”可能在三个文件里报三次,但它们其实是同一个问题。不归并就去排期,很可能会让团队成员觉得AI不靠谱,反而损害了这个工具的可信度。

3. 20个坑,老炮为什么只认15个

3.1 15个真问题速查表与分级标准

20个候选问题,经过同事复核后,15个被认定为“真问题”。我按影响级别做了一个速查表,P0代表必须尽快修,P1代表强烈建议修,P2代表看机会修。分级规则不是凭感觉,而是用“故障概率×故障影响”来算。比如IO流不关闭,在交易链路里就是必然事件,影响是进程假死,直接定为P0;魔法数字在绝大多数场景里只是可读性问题,不会引发线上故障,所以只能算P2。

编号问题描述影响级别风险类别
1文件流/数据库连接未用try-with-resources释放P0资源泄漏
2SimpleDateFormat使用全局静态实例P0线程安全
3循环内逐条执行SQL(N+1查询)P0性能/数据库
4catch块吞掉异常,无日志无上报P0故障不可见
5大事务中同步调用外部接口P0数据一致性
6静态List缓存无限增长P1内存泄漏
7并发场景HashMap裸用P1线程安全
8日志用字符串拼接而非占位符P1可观测性
9多个写操作缺少事务注解P1数据一致性
10正则表达式每次调用都重新编译P2性能
11equals和hashCode行为不一致P2集合异常
12StringBuffer滥用(应使用StringBuilder)P2性能
13空判断顺序颠倒,equals前不判nullP1空指针
14使用过期的Date相关APIP2技术债
15魔法数字散落在业务条件中P2可维护性

这张表里,前五条是线上事故的常见主角。如果你所在的项目也有类似代码,建议直接按P0的顺序排期修复。P1里面,HashMap并发和日志占位符属于改动成本不高、收益明确的,能顺手处理就顺手处理。P2那些更多是代码卫生问题,可以在需求改动到对应类时顺带清理。

3.2 高风险问题逐条拆解:6个当场改掉的案例

P0级别的5个问题,加上P1里的HashMap并发,一共6个,我逐个拆开说。这些代码片段都是老项目里非常典型的样子,即使你没见过一模一样的,大概率也见过近似的。

第一个是IO流不关闭。老项目里总有那么几个导出的方法,打开文件然后“忘了”关:

public void exportReport() { FileInputStream fis = null; try { fis = new FileInputStream("/tmp/report.xlsx"); // 处理文件内容 } catch (IOException e) { log.error("export failed", e); } finally { // 这里忘了关闭fis } }

这类代码在上线初期往往跑得好好的,因为数据量小、并发低,进程的生命周期掩盖了问题。等到要同时导出大文件、并发一上来,文件句柄耗完,进程直接假死。AI判断这类问题很准,因为这是训练数据里出现频率极高的反模式。修复方式也简单,用try-with-resources把它包起来就行,顺手还能省掉finally块。

第二个是SimpleDateFormat全局静态实例。这在老项目里几乎是标配:

public class DateUtil { private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd"); }

单独的格式化操作没问题,但多线程环境下,SimpleDateFormat内部的Calendar是共享的,毫秒级错乱、NumberFormatException都可能出现。更麻烦的是,这类问题很难被测试覆盖,因为它往往只在某个特定日期格式、特定并发量下才蹦出来。AI能识别这种模式,但修复方式值得注意:不是每个地方都立刻换DateTimeFormatter,如果项目暂时没有升级JDK的打算,局部用ThreadLocal封装也可以。

第三个是循环内逐条执行SQL。这在订单、库存类业务里太常见了:

List<OrderItem> items = orderItemMapper.selectByOrderId(orderId); for (OrderItem item : items) { Product product = productMapper.selectById(item.getProductId()); // 循环查库 }

一个订单有100个明细,就会产生100次SQL;列表页一次展示100个订单,SQL数量直接上千。数据库连接池再大也扛不住这种放大效应。AI能轻松看出这是循环内数据库访问,但修复方案要结合数据量来定:数据量小可以用批量IN查询,数据量大可能要走分页或者异步汇总。重点是不要无脑改成一条大SQL,那样可能又引入慢查询问题。

第四个是catch块吞异常。这个是我见过最严重的坑之一:

try { riskService.check(accountId); } catch (Exception e) { // 什么都不做,风控校验形同虚设 }

吞异常比没有try更糟糕,因为所有失败都被静默掉,运维完全不知道系统已经在错误状态里跑了好几天。AI能识别“catch块为空或只有注释”这种模式,但只能靠人来拍板:到底应该把异常抛出去,还是记录日志后走降级分支。这个决定必须结合业务场景,比如风控校验失败,业务上是要阻断交易还是放行,那得问产品,但至少得让这个异常“发声”。

第五个是大事务中调用外部RPC:

@Transactional public void createOrder(OrderDTO dto) { orderMapper.insert(dto.toOrder()); paymentService.pay(dto.getOrderId()); // RPC,可能耗时数秒 inventoryClient.deduct(dto.getProductId()); // 外部HTTP // 整个事务被外部调用拖得很长 }

@Transactional直接包住RPC,是Java后端面试经典问题,但存量项目里就是能活下来。原因很简单:单机开发环境下一切正常,一旦外部服务超时,数据库连接就被占着不还,连接池一满,整个模块就瘫了。老炮认可这个问题的原因是它一定会爆,只是时间问题。修复方式是把RPC调用挪出事务,先落本地数据,再异步通知外部服务,配合补偿机制保证最终一致。

第六个是HashMap并发裸用:

private static Map<String, ConfigItem> cache = new HashMap<>(); public ConfigItem get(String key) { if (cache.containsKey(key)) { return cache.get(key); } ConfigItem item = loadFromDb(key); cache.put(key, item); return item; }

这个问题的隐蔽性很高。HashMap在并发put时可能形成环形链表,导致get卡死或者CPU跑满。老项目里缓存逻辑如果散落在各处,靠人工review很难发现,因为单看每个方法是正常的。AI扫出来之后,最稳的修复方式是换ConcurrentHashMap,或者直接上一个成熟的开源缓存框架,把缓存生命周期管起来。

3.3 中低风险问题怎么批量处理不返工

P1和P2级别的问题,修起来比P0简单,但在工时排期上容易被砍掉。我的建议是不要一次性铺开狂改,而是结合需求迭代“顺手”处理。日志占位符、StringBuffer这类属于机械替换,让团队新人练手非常合适,风险低、边界清楚。魔法数字、equals/hashCode这类需要理解业务逻辑,就放到需求改动涉及的类里去改,不要单独开票。这样既不额外占用迭代容量,又能保证改动的代码有人review。

另一个经验:15个被认可的问题里,真正需要立刻动手的可能只有七到八个,其余的必须拉上业务侧确认。比如第9条“多个写操作缺少事务注解”,如果业务上本身允许部分成功、后面有补偿任务兜底,那就不一定要套事务。AI给出的是通用最佳实践,但落地要看业务模型。老炮所谓的“只认15个”,不是说15个都得马上改,而是“这15个是真实存在的技术债,记上账,按优先级还”。

4. 剩下5个坑,为什么不认账

4.1 误报的三种典型来源

AI一共报了20个,老炮只认15个。那5个不是说AI“看错了”,而是它在严格按照教科书说话,提出的建议在真实系统里站不住脚。我复盘了一下,误报主要来自三个来源。

第一是缺少业务上下文。AI不知道哪些“看似无用”的代码是当年为了兼容某个特殊渠道留下的,也不知道哪些接口字段虽然现在没人用,但老客户端还在调。第二是对历史债务缺乏同理心。AI默认你活在“理想的Java 17环境下”,默认你有无限时间重构,默认外部依赖都可以随便换,但这些在存量项目里根本不存在。第三是“过度优雅症”。AI倾向于把代码往设计模式、函数式风格、不可变对象上引,至于改完之后框架认不认、同事读起来累不累,它不负责。

这三个来源决定了误报不是偶发现象,而是一种系统性偏差。理解了这一点,后面在处理AI报告时就不会简单地把它们当成噪声丢掉,而是能判断出:这类建议背后,AI到底忽略了项目的哪部分现实。

4.2 五个具体翻车案例的分析

第一个案例是AI建议把订单模块里根据用户等级走不同折扣的if/else重构成策略模式。原代码大概两三个分支,AI给出一整套Strategy接口加工厂类的设计。单看代码,这个建议不算错,但真实情况是这段逻辑下个月就要跟着营销活动改成配置化,现在重构成策略模式纯属白费功夫,改完三个月内又要全部删掉。老炮的评价很直接:两个分支的策略模式,是给面试题准备的,不是给线上代码准备的。

第二个案例是AI建议升级新JDK的API写法。代码里有大量new String(bytes, charset)这类老API,AI建议统一改成更现代的写法。但项目目前跑在Java 8上,升级JDK涉及基础镜像、编译插件、线上JVM参数、性能回归测试,是一整套基建工程。更关键的是,现有API用着并没有出过问题。为了“更规范”去升级整个运行环境,属于主动放大风险,老炮自然不会签字。

第三个案例是AI标记了一个“从未被调用”的私有方法,建议删除。实际上那个方法是被Spring的定时任务加反射机制调用的,AI单看静态调用链根本不会发现。这提醒我们:AI的判断基于统计规律和静态上下文,遇到反射、SPI、字节码增强这类机制,很容易看走眼。拿到“死代码”类建议时,先全局搜一下符号,再决定动不动手。

第四个案例是AI建议把一段for循环过滤汇总改成Stream加collect。原代码本身没问题,但循环里需要处理一个checked exception,Stream跟checked exception天生不对付,还得包一层wrapper才能编译。数据量也就几百条,两种写法性能上没有差别。改完之后,老手反而要额外多读半分钟才能理解逻辑。这类“为优雅而优雅”的建议,被一票否决是必然的。

第五个案例是AI建议把核心DTO的字段全部改成final,理由是“不可变对象更安全”。这个建议在纯自己写的代码里成立,但项目里的DTO要过MyBatis的结果映射和Jackson序列化,这两个框架默认依赖无参构造和setter。字段一旦声明为final,运行期反序列化会直接抛异常。任何脱离框架约束的建议,都只能停留在PPT层面。

4.3 把误报率从25%降到10%的三个约束

误报没法完全消灭,但能明显压下去。我试过的最有效做法有三个。

第一个约束是审查前写清楚技术栈和边界。比如在提示词里加一句“项目使用Java 8、Spring Boot 2.x、MyBatis、不允许引入新框架、不允许改变数据库表结构”,AI就会自动收敛一大批建议,不会整天推荐你用虚拟线程和records。第二个约束是强行区分“问题”和“优化点”。定义也不复杂:问题必须有明确的失败场景,比如“并发下会抛异常”“连接耗尽会宕机”;优化点是可做可不做的,比如“用Stream更优雅”“用record更简洁”。凡是只讲好处、讲不出什么场景会炸的,默认按优化点处理,挂起等人确认。第三个约束是对AI输出做二次筛选:凡是无法说清“在什么条件下会触发”的问题,都先不纳入修复清单。

经过这三层约束,实际误报率从我第一轮跑出来的大约25%,降到了10%左右。这个比例带来的好处是,人工复核时不用在一堆噪音里大海捞针,团队也不会因为AI报告太水而产生抵触情绪。

5. AI审查结果怎样人工复核与落地

5.1 分级复核流程:从扫描报告到修复工单

20个问题不可能一次性全改,我搭了一个简单的四级复核流程。先逐条确认问题真实性和影响,这一步由最熟悉对应模块的人来做;再按模块归属把问题分给对应的代码owner,避免一个人同时改所有模块;然后让owner反馈修复成本和风险,特别是那些涉及兼容逻辑的问题,必须写清楚“改了会影响谁”;最后由我拍板排期。排期原则很朴素:P0必须纳入最近一个迭代,P1争取下一个迭代,P2挂到技术债清单持续跟踪。

每个修复完成后,我会把改前和改后的代码都喂给同一个AI审查提示词做一次对比复核,确认它认为问题已经闭合。这个过程每次大概一刻钟,但能有效防止“改了一半漏了另一半”。比如修复SimpleDateFormat时,只改了工具类,但业务侧还有几个new SimpleDateFormat的零散调用,AI对比复核能帮你把它们都找出来。

5.2 把项目背景写进提示词,误报率立刻下降

AI误报率高,很多时候是输入信息不足,不是模型不行。我在第二轮扫描时往提示词里追加了一段项目背景,比如“这个模块需要兼容2020年以前的历史订单数据”“接口字段不能随便删,因为有老客户端在调用”“优惠逻辑下个月会迁移到配置中心”。这些背景词一加进去,AI自己就把好多建议撤掉了,因为它也开始意识到“这个改动在现实里会碰壁”。

还有一个非常实用的技巧:把团队内部的代码规范文档也塞进上下文。比如你们约定Controller所有返回都用统一的Result包装,那AI就不会因为“返回裸对象”给你报一条假问题;你们约定某个遗留模块不允许改动状态,那AI就不会整天建议你加final或改不可变设计。让AI按照你们自己的规范审,而不是按全网通行的规范审,效果天差地别。

5.3 把AI审查变成日常习惯的三个建议

一是固定节奏。我们目前是两周扫一次,每次挑一个模块,大约花半天时间。节奏不要太密,否则修复跟不上,报告堆积成山;也不要太疏,隔三个月想起来的模式,基本等于没有降低风险。二是沉淀语料。每次被人工否决的AI建议,我都整理进一个文档,下次在提示词里加一句“以下情况不要提示”,模型对这个项目的贴合度会越来越高。这本质上是在用团队经验持续校准AI的审查尺度。三是跟人结合。AI负责把代码从头到尾读一遍,人负责带着业务视角再读一遍。它替代不了code review,但它能让code review的输入质量高一个档次。

6. 实测复盘与常见问题速记

6.1 数据复盘:从120条原始问题到20个真坑

把整个过程的数字摆出来,第一轮全量扫描,AI返回的原始问题超过120条。经过人工去重和归并,收敛成20个独立问题。这20个里,老炮认可15个。15个真问题里,有8个是我不借助AI靠日常review很难发现的,尤其是那些跨文件的并发隐患和资源管理问题。整个过程我花了大概两天:第一天跑脚本、调提示词、做批量扫描,第二天对清单做人工复核和分级。如果完全靠人肉去把这15个问题找出来,没有三周时间下不来。这个投入产出比,我认为相当划算。

但说实话,AI审查也有自己的天花板。它对已知模式敏感,比如线程安全、资源关闭、循环查库,这些领域大模型训练数据足够多,判断可信;但到了业务规则混淆、历史数据兼容这类“信息藏在大脑里”的问题,它基本无能为力。我的结论一直是:AI的作用是扩大搜索半径,人负责把半径内的东西看准。

6.2 土办法与总原则

分享三个自己总结的原则吧。第一,别贪多。一次喂500行核心代码,比一次喂5000行效果好得多。AI跟人一样,输入超过处理能力之后就会开始划水,要么漏报要么瞎报。第二,别全信,也别不信。AI说“严重”的时候,你把代码复制下来自己跑一遍、推演一遍;AI说“没问题”的时候,你也不用完全放松警惕,毕竟它只看了你喂给它的那一段。第三,留着债务清单。如果项目本身已经列入重构计划,那这些坑直接进重构需求文档,不用在旧代码上强行做完美主义。技术债只要记账清楚,就不算坏债;最怕的是既不知道有债,也不知道债在哪。

6.3 实操中遇到的三个典型问题速记

再记录三个实操中反复踩到的细节问题。第一个是同一段代码用AI跑两次,结果可能不一样,特别是模型版本滚动更新的情况下。我的处理方式是固定模型版本,如果条件不允许,就把上一次的输出作为参考一起喂进去,让AI在已有结论基础上做增删,而不是每次从零开始。第二个是提示词太长导致输出截断。解决办法是把“找问题”和“给建议”拆成两轮:第一轮只让它列出问题清单,第二轮再挑重点追问修复方案。这样每轮输出都短小精悍,不会生成到一半断掉。第三个是AI给的建议代码有时候根本编不过编译。我现在的策略是只让AI给“修改思路+核心片段”,不要大段整页代码,核心片段拿过来后自己补充完整,反而比全量照搬更靠谱。

回到开头那个问题:AI挑出20个坑,老炮只认15个,剩下5个是不是白挑了?我的看法是,不白挑。那5个误判帮我们重新确认了一遍项目边界:哪些能改、哪些不能改、哪些业务约束到今天依然生效。一次审查能同时拿到问题清单和项目认知,这笔投入就很值。下个季度我打算把AI审查推广到另外两个老模块,继续让机器和人互相校准,把代码债的下落彻底理清楚。如果你也在盘算类似的事,别犹豫,先拿一个模块试试,重点是记得把提示词里的“不要凑数”加上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询