ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI代码审查实战:Java老项目20个坑,老炮只认15个

AI代码审查实战:Java老项目20个坑,老炮只认15个 前几天我把公司一个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循环内逐条执行SQLN1查询P0性能/数据库4catch块吞掉异常无日志无上报P0故障不可见5大事务中同步调用外部接口P0数据一致性6静态List缓存无限增长P1内存泄漏7并发场景HashMap裸用P1线程安全8日志用字符串拼接而非占位符P1可观测性9多个写操作缺少事务注解P1数据一致性10正则表达式每次调用都重新编译P2性能11equals和hashCode行为不一致P2集合异常12StringBuffer滥用应使用StringBuilderP2性能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。这在订单、库存类业务里太常见了ListOrderItem 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块为空或只有注释”这种模式但只能靠人来拍板到底应该把异常抛出去还是记录日志后走降级分支。这个决定必须结合业务场景比如风控校验失败业务上是要阻断交易还是放行那得问产品但至少得让这个异常“发声”。第五个是大事务中调用外部RPCTransactional 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 MapString, 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)这类老APIAI建议统一改成更现代的写法。但项目目前跑在Java 8上升级JDK涉及基础镜像、编译插件、线上JVM参数、性能回归测试是一整套基建工程。更关键的是现有API用着并没有出过问题。为了“更规范”去升级整个运行环境属于主动放大风险老炮自然不会签字。第三个案例是AI标记了一个“从未被调用”的私有方法建议删除。实际上那个方法是被Spring的定时任务加反射机制调用的AI单看静态调用链根本不会发现。这提醒我们AI的判断基于统计规律和静态上下文遇到反射、SPI、字节码增强这类机制很容易看走眼。拿到“死代码”类建议时先全局搜一下符号再决定动不动手。第四个案例是AI建议把一段for循环过滤汇总改成Stream加collect。原代码本身没问题但循环里需要处理一个checked exceptionStream跟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审查推广到另外两个老模块继续让机器和人互相校准把代码债的下落彻底理清楚。如果你也在盘算类似的事别犹豫先拿一个模块试试重点是记得把提示词里的“不要凑数”加上。
返回列表