夜雨聆风学习资料网

ARTICLE · 1068488

AI 写的代码看着都对,我上线前必查这6处

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 的时候经常这么写:

@Service   public 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 4000020;  

看着正常。实际 MySQL 要先把前 40020 行读出来,扔掉前 40000 行,再返回 20 行。

每次回表 40000 次。表大、用户翻得深,数据库 CPU 就上去了。

分页有两种写法。

上一页、下一页这种,用游标:

SELECT * FROM orders WHERE id > #{lastId} ORDER BY id LIMIT 20;     

要支持跳页的,用延迟关联,先在内层用覆盖索引把主键捞出来:

SELECT o.* FROM orders o    INNER JOIN (        SELECT id FROM orders ORDER BY id LIMIT 4000020   ) 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 上那种。

#java #代码审查 #AI编程

相关学习资料