
2022年底我接手了公司一个跑了近十年的Java老项目的例行维护。项目不算大也不算小约32万行代码Java 7和Java 8混着写经历过好几轮团队交接注释一半过时一半缺失线上偶尔冒出点诡异问题但一直没人系统的梳理过代码质量。我当时没犹豫直接做了一次全量代码审查而且这次把AI代码审查也拉了进来想看看它到底能挑出多少东西。两条通道叠加之后AI一共标出了20个“坑”。等我逐条人工复核结合线上日志、压测数据、历史迭代记录挨个验证真正值得动手修的只有15个。这个“20比15”的差距很有意思——AI不是没用但它给出的结论不能无脑采纳。这篇文章我会把这20个坑一张一张摊开讲清楚为什么AI这么报、为什么我最后只认15个、另外5个又是凭什么被否决的。如果你手上也有一堆老代码正在纠结要不要上AI代码审查这篇至少能帮你少走几天的弯路。1. 为什么给老Java项目做AI代码审查这次体检不算顺先说背景。这个web项目的技术栈相当“经典”Struts时代的遗留逻辑和Spring Boot的新模块混搭数据库访问部分既有MyBatis也有原生JDBC还有一些定时任务裸跑在项目里。这类老项目有个共同点能跑、别动一改就出幺蛾子。但代码质量是真的差团队交给我的时候就一句话——“线上偶尔报错查不到原因”。我一开始的计划是先上SonarQube扫一遍毕竟静态分析工具在查“确定的坏味道”这件事上很擅长比如未关闭的流、魔法数字、循环复杂度过高这些它都能给出一份稳定的报告。扫完确实有收获但SonarQube有个明显的短板它追不了语义层面的问题。线程安全为什么出问题、事务边界为什么包错了、某个循环为什么会造成N1查询这些需要跨方法、跨模块理解业务逻辑的问题静态规则无能为力。所以我又加了一层基于大语言模型的AI审查。做法是写了个脚本把项目的关键类按依赖关系切片塞进带上下文的审查提示词里让AI按“并发”、“资源管理”、“SQL”、“异常处理”、“性能”几个维度输出报告。再加上一遍全仓提示的粗扫AI一共产出了20条带严重级别的建议。注意我这里说的是“建议”不是“结论”。因为AI的问题在于它太自信有时候它会根据断章取义的代码块得出一个逻辑自洽但不符合实际的判断。后面我会详细讲20条建议里至少有5条是这类“看似合理、实际跑偏”的东西。对这套组合拳我的结论是静态分析适合做第一层过滤AI适合做语义挖掘但最后的裁决权一定得交给那个真正理解项目上下文的人。没有人工复核的AI审查报告拿去给领导看可以拿去改线上代码不行。2. 20个坑是怎么挖出来的问题类型全拆解先给这20条建议分个类后面逐类展开。别小看分类分类本身就是人工复核的第一道关——同样一条建议放在并发场景和放在业务规则场景里权重完全不一样。2.1 线程与并发问题老项目翻车重灾区坑1SimpleDateFormat作为静态字段在多线程下共用。这个坑在Java 8以前太经典了。项目里有个DateUtils工具类把SimpleDateFormat定义成public static final所有时间格式化都走这一个实例。AI报这条的时候我都懒得细看直接认了。因为SimpleDateFormat内部的Calendar是共享可变状态并发调用format时会出现线程安全问题表现为时间错乱甚至数组越界。修复很简单要么每次new要么换成DateTimeFormatterJava 8要么用ThreadLocal包一层。老项目上我采用的是每次new改动最小。坑2HashMap在并发环境下被多个线程读改写。有一段用户会话管理代码直接用HashMap存在线状态多个线程同时读写。AI报的是“HashMap在多线程下可能造成死循环或数据丢失”这条也没争议。虽然Java 8之后的HashMap不再那么容易在put时触发环形链表死循环但数据不一致的问题依然存在。替换成ConcurrentHashMap即可属于低成本高收益的修改。坑3双重检查锁单例缺少volatile。项目里有一个配置中心客户端用了经典的双重检查锁写法但instance字段没加volatile。AI给出的解释很到位在JMMJava内存模型下对象的new操作不是原子的可能发生指令重排另一个线程读到半初始化的对象。这条我认但得说一句这种写法在Java 5之前加了volatile也没用Java 5之后JSR-133重新定义了volatile语义才真正可用。我们项目跑在Java 8上直接补volatile就完事。坑4线程池中ThreadLocal使用后未remove。这个比较隐蔽。项目里有个业务线程池任务内用ThreadLocal存用户上下文但任务结束没有调remove。AI标注的是“线程池复用时ThreadLocal数据串线且对象滞留可能引发内存增长”。仔细查了代码确实有一处任务分支直接return漏掉了finally块里本该执行的remove。修复方式是在run方法的finally里调remove或者在事务拦截器里统一清理。这条属于典型的AI能查出、人工容易漏的问题。坑5线程池execute()吞异常。项目里多处用ExecutorService提交任务用的都是execute()。问题在于execute()提交的任务如果抛出未捕获异常线程池的Future拿不到异常信息日志只会在后台线程里打一行极易被忽略。AI建议改成submit()并检查Future.get()或者给线程池设置自定义的UncaughtExceptionHandler。我认这条但修复时没有全局替换——submit()的返回值如果没人接收异常会被吞得更安静所以我们采用了给线程池统一加afterExecute钩子记日志的方案。坑6方法级synchronized粒度过大引发性能瓶颈。一个库存扣减服务整个public方法都加了synchronized内部还包含一次网络调用和一个本地缓存刷新导致并发量一上来锁等待严重。AI建议缩小锁范围到真正操作共享数据的代码段。这条问题本身我认但AI给的修复方案不能直接抄——它建议把synchronized拆到方法内两处关键语句上就行而实际上这两个语句之间必须保持原子性拆开反而破坏业务一致性。我的最终方案是把整个临界区包进一个独立的私有方法再对这个私有方法加锁既缩小了粒度又保住了原子性。2.2 资源管理与SQL问题漏掉一个就是线上事故坑7JDBC连接、Statement、ResultSet未关闭。老项目里直接用JDBC访问数据库的地方还有不少有一个报表模块每次查完数据都不关ResultSet和Statement更没把Connection还回连接池。AI报的是“资源泄漏”这条我没有半点犹豫直接认。结果就是连接池被占满高峰期报表一跑其他模块就拿不到连接线上卡死。修复方式所有手工JDBC块统一改成try-with-resourcesConnection交给连接池管理只关ResultSet和Statement即可。坑8循环内逐条查询数据库N1问题。有一个导出功能先从订单表查出1000条记录然后for循环里逐条去查明细表每个循环一次数据库交互。AI把这个标为高优先级理由是“往返次数过多DB压力大”。认必须认。这个倒不是AI多聪明而是这种问题在代码里实在太扎眼。修复方式是改成一次批量查询再把结果放到Map里内存映射循环内只做Map.get。优化后该导出接口耗时从8秒降到了1.2秒效果立竿见影。坑9大事务内包含远程调用和IO操作。一个同步接口在事务方法里既调了外部HTTP接口又做了文件解析整个事务从开始到提交可能持续好几秒。AI指出“事务内做外部交互会长时间占用数据库连接且外部调用失败会导致事务长时间不提交”。认。这个问题的修复比较重老项目没有现成的分布式事务方案所以我没有照AI建议强行拆事务而是做了两步把外部调用挪到事务提交之后用事务同步器注册回调文件解析改成事务外先做。这样一来事务窗口缩短到几十毫秒。坑10SQL字符串拼接导致注入风险。有个历史模块用的还是Statement来执行拼接出来的SQL参数直接拼字符串。AI报了SQL注入风险严重级别是critical。认没有讨价还价的余地。老项目里MyBatis的#{}已经普及了但这几个漏网之鱼还在。修复就是改成PreparedStatement或者条件允许的话直接改成MyBatis的XML查询。后来我也加了一条审查规则项目中禁止再用Statement执行动态SQL。*坑20select查询。有一条查询逻辑select *然后取其中两个字段用。AI建议显式列字段理由是减少无意义的数据传输、提升索引利用率。这条我认了但归为低优先级因为没有性能压力改起来语法风险也不大让开发顺手改掉就好。这里想说明一点AI报告中90%的select *建议确实值得跟但如果你表里有五十个字段而业务就要两个认如果查询结果被序列化到外部接口字段一个都不能少那select *反而是维护性的“方便”要不要改得结合消费者来判断。2.3 性能与异常问题隐蔽又致命坑11日志中直接打印大对象toString。有一段日志把整个订单对象连带嵌套的子对象列表全部打出来一个对象toString能产出几千行日志高并发下磁盘IO直接被打满。AI建议只打印订单ID、状态等关键字段。认。这问题定位起来特别快只要看线上日志文件的膨胀速度就行。修复后日志量降低了70%以上。坑12Pattern.compile写在方法体内。一个手机号校验工具每次调用方法都重新Pattern.compile接口QPS又高等于反复编译正则。AI建议把Pattern提升为static final。认。这条属于教科书级别的低级性能问题AI报出来后我直接批量改了项目里所有类似写法。坑13catch Exception后直接吞掉异常。这个才是最坑的。项目里有一段异步回调catch Exception后只写注释“忽略此异常”连log都没有。AI报的是“异常被吞故障无法追踪”。认。后来翻线上日志发现这个位置其实已经静默失败了大半年某个数据同步任务一直没生效。修复不是简单的加log还得把业务补偿机制补上。这里给个忠告AI报“吞异常”的时候别只想着加一行日志先想清楚这个异常是不是有意的——老项目里偶尔会出现“这里就是故意的”的注释需要跟前人确认。坑14用List.contains()在循环中做去重O(n^2)。有一段对账逻辑外层循环几千条内层用contains判断是否已存在最坏情况下要百万次比较。AI提示时间复杂度问题。认。修复就是把List换成HashSet代码改动只有两行性能提升却是数量级的。这条建议的价值在于提醒了审查者老项目里很多“能跑但很慢”的代码往往就是这种集合误用堆出来的。2.4 设计风格类AI最活跃也最容易跑偏坑15魔法数字。AI对项目里大量裸数字表达了不满比如状态值直接写1、2建议全部抽成常量。这条我拒绝了。原因是这些数字不是随手的魔法数字而是和历史业务规则强绑定的字典值代码里出现1就对应数据库字典表里的一条配置几个模块都依赖这个编码。硬抽常量反而会埋没这层“配置即历史”的信息。最好的做法不是改代码而是在关键位置加注释说明数字的业务含义。AI不理解业务它只看代码结构所以这一条被否决了。坑16死代码建议删除。AI报告里有一处“initXxx()方法从未被调用建议删除”。我看了下这是一个组件初始化方法在Spring的XML配置里写了init-methodinitXxx容器启动时通过反射调用所以在代码扫描层面看起来是“没有调用方”。要是照着删项目一启动就会报初始化失败。这直接印证了一个观点AI的静态视角永远拼不过框架运行时的真相。老项目里挂着绣花名头的“死代码”特别多删之前别怕麻烦先确认一下是不是被Spring反射、或者类似机制在远距离点名。坑17for-i循环建议改为增强for。AI对一段按索引遍历List的代码建议改成for-each理由是更简洁。我拒绝了。因为这段循环内部有一个if(condition) list.remove(i)的逻辑改成for-each后可能会触发ConcurrentModificationException或者出现删除元素后索引错乱导致的跳过问题。AI在简化代码时往往不在乎逻辑语义的细微差别这种建议简直是在给老项目“埋地雷”。改循环前必须想清楚这里为什么要用索引访问。坑18某个ThreadLocal持有大对象“内存泄漏”。AI在扫描时对一处置换告警说一个常驻业务线程的ThreadLocal里存了10MB数据怀疑内存泄漏。实际上这个ThreadLocal的清理动作在另一个WebRequest拦截器的finally块里AI只看到了存储端的代码没追踪到清理端属于典型的“跨模块上下文缺失”误报。这条被否决但倒是给我提了个醒后来我在提示词里专门加了“请追踪Try-Finally和拦截器链”让AI的跨文件分析再进一步。坑19建议为某个列表接口增加Redis缓存。AI看到这个接口的数据库查询逻辑比较重建议引入Redis缓存。我看了接口的调用数据调用量每天不到200次数据实时性要求秒级而且来源表还会被其他系统直接改。这种情况下加缓存收益微乎其微反而引入缓存与数据库的一致性维护负担还要考虑序列化兼容。这条属于AI的“伪优化”——从代码看确实有优化空间但从业务场景看完全不值得动。否决之后我留了个备注AI给性能建议时得给它喂真实流量和调用量数据否则就是纸上谈兵。3. 实操实录从AI报告到人工复核老炮的15个结论前面把20个坑逐个讲完了这一章重点说清楚我怎么从这20条里筛出15条以及5条否决建议的完整复盘逻辑。这套筛选流程对于任何想引入AI代码审查的团队都有直接参考意义。3.1 审查环境与工具组合先说说环境怎么搭的。我当时不是只用一个工具而是分了三层第一层是SonarQube扫全仓它的产出是“确定性问题清单”比如未关闭流、空指针风险、复杂度过高。这一层花费最少的时间因为规则稳定、误报率低交给它当保险。第二层是自建的LLM审查流水线。我把项目按模块拆解每个模块的文件清单、依赖关系和关键配置一起打包喂给审查提示词让AI输出结构化报告。提示词里明确要求按严重级别排序而且要输出“问题代码位置”、“问题描述”、“修复建议”三段式。第三层是人工复核也是花费时间最多的。我没法让AI直接跑到生产环境所以所有AI报告里的“危险”都需要人去验证。这套组合拳最大的好处是分工明确SonarQube抓语法和规范AI抓语义和场景人做终审。单靠任何一个效果都会大打折扣。尤其是LLM审查如果提示词里不给上下文它给出的建议会非常泛——什么“你应该考虑使用连接池”之类的正确的废话完全没法落地。3.2 人工复核的“三步筛选法”第一步是看是否真实存在。结合代码调用链、线上日志、监控曲线确认AI描述的问题是不是真的会触发。比如坑17的增强for建议我直接看了循环内部有没有remove操作有就说明AI的建议会引入新bug否。第二步是看修复价值。存在不代表要修。坑20的select *虽然不优雅但接口没有性能瓶颈改起来反而有可能引入兼容问题所以低优先级处理。另外修复成本也是重要考量老项目最忌讳大动干戈去重构一段能稳定运行的代码。第三步是看是否匹配业务约束。这一步是AI几乎做不好的。坑15的魔法数字、坑19的Redis缓存都是死于业务约束不明。AI不知道这些数字是字典表编码更不知道这个接口的数据实时性要求是秒级。最终认定20条建议里14条直接采纳1条坑6采纳问题但重写了修复方案总共15条进入施工清单。另外5条被明确否决并在代码评审记录里写明了否决理由这个是留给团队其他人看的免得以后有人再翻出来提。3.3 五个被否决的建议AI的自信撞上老炮的不屑这5条值得单独拉出来复盘因为它们是AI最自信、最容易被当“成果”上报的部分。否决一删除“死代码”。人家只是看起来死实际被Spring反射调用。这提醒所有搞AI审查的人框架的反射、SPI、字节码增强都会让“死代码”判断失真。AI的世界里不存在反射但老项目的世界里有。否决二魔法数字抽常量。否决不是说常量不好而是这些“魔法数字”本身就是和数据库字典表耦合的配置信息。把1改成STATUS_VALID并不能让代码更容易理解反而断了“这个1对应字典表哪条记录”的追溯链条。正确做法是加注释、建数据字典映射而不是动代码。否决三for-i改增强for。这个纯属AI不看循环体的后果。老项目里循环内删除列表元素的场景比比皆是这种改法带来的ConcurrentModificationException和数据跳项问题比代码不够“优雅”严重一百倍。否决四ThreadLocal内存泄漏误报。AI只看了ThreadLocal.set那一段没有去追踪清理逻辑所在的拦截器链。跨文件信息缺失是LLM类AI审查的典型天花板越是老项目跨模块的调用关系越隐晦AI越容易在这里翻车。否决五给低频接口加Redis缓存。AI看代码觉得慢但它看不到真实QPS和调用频率。给一天两百次、实时性要求秒级、且数据源随时会被外部系统修改的接口加缓存就是负优化。性能优化永远要拿数据说话AI给出的性能建议必须拿到真实运行数据复核。4. AI代码审查的常见误报和提升命中率的实操方法用AI做代码审查核心矛盾就一句话它能发现人容易漏的生成性问题但也会生成一堆看起来头头是道的错误结论。我个人测试下来误报率大概在15%到25%取决于代码库的老化和混乱程度。代码越乱AI的误报率越高因为它缺少足够的上下文来理解“为什么当年要这么写”。4.1 AI误报的五类典型场景我整理了一个对照表方便你遇到类似问题时快速判断要不要采纳AI的建议。误报场景AI典型表现人工复核要点框架反射调用报“方法未被调用建议删除”检查Spring init-method、ApplicationListener、SPI配置跨模块上下文丢失只看局部不看全链路报内存泄漏之类追踪finally、拦截器、AOP切面是否做了清理业务规则不理解把字典值、历史编码当魔法数字查数据库字典表、历史需求文档修复方案过于机械建议改语法规范却忽略语义副作用检查循环内删除、索引依赖、有序性需求性能建议脱离流量看到重查询就推荐缓存核对真实QPS、响应时间、数据一致性要求这五个场景在你给AI写提示词时如果能主动规避误报率会明显下降。代码审查这件事AI可以当“眼睛”但“脑”还是得长在人身上。4.2 给AI注入上下文让报告从“泛泛而谈”到“有的放矢”我第二次跑AI审查前专门优化了提示词效果非常明显。几个关键做法分享出来告诉AI这个项目的技术栈和框架版本比如“基于Spring Boot 1.5、MyBatis、JDK 7/8混用”让AI不要给出JDK 11才适用的建议。明确要求区分“技术问题”和“业务约束”比如加一句“如果怀疑魔法数字请先说明这里可能有哪些业务含义不要直接建议抽取常量”。要求AI对每条建议给出“如果不修复会引发什么线上症状”这个约束能逼它从“症状推导问题”而不是从代码表面自说自话。把项目的历史迭代信息摘要也喂给它一份比如“定时任务模块近期重构过”、“库存扣减逻辑是两年前加的”AI会在判断时更贴近真实情况。这些上下文注入操作不需要多复杂但需要你在跑之前认真准备材料。用一句话总结AI代码审查的质量约等于你给它提供上下文的质量。4.3 老项目AI审查要注意的几个实操细节别让AI直接改代码。这次实战里我全程只让AI给“建议”不给“补丁”。因为自动生成的补丁在老项目上很容易出现上下文依赖问题改完编译不过更别提和既有风格的契合度。给AI喂代码时要分层。全量塞进上下文窗口不现实按模块和依赖关系碎片化输入才是可行做法。代价是会丢失一部分跨模块信息所以最后一定要有人把AI的报告汇总起来做人工交叉验证。严重级别别照着AI的来。AI给Redis那条标的是“高优先级”我觉得是“不需要做”AI给死代码那条也标了“中优先级”我直接给了“不可用”。优先级必须结合线上影响和团队人力来定。给每条AI建议建立追踪记录。我建了一个简单的Excel表每行一条建议列字段包括AI建议内容、归类、采纳与否、理由、修复责任人、预计工时。这一步看着琐碎但在向团队和管理层汇报的时候这套记录的信任度比AI报告本身高太多了。5. 最后再聊两句AI代码审查到底值不值得用我的答案是值但前提是你把它当成“实习生侦探”而不是“资深专家”。它能帮你把三十万行代码里容易出问题的地方全翻一遍筛出一批名字响亮、逻辑清楚的问题线索效率确实比人肉扫代码高。但它不懂你的业务不懂你们的框架配置更不懂每个数字背后那段说不清道不明的历史。所以它会时不时给出一些特别自信、又特别离谱的建议比如让你删掉Spring正在反射调用的初始化方法。我个人的习惯是AI给的每一条建议都必须能回答清楚“这个问题会在什么场景下变成线上故障”才允许进入施工清单。回答不清楚的先挂着不急着改。代码审查这件事AI负责“量”人负责“质”两边结合好老项目也是能慢慢养回来的。如果你也准备给自己的老项目做一轮AI代码审查我建议从最痛的一个模块开始试别一上来就全仓跑。先把流程跑顺把误报过滤机制搭好再扩大范围。这个过程里最有价值的东西其实不是最后修掉的15个坑而是那5条被否决的建议——它们才是人比AI强的地方。