读《阿里巴巴Java开发手册》五年,这7条规约救了我太多次

有些规约你不当回事,直到它在凌晨三点把你叫醒。

说在前面

第一次看《阿里巴巴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来兜底。

以上,共勉。

相关推荐
凯哥Java1 小时前
System.setProperty 的正确姿势:Spring Boot 启动类里的“缺省值“魔法
java·spring boot·后端
lhldsg1 小时前
智慧场馆解决方案小程序系统:从架构设计到落地实践
java·小程序·需求分析
hfywmsj2 小时前
广州餐饮铺位招租的选址架构:流量入口与成本函数分析
java·大数据·jvm·广州餐饮铺位招租
爪哇岛国人2 小时前
原来我一直理解错了:实现接口真的必须实现所有方法吗?
java
万年咸鱼2 小时前
Java BufferedOutputStream 详解:原理、用法与性能优化
java·开发语言·性能优化
万年咸鱼2 小时前
Java BufferedInputStream 详解:原理、用法与实战
java·开发语言·python
~木雨2 小时前
Java 线程池七问七答:参数、执行流程、拒绝策略到 ThreadLocal 内存泄漏,面试必背
java·面试·线程池·threadlocal·threadpool·executor
艾莉丝努力练剑3 小时前
【AI大模型接入SDK】Gemini模型接入知识体系
java·开发语言·网络·人工智能·网络协议·学习·http
云运维笔记3 小时前
Zabbix 分布式监控搭建实战:基于 Proxy 实现 MySQL、Java、Nginx 监控
java·mysql·zabbix