
说实话刚接到给2022年那个Java老项目做AI代码审查的任务时我是有点抵触的。项目不大但味道很冲Service类一个方法写七八十行异常捕获套了三层最后只打一行logger.error(e)类名从OrderServiceV1排到OrderServiceV4——这种代码靠人肉看一周不一定能看完看完也未必敢动。后来我用AI试了一轮20个坑摆在面前我过了一遍只认了15个。剩下5个不是AI瞎说但它确实没有足够上下文来理解这5个问题背后的历史原因。这篇文章不是想吹AI多神也不是想贬低工具。我就把这个项目怎么被AI扫出20个问题、我为什么只保留15个、以及这套流程还能怎么复制到别的老项目里原原本本讲一遍。如果你手里也压着一个能跑但没人敢动的遗留Java服务这篇应该能给你一点实操参考。1. 为什么偏偏拿AI去审一个2022年的Java老项目1.1 项目背景一个交付两年、换过三拨人的典型老项目先说清楚这个项目长什么样。Spring Boot 2.xJava 8MyBatis连MySQL订单模块为核心附带定时任务、Excel导出、第三方接口对接代码总量不算恐怖大概二十万行左右。2022年上线之后经历了两轮团队交接最早写核心逻辑的人已经不在现在的维护者基本都是通过日志认识业务的状态。这种项目有一个共同点线上能跑但没人敢保证它为什么能跑。每个类里都藏着几个历史补丁注释和代码甚至经常对不上。表面看没有明显故障但一问到这个订单状态流转到底有几个分支能说全的人就没有。我最初的想法是让团队里两个核心开发手动过一遍核心链路的代码。但他们一看到Service层三千行的类第一反应是这得排两周计划。后来我决定先让AI做一轮地毯式扫描把可量化的、规则明确的代码风险和隐患先捞上来人再针对结果做判断。这个分工后来被证明是对的——AI负责撒网人负责收网。1.2 AI不是来替代人力的是来给人力省时间的很多人一听AI代码审查就以为是把代码丢给ChatGPT让它从头看到尾。坦白讲直接这么做效果很差。大模型上下文有限二十万行代码根本塞不进去而且它不了解这个项目的业务语义。我更推荐的方式是把AI当做一个高配合度的初级审查员静态分析工具做全量扫描LLM做重点模块的定向抽查人做最终裁决。AI真正的价值是它不会累、不会烦、不会因为这块代码不是我写的就跳过。它能把最耗时间的通读环节压缩掉让有经验的人把精力集中在判断上。还有个好处是AI特别擅长做交叉验证。比如静态规则说某处有资源未关闭但只看单点可能误判如果让AI把打开资源的调用链全部列出来再和实际运行逻辑比对误报率能降不少。这就是我在这次实战里的核心思路工具之间互相印证而不是相信任何单一来源。2. AI一口气挑出20个坑我把清单原样贴在下面这一轮我用SonarQube、SpotBugs跑全量再用LLM定向review订单核心链路两边结果合并去重最后归纳出20个值得讨论的问题。为了让你有直观感受我先按严重程度分了三组。2.1 五个高危并发、事务、金额全是最容易出事的地方第一类问题集中在并发安全。代码里有一个静态的HashMap当缓存用多个线程会往里写订单快照。项目刚上线时可能没触发问题但一旦流量翻倍resize()阶段就可能出现死循环或者数据覆盖。AI给出的修复建议很简单换成ConcurrentHashMap如果度多写少再加volatile修饰引用。这类问题没有什么辩论空间属于典型该改的。第二类是SimpleDateFormat定义为 static 成员。它在多线程环境下不是线程安全的解析同一个时间字符串时不同线程可能互相踩坏内部状态导致日期解析错乱。项目里有几个定时任务都在用这个格式化器属于半夜最容易出事的类型。第三类是金额字段用 double 计算。订单金额、退款金额都涉及浮点运算0.1 0.2这种精度问题在金额场景下是不能接受的。现实中它不一定会立刻报错但它会在累计计算、四舍五入和比较大小的时候悄悄埋雷。第四类是Transactional自调用。有段代码在同一个类里一个方法直接调用了另一个带事务注解的方法事务并没有生效。AI把这个问题标成高危我认可它的观察但具体到这条代码结论我后面否掉了这在第3节会细说。第五类是定时任务没有加锁。这个项目部署了两台实例定时任务没有分布式锁也没有做for update之类数据库级别的互斥。如果任务执行时间超过触发间隔两个实例就可能同时跑轻则重复发通知重则生成重复对账单。2.2 六个中危异常与资源管理,线上事故的常客中危组里最常见的是异常被吞掉。我见到最多的写法是catch (Exception e) { // TODO }或者catch (Exception e) { return false; }日志里什么都没有。AI能识别出这种模式但真正有价值的判断是这个异常到底该不该往上抛吞掉之后是否有补偿逻辑这些问题还是要人工确认。资源释放问题也不少。旧代码里有一堆手写的Connection、Statement、ResultSetfinally块里只关了ConnectionStatement和ResultSet经常漏。虽然Java 8里连接关闭通常能连带释放但只能算是运气好不是设计好。循环内字符串拼接是静态检查必报的问题。日志参数、大批量报文拼接只要循环里用了就会反复创建StringBuilder。流量小的时候没什么感觉一旦单次任务处理上万条数据GC时间就会明显拉长。还有返回null代替Optional、日志中打印手机号和身份证号、Spring Bean循环依赖。这三个问题不一定马上炸但null容易造成NPE波动敏感信息会进日志系统循环依赖会让项目启动时出现警告、后续扩展时容易踩坑。我把它们归到中危是觉得它们不一定导致立刻挂了但确实是线上问题的高频来源。2.3 九个低危味道差、不好改但还能忍低危组就更有意思了上帝类、魔法值、硬编码内网IP、SELECT *、老旧的java.util.Date、过期TODO、重复代码块、超深嵌套、DCL单例没加volatile。这些问题的共同点是不影响我跑但影响我改属于典型的可维护性负债。我把AI挑出的20个问题整理成了一张表后面老炮复核时直接在表上做的判定编号问题描述AI原始判定最终结论1静态HashMap多线程并发高危真问题改2SimpleDateFormat静态共享高危真问题改3金额使用double计算高危真问题改4Transactional自调用失效高危误报暂不改5定时任务多实例无锁高危真问题改6异常被吞且无日志中危真问题改7Connection/Statement未释放中危真问题改8循环内字符串用拼接中危真问题改9方法返回null而非Optional中危真问题改10日志打印手机号等敏感字段中危真问题改11Spring Bean循环依赖中危真问题改12Service层上帝类三千行低危真问题逐步拆13魔法值直接散落业务代码低危真问题改14硬编码内网IP低危误报暂不改15SELECT * 查询全字段低危误报暂不改16日期类型仍用java.util.Date低危误报暂不改17代码里残留过期TODO低危真问题清掉18多处重复代码块低危误报暂不改19if嵌套超过5层低危真问题重构20DCL单例未使用volatile低危真问题改20个里最终认了15个另外5个被否决。否决不等于代码没问题而是在当前业务上下文里改动风险远大于收益。下面把这5个逐一拆开。3. 老炮只认15个被否决的5个坑为什么是误报3.1 误报一自调用导致Transactional失效——方法根本没走事务AI看到OrderService.update内部直接调用了同一个类的updateStatus而updateStatus上标着Transactional。按照Spring AOP的原理自调用不会经过代理对象所以事务注解确实无效。这个结论本身完全正确。但问题在于我看了一下updateStatus的完整实现里面只做了一次状态字段的update没有写其他表也不涉及多步修改。换句话说这条SQL本身就是一条原子操作不需要外层事务。就算Transactional没生效也不会出现改了一半的情况。这属于典型的规则发现了问题但没有发现这个问题的业务权重。如果这段代码将来扩展成两步更新那就必须处理事务。我会把它记在技术债清单里但不会排到当前迭代的修复计划中。3.2 误报二硬编码内网IP——AI不知道这是配置中心的兜底地址有一处代码直接写了类似http://10.10.12.34:8080/xxx的地址AI一看就说配置应外置不应该硬编码。乍一听很有道理但我去翻了配置仓库发现这是文件存储服务的地址正常情况应该从配置中心动态获取。这里之所以写死是因为当时客户环境不允许部署配置中心运维要求必须有一个兜底地址只有当配置中心完全不可用时才会走这个常量。这种上下文信息AI在静态代码里根本看不到。你要说它是坏味道我承认但你要说这是必须现在修的坑我不认同。改了反而可能破坏客户环境的特殊约定。正确做法是在常量旁边加注释说明何时会使用、为什么存在而不是贸然删除。3.3 误报三SELECT * 影响性能——这条SQL走了覆盖索引AI看到MyBatis里有一条SELECT * FROM order_item WHERE order_id ?提示不要无脑select *只查需要的字段。这是一条通用最佳实践但放到实际场景中要打问号。这张表一共十个字段左右业务上需要展示商品名、数量、单价、优惠明细几乎全字段都要组装到VO里。更重要的是我在执行计划里看到这条SQL已经命中了order_id对应的联合索引而且回表成本很低。改成只查特定字段省不了几个IO反而会让以后再新增字段时频繁改SQL维护成本上升。性能优化最忌讳的是抛开数据量谈 最佳实践。这张表当前单日数据量不过几万行全字段查询完全在可控范围。AI不认识这些业务背景自然会把不该被优化的地方当成优化点。3.4 误报四Date不换LocalDateTime——历史接口不能随便动Java 8之后的项目原则上新代码应该用LocalDateTime这是共识。但这个老项目不是从零开始的大量对外接口、数据库字段、DTO结构都基于java.util.Date。如果把内部流转改成LocalDateTime意味着所有getXxx()的返回类型都可能变化Jackson序列化格式也可能跟着变。前端如果依赖了/order/list返回的yyyy-MM-dd HH:mm:ss格式后端一旦改成LocalDateTime且没有统一配置格式可能直接输出一长串时间戳。这种问题比Date的旧API难查一百倍。所以我把这条也否了只要求新增代码不再使用Date存量代码等接口有机会升级时再一并处理。3.5 误报五重复代码块——长得像业务含义完全不同AI识别出三处重复的金额累加代码建议抽取公共方法。我逐段看了一遍发现它们长得是像但来源分别为用户主动下单、客服后台改单、定时任务自动退款。三处的金额计算规则各不相同有的是含运费有的是含优惠券分摊有的是不含任何附加费用。如果强行抽成一个公共方法势必要加一堆参数来区分行为最终公共方法的复杂程度可能比三处重复代码还高。这种情况下重复带来的改一处忘一处风险其实小于抽方法抽错逻辑的风险。我会在团队规范里注明只有行为语义完全一致的重复才抽取公共方法形似神不似的保持现状更好。3.6 AI误报的根因缺少上下文而不是模型不够聪明五个误报放到一起看原因非常清楚AI能看到代码结构但看不到业务历史、部署环境、数据规模、团队约束这些外部信息。它在做符合通用规范的审查而老炮在做符合这个项目生存状态的判断。所以不要因为误报就否定AI审查也不要把AI结论直接扔到Jira里当成任务指派。正确的态度是把AI当成唤起了讨论的队友它指出问题你来判断优先级、修复风险、改动边界。人机各干各擅长的事准确率才会真正上去。4. 从20个到15个的全过程我的AI审查实操路径这节把流程复现一遍方便你直接抄作业。我这里的三步走不是一次性设计的而是头两个项目试错试出来的。4.1 第一轮静态规则引擎跑面SonarQube SpotBugs第一步不是直接找AI而是把SonarQube搭起来拉上SpotBugs做全量扫描。为什么用两个因为它们的规则侧重点不同SonarQube偏代码质量和可维护性SpotBugs偏字节码层面的潜在缺陷两者有重叠也有互补。实际操作命令大致是这样的# 先编译项目并跳过测试确保字节码是最新的 mvn clean package -DskipTests # 跑SpotBugs mvn com.github.spotbugs:spotbugs-maven-plugin:4.7.3.1:check # 生成SpotBugs HTML报告 mvn com.github.spotbugs:spotbugs-maven-plugin:4.7.3.1:spotbugs -Dspotbugs.reportEncodingutf-8如果公司已经有SonarQube服务可以用mvn sonar:sonar \ -Dsonar.projectKeylegacy-order \ -Dsonar.host.urlhttp://your-sonar-server:9000 \ -Dsonar.loginyour_token这一轮会产出大量预警很多是格式问题、命名问题真正需要人盯的其实是高优先级和阻断级。我在这个项目里先不细看具体条目而是用它锁定哪些类/模块出问题最多再把那些类放进下一轮交给LLM做定向分析。4.2 第二轮LLM定向深挖线Prompt模板静态规则工具擅长找模式已知的问题但它看不懂业务含义。所以第二轮我挑出订单核心链路的十几个核心类分批次丢给LLM用的提示词不是帮我看看这段代码那太笼统了。我实际用的模板是这样的你是一个做过多年Java后端维护的资深工程师。下面我会贴出一段真实业务代码。 请只关注运行时风险并发安全、事务边界、资源释放、异常处理、性能隐患。 不要提代码风格问题也不要提命名规范。 对每个问题输出四行 - 位置类名/方法名/行号 - 为什么危险解释触发场景 - 修复建议给出最小改动方案 - 修复风险改动后可能影响什么业务 如果某条建议你觉得在缺少上下文时无法判断直接说存疑。模板里不要提代码风格问题这个限制很重要。如果你不限制LLM会把一半篇幅花在方法命名不够语义化这种废话上。限定运行时风险之后返回结果的质量会明显提升。4.3 第三轮人机交叉验证把误报率打下来两轮结果到手后我最重要的一步是交叉验证把SonarQube/SpotBugs扫描出的问题和LLM识别出的问题放在一起比对。两边都报的基本可以列为高置信度问题只有一边报的就要人工重点看。交叉验证不是机械地去重而是要关注同一个问题在不同表述下指的是同一处风险。比如Sonar报Remove this method call from a read-only methodLLM报这里幂等性设计有问题初看不是一条但它们背后都指向这个接口重复调用会重复扣款。把这些问题归并到一起才能真正评估修复优先级。这一轮同时也是老炮拍板的时刻。我在项目里组织了半小时评审会把20个问题念给两个核心开发听现场对照代码逐条确认。最终15个被认可、5个被否整个确认过程很快因为大家心里都有数AI只是帮我们把这些本来要花一天才能凑齐的问题一次性摆上了桌。4.4 一轮花费的时间和实际产出从跑工具到交叉验证再到评审会总耗时大约两个工作日。如果纯人工静态阅读想覆盖核心链路并给出可评估的问题清单我保守估计要一周到两周而且很可能漏掉几个并发类的隐患。产出是15个确认问题按P0/P1/P2排好了优先级每个问题都带修复建议和风险提示。这个产出已经可以直接进入排期。对一个小团队来说两个工作日换两周的人力消耗这个ROI是值得的。5. 落地这15个修复时的一些教训5.1 别让AI直接改代码让它给建议让老手定方案AI现在能生成修复代码而且看起来很像那么回事。但我这次吃过一次暗亏把一个double金额字段直接按AI建议改成BigDecimal后没有注意到实体类没写JsonFormat结果内部计算结果对了对外JSON却从数字变成了字符串对接方立刻报错。AI的修复建议只能当参考答案不能当最终补丁。它的优势在于提供最小改动方向和解释为什么这样改但具体落地的方案必须由熟悉业务的人确认。凡是涉及对外接口、字段类型、日期格式、数据库列的改动都得额外做兼容性测试。5.2 优先级怎么排按线上事故概率 x 修复成本算15个问题不能一窝蜂改完否则很容易引入新问题。我排优先级用的是很简单粗暴的公式事故概率乘以修复成本。P0是几乎必炸且修复便宜的问题金额用 double、定时任务没锁、Connection未关闭、异常吞掉无日志。这些我会要求在一个迭代内处理完。P1是可能在特定流量下炸但修复需要点功夫的问题并发HashMap、SimpleDateFormat、日志敏感字段、循环依赖。这类排到两个迭代内。P2是不炸但影响后续维护的问题上帝类、魔法值、嵌套过深、过期TODO、DCL单例。这类不需要停线穿插在需求迭代里逐步减量就行。5.3 如何避免下次再来一轮大扫除把规则固化进CI代码审查不能做成一年一次运动式检查否则很快又会长成一片森林。修复15个问题的同时我把SpotBugs和SonarQube扫描接进了CI流水线新代码如果触发了阻断级规则构建直接失败高优先级问题数量超标时MR不允许合并。另外在 .gitlab-ci.yml 里或者 Jenkins Pipeline 里加了一个 Job专门跑 SpotBugsstages: - code-review code-review: stage: code-review script: - mvn compile - mvn com.github.spotbugs:spotbugs-maven-plugin:4.7.3.1:check only: - merge_requests这样后续新代码会在一开始就受到约束老代码的历史债用技术债清单跟踪定期看是否有新增。规则内化到流程里之后AI大扫除的必要性就降下来了。5.4 AI代码审查的适用边界做了这个项目后我对AI代码审查的边界有了更明确的认识。它特别擅长的是静态规则、常见并发陷阱、资源管理、异常模式、统一编码规范这类有标准答案的内容。它不太擅长的是评估架构扩展性、判断业务规则是否合理、识别深层次的产品逻辑漏洞。所以别指望AI能替代资深开发做架构评审也别用它来评判这个方案该不该这么做。把它用在找出90%的常规问题把时间留给20%的关键判断这个位置上它就是好工具。反过来说如果团队里没有人能判断AI输出是否合理那一开始就不要大规模使用AI审查否则那几个误报就够团队忙一壶。最后再分享一点个人体会。AI代码审查对我来说不是“用工具替代人”的新玩具而是一种把人力从不值得花的通读成本里解放出来的手段。它给出20个坑我留下15个不是因为它造了5个多余条目而是因为它让我更快想起了那5个问题背后各自的来龙去脉。这正是老出活儿的地方工具把问题顶到台面上人用判断力决定哪些值得动手哪些值得再等等。