ARTICLE · 1068488
AI 写的代码看着都对,我上线前必查这6处
我现在大部分代码都是 AI 写的。
写得确实好。命名规范,注释齐全,空值判断顺手补上,异常也给你 catch 了。
但它有个特点:写得像真的。
会给你编一个不存在的配置项。比如 HikariCP 的最大连接数,正确的 key 是spring.datasource.hikari.maximum-pool-size,它给你写成max-active(那是 Druid 的写法)。这个 key 放配置文件里不报错,启动也不报错,就是不起作用。
会给你编一个不存在的方法。这种反而好,编译期就给你拦住了。
最麻烦的是第三种:代码是对的,逻辑是通的,编译能过,测试也能过。
上线一段时间才炸。
下面这六处,就是我每次 review 必看的地方。
一、循环里查数据库
AI 特别爱这么写:
List ids = orderMapper.selectPendingIds();for (Long id : ids) {Order order = orderMapper.selectById(id);result.add(convert(order));}
逻辑没问题,一条一条查,代码也读得懂。
问题在次数。1000 个 id 就是 1000 条 SQL。
每条 SQL 的开销不只是语句本身,还有一次网络往返。同机房大概 0.2 毫秒,跨机房 2 毫秒起步。1000 次就是 2 秒往上。
改成批量:
List orders = orderMapper.selectBatchIds(ids);
写 XML 就是 WHERE id IN (...)。列表特别长就分批,500 或 1000 一组。
怎么发现自己写了 N+1:把 MyBatis 的 SQL 日志打开,看控制台是不是同一张表刷了几百行。
二、@Transactional 没生效
这个坑最阴。代码里明明写着注解,看着有事务,其实没有。
AI 生成 Service 的时候经常这么写:
@Servicepublic class OrderService {public void createOrder(Order order) {// 一堆前置校验this.saveDetail(order); // 这一行,事务不会生效}@Transactional(rollbackFor = Exception.class)public void saveDetail(Order order) {// 写订单、写明细、扣库存}}
this.saveDetail(order) 走的是当前对象,不是 Spring 生成的代理对象。事务拦截器根本没被触发。
把 saveDetail 挪到另一个 Bean,或者从容器里捞自己再调,都能绕过去。最省事的是拆类。
除了自调用,还有三种:
方法不是 public。事务靠代理实现,非 public 方法代理不到。
异常被 catch 了。方法里 try { ... } catch (Exception e) { log.error(...); },异常没往外抛,Spring 认为执行成功,事务照常提交。
没写 rollbackFor。默认只回滚 RuntimeException 和 Error,受检异常不回滚。所以这个属性一定要显式写全。
三、深分页
SELECT * FROM orders ORDER BY id LIMIT 40000, 20;
看着正常。实际 MySQL 要先把前 40020 行读出来,扔掉前 40000 行,再返回 20 行。
每次回表 40000 次。表大、用户翻得深,数据库 CPU 就上去了。
分页有两种写法。
上一页、下一页这种,用游标:
SELECT * FROM orders WHERE id > #{lastId} ORDER BY id LIMIT 20;
要支持跳页的,用延迟关联,先在内层用覆盖索引把主键捞出来:
SELECT o.* FROM orders oINNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 40000, 20) t ON o.id = t.id;
内层只在索引上走,回表只有 20 次。
这两种写法记一句就够:别让数据库读完再扔掉。
四、CompletableFuture 没传线程池
CompletableFuture f = CompletableFuture.supplyAsync(() -> orderMapper.selectById(id));
不传第二个参数,它就用 ForkJoinPool.commonPool()。
公共池并行度是 CPU 核数减一。8 核机器就 7 个线程。
你这是查数据库,是 IO 任务,线程全在等网络。7 个线程占满,后面提交的直接排队。
更麻烦的是公共池全 JVM 共享,parallelStream() 用的也是它。异步任务把池占住,别处跟着一起卡。
传自己的池:
CompletableFuture.supplyAsync(() -> orderMapper.selectById(id), asyncPool);
IO 密集的池核心线程给到 16、32 都正常。配上线程名前缀,出问题一眼能定位是哪个池干的。
五、ConcurrentHashMap 先查后写
if (!cache.containsKey(key)) {cache.put(key, load(key));}
这是两步操作,中间别人能插进来。两个线程同时判断 containsKey,都为 false,就各加载一次。
改成原子的:
cache.computeIfAbsent(key, this::load);
一句额外提醒:load 里别放 RPC 或者慢查询。computeIfAbsent 会锁住这个 key 所在的桶,慢操作会拖住映射到同一个桶的其他 key。
六、BigDecimal 用 double 构造
BigDecimal a = new BigDecimal(0.1);System.out.println(a);
打印出来是 0.1000000000000000055511151231257827。
double 是二进制浮点数,0.1 存不下,只能存最接近的那个值。用这个构造器,等于把误差原样搬进了 BigDecimal。
用字符串,或者用 valueOf:
new BigDecimal(”0.1”);BigDecimal.valueOf(0.1); // 内部走 Double.toString(0.1)
两个都是 0.1。涉及金额的地方,这个必须改。差一分钱也是事故。
这六处都不是 AI 发明的新问题,是 Java 本身的老坑。
区别在数量。AI 一天能给你写两百个方法。踩这些坑的次数比你手写多得多,坑还每次都踩在同一个地方。
所以判断标准就一条:这段代码跑一万次的时候,还对不对。
单测跑一次通过,说明不了任何事。
上面这六处,你踩过几个?还有哪一处是你每次必查的?
留言区补上。攒够了我出一版完整清单,可以直接挂团队 wiki 上那种。