有些规约你不当回事,直到它在凌晨三点把你叫醒。
说在前面
第一次看《阿里巴巴Java开发手册》是2018年,当时觉得"不就是一堆编码规范嘛,有啥好看的"。后来经历了几次生产事故,回头再翻这本手册,才发现每条规约背后都是真金白银换来的教训。
这篇文章不讲大道理,就聊聊过去几年我亲手踩过、或者亲眼看着同事踩过的几个坑。每一条都能在手册里找到对应规约,但手册不会告诉你------踩坑的那一刻有多疼。
1. 线程池别用Executors,这个坑我等了半年才填上
手册里白纸黑字写着:
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
刚入行的时候觉得这是小题大做。Executors.newFixedThreadPool(10)多清爽,非要写一大坨构造参数,不是炫技是什么?
直到有一天,线上服务半夜挂了。
那是一个做数据导出的功能,每次导出会创建一批子任务往线程池里丢。用的是Executors.newFixedThreadPool(10),底层队列是无界的LinkedBlockingQueue。
某次运营同学导了一个超大文件,任务生产的速率远超消费速率。队列里积压了几十万个任务,每个任务都占用一点内存,最后堆被撑爆了,Full GC频繁到服务完全不可用。
换成ThreadPoolExecutor之后,我指定了有界队列和拒绝策略。队列满了直接抛异常,至少服务不会挂,只是这次导出失败而已。
java
java
// 别这么写
ExecutorService pool = Executors.newFixedThreadPool(10);
// 改成这样,明确告诉自己和后来的人:队列多长、满了怎么办
ExecutorService pool = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
new ThreadPoolExecutor.CallerRunsPolicy()
);
手册里那句话我现在信了:让写代码的人明确线程池的运行规则。用Executors就是把风险藏在了"便捷"两个字后面。
2. BigDecimal除法不指定精度,线上金额直接算错
手册规约:
在使用BigDecimal的divide方法时,需要指定精度和舍入模式,否则在除不尽时会抛出ArithmeticException。
这个坑我倒是没踩,因为我在代码Review里堵住了不下十次。
很多人觉得用BigDecimal就是为了高精度,但写着写着就忘了。a.divide(b)这种写法在大多数情况下都能跑,直到某天分母是3、7这种数,控制台直接给你抛个异常。
更隐蔽的情况是:你本地测试用的数据都能整除,上线跑了两天才碰到一个除不尽的数据,这时候再出异常,定位问题还得先翻日志。
我现在的习惯是:只要是除法,闭着眼睛写divide(x, 2, RoundingMode.HALF_UP),金额相关的保留两位小数,百分比保留四位。多打几个字符的事,但能省掉半夜爬起来看日志的麻烦。
3. SimpleDateFormat要加锁,或者干脆别用
手册规约:
SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类。
这事我真见过有人"凭运气写代码"。
一个生成订单号的工具类,里面定义了个static的SimpleDateFormat,format当前时间戳拼到订单号后面。本地、测试环境跑了半个月一点事没有。
上线之后,偶尔会有两个订单号生成一模一样的。查了半天,发现是SimpleDateFormat的format方法内部用了一个Calendar对象,多线程并发调用的时候,一个线程刚把Calendar设成"2026-09-06",另一个线程立刻改成了"2026-09-07",两个线程拿到的结果串了。
解决方案其实很多:
java
arduino
// 方案1:每次new一个,牺牲点性能换安全
new SimpleDateFormat("yyyyMMdd").format(new Date());
// 方案2:用ThreadLocal包一下
private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL = ...;
// 方案3:直接用Java 8的DateTimeFormatter(推荐,线程安全)
DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDateTime.now());
手册没明说但潜台词是:SimpleDateFormat已经过时了,Java 8那套时间API它不香吗?
4. 循环里拼字符串,小心把GC累死
手册规约:
循环体内,字符串的连接方式,使用StringBuilder的append方法进行扩展。
这规约太基础了,Java开发第一天就知道String是不可变的。但知道归知道,真写起来一顺手就写成"+"了。
有次Review看到一段代码:
java
sql
String result = "";
for (Order order : orderList) {
result += order.getId() + ",";
}
几百条数据看不出问题,但orderList一旦上万,这段代码就会创建上万个StringBuilder对象(每次"+"在编译期会变成new StringBuilder().append())。虽然不是bug,但属于"能跑但没必要"的写法,白白增加GC压力。
顺手改成StringBuilder的事,让代码更体面一些。
5. 捕获异常却啥也不做,比不捕获更可怕
手册规约:
异常要么向上抛出,要么记录日志,切忌
e.printStackTrace()或者空catch块。
e.printStackTrace()的问题在于:线上日志里根本找不到这些堆栈,它们打到标准错误流里去了。排查问题的时候,日志里一片空白,谁都不知道发生了什么。
比这更糟的是空catch:
java
php
try {
// 业务逻辑
} catch (Exception e) {
// 什么也不做
}
这种代码你甚至没法骂,因为它"不影响业务"------业务该出错还是出错,但你看不到任何线索。就像家里漏水了,你把报警器关掉,然后假装一切正常。
我的习惯是:能抛就抛,别在底层把异常吞了。如果必须在catch里做处理,至少写一行log,哪怕只是log.warn("某个操作失败,原因:{}", e.getMessage()),也比啥都没有强。
6. 事务注解别乱加,加错地方会出事
手册规约:
事务注解
@Transactional应该放在public方法上,并且需要关注rollbackFor属性。
不展开讲原理,就说两个真实案例。
案例一:把@Transactional加在private方法上。Spring用的是动态代理,private方法压根代理不了,事务注解直接失效。代码跑着没问题,直到某次异常出现,发现数据没回滚,查了半天才发现注解形同虚设。
案例二:默认只对RuntimeException回滚。有人抛了个SQLException(它是Exception的子类,不是RuntimeException),结果事务提交了,数据写进去了,但后续逻辑失败了。手动设置rollbackFor = Exception.class可以解决。
java
java
@Transactional(rollbackFor = Exception.class) // 别偷懒,加上这个
public void doSomething() throws Exception {
// ...
}
7. POJO的布尔类型别叫isXXX,这坑藏得深
手册规约:
POJO类中的任何布尔类型的变量,都不要加is前缀,否则部分框架解析会引起序列化错误。
这条是我踩得最莫名其妙的一次。
定义了个实体类:
java
kotlin
private Boolean isDeleted; // 表示是否已删除
对应的getter是getIsDeleted(),setter是setIsDeleted(),看起来没毛病。
然后某天用FastJSON序列化,出来的是{"deleted": false},字段名少了"is"。接着前端反序列化的时候按isDeleted去取,取不到值,页面显示异常。
原因是某些序列化框架的"JavaBean规范"把is前缀处理掉了。后来乖乖改成deleted,啥事没有。
规约里的坑都是前人踩过的,没必要自己再踩一遍。
多说几句
《阿里巴巴Java开发手册》现在有PDF版、有IDE插件,甚至还有考试认证。但我觉得最有价值的不是背下所有规约,而是理解每条规约背后的事故场景。
如果你有时间,建议把手册里每条规约都问自己一个问题:"如果我不遵守这条,最坏会发生什么?"想明白了,自然就记住了。
最后,插件的自动检查开着挺好,但别完全依赖它。规则是死的,人是活的,有些代码风格层面的东西还是得靠团队内部的Code Review来兜底。
以上,共勉。