1. 为什么每个Java团队都需要一份“强制”规约
说到阿里巴巴代码规约,可能很多同学第一反应是那本《Java开发手册》电子书或者IDEA里的Alibaba Code Guide插件。确实,这套规约从2017年首次公开到现在,已经成了国内Java团队做代码规范绕不开的基准线。我团队在落地这套规约将近三年,最大的感受不是“多了条条框框”,而是代码评审的吵架次数明显变少了,线上故障里因为低级编码问题引发的基本绝迹。
这次专门把“强制”级别的规约挑出来整理,是因为这一档和其他“推荐”“参考”的性质完全不同。强制规约意味着如果你违反了,代码在外面是有可能直接出事的:要么并发环境下数据错乱,要么对象比较出现诡异结果,要么线上OOM。而推荐级别的顶多算“写得不优雅”,参考级别的更是纯粹的风格偏好。所以,强制规约才是保命条款,值得每一个写Java的人背下来。
这个整理适合谁看?我觉得覆盖三类人:刚入行的Java后端,你把它当避坑清单;三到五年的开发,你拿它对照自己的习惯,改掉那些“一直这么写也没出事”的侥幸心理;带团队的技术负责人,你可以直接拿这份清单去做代码评审的检查项,省得每次评审都要翻手册。
2. 强制规约整体架构与核心逻辑
2.1 从一份手册看它的设计思路
《阿里巴巴Java开发手册》把规约分成了编程规约、异常日志、单元测试、安全规约、MySQL数据库、工程结构、设计规约这七大板块。强制级别的条款均匀分布在各板块里,但仔细观察会发现,它其实遵循着一条隐藏的主线:先保证代码“不会错”,再谈“好不好看”。
比如在编程规约里,命名、常量定义、代码格式这些基础的强制条款,解决的是“代码能不能被人类正确理解”的问题。一个变量命名能让人产生歧义,后面接手的人改出一个bug,这在工业界太常见了。而集合处理、并发处理里的强制条款,解决的则是“代码在极端情况下能不能扛住”的问题。这是两个量级的事情。
我还注意到一个细节,MySQL数据库部分虽然标题是数据库规约,但里面的强制条款几乎全是关于建表规范和索引规约。为什么数据库部分的强制条款这么多?因为数据库层面的错误不像Java代码那样可以随时发布修复,改表结构、加索引这种操作在线上可能要经过审批窗口,而且数据量大以后,一次错误的全表扫描就能拖垮整个服务。所以数据库的强制规约,本质上是让你在写第一行SQL之前就避开那些高危操作。
2.2 强制条款的底层逻辑:违反的代价是什么
我整理这份规约的时候,有一个比较深刻的体会:每一条强制规约的背后,都对应着一个真实的线上事故或者至少是潜在的生产故障。
拿“POJO类布尔类型的变量都不要加is前缀”这一条来说,乍一看觉得多管闲事。但如果你定义一个private Boolean isDeleted,很多框架在做属性映射的时候,会把isDeleted解析成deleted字段,导致数据就是映射不上。这种问题排查起来极其恶心,因为不是报错,是静默失败。再比如“关于hashCode和equals的处理”,如果只重写equals不重写hashCode,在HashMap里存入对象后,查询时可能永远get不到,因为hashCode返回的不是同一个整数,HashMap根据hash定位桶位,直接定位到了另一个桶。
有意思的是,这些强制规约并不是某个理论上的最佳实践,而是被真实环境反复验证过的“不能碰的红线”。所以如果你想在团队里推动这套规约,最好的方式不是在评审时说“阿里说了不能这么写”,而是把违反这条规约会引发什么后果讲清楚。大家理解以后,遵守就不是负担,而是自我保护。
2.3 强制、推荐、参考三个级别怎么区分使用
我见过不少团队把整个手册打印出来,要求所有人从头到尾背,结果没两周就放弃了。其实正确的用法应该是分级别对待。
强制级别是代码合入的硬门槛,在代码评审阶段有一票否决权。我团队的做法是在CI流水线里接入了Alibaba Code Guide的静态检查插件,直接把强制级别的违规项设为build failure,代码不合标准根本跑不到评审环节。推荐级别是评审时建议修改的项,不阻塞合入,但会记在评审意见里。参考级别基本不强制,属于个人风格的自选动作。
这种分级处理的逻辑也很简单:强制级别的条款直接影响代码正确性和线上稳定性,必须靠工具去兜底,不能靠人的自觉。推荐级别的条款影响的是代码可维护性,可以通过评审慢慢养成习惯。参考级别的条款,说实话,不同团队对代码风格的偏好差异太大,强行统一反而会引起反感。
3. 强制规约核心细节拆解:命名、常量和代码风格
3.1 命名规约里那些容易被忽略的强制项
命名类的强制条款,大部分人都知道“类名使用UpperCamelCase,方法名和参数名使用lowerCamelCase”,但有几个隐藏条款是深度踩坑之后才真正理解的。
第一个是“POJO类中布尔类型的变量不要加is前缀”。这句话看着简单,落地时却很容易被忽略,因为很多人从大学写JavaBean的习惯里就带了这个毛病。我见过一个典型的例子:一个订单实体里定义了private Boolean isPaid,MyBatis映射的时候,结果集字段明明叫is_paid,但映射到实体上怎么都是null。排查了半天才发现是属性名以is开头,导致框架生成的setter方法名和数据库字段映射对不上。后来把属性改成paid,加上@TableField("is_paid")注解才解决。
第二个是“包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词”。这一条之所以强制,是因为大小写混乱的包名在跨平台环境里会引发类加载问题,尤其是Windows和Linux文件系统对大小写敏感度不同,可能导致开发环境正常运行,部署到服务器就报ClassNotFound异常。
第三个是关于“常量命名全部大写,单词间用下划线隔开”的强制条款,这个大多数人都知道,但“常量”的定义边界很多团队是模糊的。手册里明确说明,常量是指static final修饰的字段,但如果是static final修饰的引用类型,指向的对象内容变了不算常量变化,所以命名仍然用大写。这个细节没搞清楚的话,容易在评审时产生争议。
3.2 代码格式的强制项:为什么这类条款值得上升到强制
代码格式方面的强制条款,很多人觉得这不就是风格问题吗?其实不是。手册里强制规定的几个格式项,比如“大括号的使用约定:如果是大括号内为空,则简洁地写成{}即可,不需要换行”,还有“if/for/while/switch/do等保留字与括号之间都必须加空格”,这些条条框框表面上是在管格式,实际上是在降低代码的认知负担。
一个真实的体会是:强制代码格式的最大价值不在美观,而在diff的可读性。团队里每个人的格式习惯不同的时候,代码评审的diff里会混入大量无关的空白字符变动,reviewer很难看出真正的业务改动是什么。一旦格式统一了,diff就干净了,评审效率能翻一倍。
我团队在接入强制格式检查的时候,用的是IDEA的Eclipse Code Formatter插件,导入阿里规约的格式化模板,配合Save Actions在保存时自动格式化。刚推的两周确实有些人不适应,但坚持一个月以后,所有人的代码风格基本统一了。后面入职的新人,也不需要口头强调格式规范,格式化模板就是唯一的真相来源。
3.3 OOP规约里的几个高频踩坑点
OOP(面向对象编程)部分的强制条款,我觉得是整份规约里含金量最高的,因为它们直接跟JVM的对象机制挂钩,违反了以后产生的bug往往非常隐蔽。
“所有的覆写方法,必须加@Override注解”这条,很多人觉得是IDE自动生成的,加不加无所谓。但其实如果父类方法签名做了修改,子类方法没加@Override的话,就变成了一个新的重载方法,不再覆写父类方法了,编译期不会报任何错误,运行期行为却完全走样。加了@Override,编译器能立刻帮忙发现这个问题。
“Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals”这一条,我用一个案例来说明。假设有个订单状态比较的代码:order.getStatus().equals("PAID"),如果订单状态字段为null,这一行就会直接抛NullPointerException。正确写法是"PAID".equals(order.getStatus()),字符串常量调用equals,内部会处理传入参数为null的情况,返回false而不是抛异常。这个习惯养成了以后,空指针异常至少能少一半。
“关于基本数据类型与包装数据类型的使用标准”也是强制条款里的重点:所有的POJO类属性必须使用包装数据类型,RPC方法的返回值和参数必须使用包装数据类型,而所有的局部变量推荐使用基本数据类型。这条规约背后的逻辑是,POJO类属性如果是int,数据库查询结果为空时会映射成默认值0,和真实的业务值0混在一起分不清;用Integer的话,null就代表了“数据不存在”这个特殊含义。这个区别在报表统计里特别重要,0和null代表的业务含义完全不同。
4. 集合与并发:最容易出线上事故的强制条款
4.1 集合处理强制规约的实际应用
集合部分的强制条款,我挑几条在生产环境真正触发过问题的来说。
“关于hashCode和equals的处理,遵循如下约定:只要重写equals,就必须重写hashCode。”这条属于理论上的死规定,但实际中违反的人依然很多。我印象很深的一次故障是,一个缓存服务用自定义对象当作Map的key,这个对象只重写了equals没重写hashCode,从缓存里get数据的时候,明明equals是相等的,却因为hashCode不同,在HashMap里被定位到了不同的桶,结果缓存永远命中不了,后台数据库被查询请求打爆。因为分布式缓存通常会采用Hash取模分片,如果进行扩容,hashCode不一致还会导致数据迁移错乱。这已经不仅是性能问题了,是正确性问题。
“ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常”这一条,我确认过不少老手都踩过这个坑。subList返回的是一个内部类SubList,虽然继承自AbstractList,但它不是ArrayList。有些同学为了图方便,(ArrayList) list.subList(0, 5)这么一写,运行期直接异常。而且SubList是原列表的视图,修改子列表会影响到原列表,这种隐式的关联关系在大型代码库里很难追踪,容易引发诡异的并发修改。
还有“使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历”,这条不仅是性能问题,更隐蔽的是,如果遍历过程中需要修改Map结构,用keySet再get的方式在并发环境下会存在更多不确定性。entrySet直接拿到的是键值对,少了get的二次查找,在HashMap链表长度较长时性能差异会更明显。
4.2 并发处理强制规约的精髓
并发部分的强制条款,是整份规约里分量最重的,毕竟并发bug的排查成本是普通bug的十倍不止。
“获取单例对象需要保证线程安全,其中的方法也要保证线程安全。”这看起来像是废话,但实际中很多单例对象本身是用DCL(双重检查锁)模式创建了,但暴露给外面的方法内部却用了非线程安全的SimpleDateFormat或者HashMap。多线程环境下,SimpleDateFormat.parse会抛出NumberFormatException或者产生错乱的时间解析结果,这种bug只能在压测或者高流量时段才能暴露出来,排查难度极大。
“线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。”这一条是从资源管控角度定的铁律。手动new Thread的问题在于,线程创建和销毁的代价高,而且无法限制创建数量,高并发下无限创建线程就会导致OOM(OutOfMemoryError)。我记得有一个真实案例,某个服务的异常处理逻辑里手动new了线程去发送告警通知,结果异常大量发生时,线程数暴增直接把整个服务搞挂。线程池的意义不在于省那点创建线程的开销,而在于线程数量是可控的,队列是可控的,拒绝策略是可控的,说到底是一种资源保护机制。
“SimpleDateFormat是线程不安全的类,不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类。”这条我已经在多个项目里强调过无数遍了。SimpleDateFormat的线程不安全问题出在它的Calendar内部状态会被多线程修改。正解有三个:用ThreadLocal每个线程持有一份SimpleDateFormat实例,或者用DateTimeFormatter(Java 8以后它是线程安全的),或者直接用第三方的时间处理库比如Joda-Time。
“并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存层加锁,要么在数据库层使用乐观锁,使用version作为更新依据。”这一条在日常业务开发中是最常用的。最简单的落地方式是数据库表加version字段,更新时UPDATE table SET value = ?, version = version + 1 WHERE id = ? AND version = ?,受影响行数为0就说明数据被其他人改过了,可以选择重试或者提示用户操作失败。
4.3 控制语句和异常处理的强制逻辑
控制语句里的强制条款,比如“在一个switch块内,每个case要么通过break/return等来终止,要么注释说明程序将继续执行到下一个case,在一个switch块内,都必须包含一个default语句并且放在最后,即使它什么代码也没有。”这条看起来是基础中的基础,但实际评审时总能在老代码里找到漏写default或者漏写break的case。fall-through是Java里特别容易让人困惑的行为,如果一段代码从case A直接滑到case B,阅读者会认为A条件下也会执行B的逻辑,即使实际上编写者的本意可能不是这样。
“在高并发场景中,避免使用等于(==)判断作为中断或退出的条件。”这一条很有意思,它说的是比如用if (count == 100)去判断是否可以退出的逻辑,在高并发下由于线程调度的不确定性,count可能从99直接跳到101,等于100的条件永远不满足,代码就卡住了。正确的方式是使用if (count >= 100)这种范围判断。
异常处理方面的强制条款里,“不要捕获异常后不处理,但也不要在catch块中做太多复杂逻辑”这算是一个方向性的约束。最典型的问题是吞异常,catch (Exception e) { }里面什么都不写,出问题的时候日志里一片空白,排查完全无从下手。有经验的团队会要求catch块至少打印一条error级别的日志,包含异常的堆栈信息。另外,“finally块必须对资源对象、流对象进行关闭”现在基本被try-with-resources替代了,新代码里如果看到老式的手动关闭流的写法,建议直接改成try-with-resources,代码更简洁而且不会漏关。
5. 数据库与ORM:强制规约里的隐藏重灾区
5.1 建表规范里的强制项为什么重要
数据库部分的强制规约,一般不会出现在新手的视野里,因为多数人刚开始接触的是简单的CRUD,建表这件事往往是架构师或者DBA在做。但如果你的工作职责范围慢慢扩大到领域设计,建表规范就是你绕不开的必修课。
“表达是否概念的字段,必须使用is_xxx的方式命名,数据类型是unsigned tinyint(1表示是,0表示否)。”这一条和前面JavaPOJO属性不要用is前缀互相呼应,很多人觉得矛盾,其实不矛盾:数据库字段用is_xxx命名是为了数据库层面的语义清晰,Java属性不用is前缀是为了框架映射的兼容性,中间通过ORM框架进行字段映射转换就可以了。这种细节如果不注意,很容易让团队内部争吵,但背后的逻辑其实是不同层面有不同的约束。
“表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。”数据库字段名的大小写敏感度在Linux上和Windows上完全不一样,MySQL在Linux下默认对表名区分大小写,如果建表时用了大写字母,换环境部署时就会出现诡异的表找不到错误。所以统一小写,禁止数字开头这些约定,本质上不是为了“好看”,而是为了跨环境的一致性。
“小数类型为decimal,禁止使用float和double。”这条值得每一个写过金额计算的开发者刻在脑子里。float和double是二进制浮点数,在表示0.1这种十进制小数时存在精度损失。如果你用float存金额,算完账之后对不上账,不是业务逻辑写错了,是数据类型选错了。decimal在MySQL里是定点数存储,精度可控。对应到Java里,金额字段要用BigDecimal接收,计算时用BigDecimal进行运算,这是一个完整的链路。
5.2 索引规约的强制项:让SQL查询回归正轨
索引部分的强制条款,我认为是数据库规约里最有实际意义的,因为索引是影响查询性能最核心的因素。
“业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。”这条解释得很到位:不要以为应用层做了校验就万事大吉,应用层的校验在并发下存在时间窗口,两个请求同时查到“没有同名学生”,然后同时插入,最后数据库里就真的出现同名学生了。唯一索引才是最后一道防线,虽然会影响一点插入性能,但比起数据脏掉来说完全值得。
“超过三个表禁止join。需要join的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引。”这一条与其说是强制规范,不如说是对系统架构的倒逼:当你会发现单表查询不好写、多表join又受限的时候,自然的解法就是做数据冗余或者设计宽表,把关联查询变成单表查询。这在互联网高并发场景下是非常经典的反范式设计思路。
“在varchar字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。”这条对超长文本字段尤其重要,比如一个存url的varchar(255)字段,如果对整个字段建索引,索引体积会很大,而且实际区分度可能只需要前面一小部分就够了。指定索引长度既能省空间,又能让查询效率更高。这里有个细节需要说明:索引长度的确定需要对实际数据做区分度测试,你可以用SELECT COUNT(DISTINCT LEFT(column, N))来测试不同前缀长度的区分度,找一个既能保留区分度又不太长的N值,而不是拍脑袋定一个长度。
5.3 ORM映射里的强制规约
ORM部分的强制条款,主要约束的是SQL语句和实体映射的两个关键环节。
“在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。”手动指定字段有几层考虑:一是明确字段在减少网络传输量,二是避免将来表结构发生变化,导致查出来多出字段影响代码逻辑,三是能用上覆盖索引优化查询性能。在MyBatis里尤其要注意,用select *还会把一些大文本字段也查出来,在列表页这种场景下就是纯粹的性能浪费。
“POJO类的布尔属性不能加is,而数据库字段必须加is_,在resultMap中进行映射。”这个我在前面已经提到过,但这里从ORM视角再补充一下:MyBatis的自动映射是依赖属性名和列名对应关系的,Boolean paid对应is_paid字段,默认情况下MyBatis是不能自动识别这种“is_前缀+属性名”的映射,需要显式配resultMap或者写别名,否则就会出现查出来全是null的情况。
“不要用resultClass当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义;反过来,每一个表也必然有一个POJO对应。”这一条在管理严格的团队里几乎是一票否决的。如果查询结果直接返回Map,代码里用key去取值,数据库字段一改,代码编译期完全发现不了问题,运行期才会报空指针,这种隐性故障非常危险。有明确的POJO对象,至少还有编译器和IDE的自动检查帮你兜底。
6. 工程结构与其他强制规约:容易被忽略的细节
6.1 工程分层与应用分层
“采用经典三层架构:表示层、业务逻辑层、数据访问层。”这一条在阿里规约里是工程结构板块的核心强制项。虽然现在微服务架构非常流行,但微服务内部的代码结构依然遵循着三层架构的思路:Controller层负责参数接收和响应封装,Service层处理业务逻辑,Mapper/Dao层负责数据持久化。
这个分层的价值,往浅了说是“各司其职”,往深了说是“依赖方向可控”。如果Controller里直接写了SQL查询逻辑,或者Service里拼了一堆JSON响应结构,短期内看起来“高效”,长期维护下来就是灾难。后面任何一层的需求变更,都会导致代码在多处被改动,且难以定位。我见过最崩塌的例子是一个项目里Controller层达到两千行,里面混着数据校验、缓存操作、发MQ消息、拼报表数据,后来的维护者基本是提心吊胆地改代码。
关于分层,可疑的是不少人问“那工具类放哪一层”,我的经验是独立的common模块或者util包,不归属任何一层,但依赖方向只能是被各层引用,不能反向引用业务代码。
6.2 异常日志的强制规约
“应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架(SLF4J、JCL)中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。”这一条在技术选型时就要确定,如果项目中直接用了Logback的Logger,后续想换成Log4j2或者其他实现,就得把所有文件的import和API调用全部改一遍。用SLF4J作为门面,底层实现随时可以替换。这是工程上的依赖倒置原则在日志模块的具体体现。
“异常不要用来做流程控制,条件控制不要使用异常处理。”这是从编码习惯上纠正一个很常见的反模式。有些人喜欢在业务代码里try-catch一个自定义异常来模拟return,这种做法会破坏代码的阅读流畅性,而且异常处理的开销远大于正常的条件判断。正确的做法是:异常只处理真正的异常情况,业务分支用if-else处理。
6.3 其他强制条款的补充说明
“在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。”这条很多人没注意,在Java中Pattern.compile是一个相对重的操作,如果放在方法内每次调用都编译一次,在高频路径上会浪费大量CPU。正解是把Pattern定义成static final常量,只编译一次,每个线程共用,内部匹配是线程安全的。
关于强制规约的数量,我看到网上有各种统计,有的说90多条,有的说110多条。这里我不做精确统计,因为版本迭代中条款数量会变,而且不同统计口径不同。我更想强调的是:不要去背条款,而是去理解条款背后的风险模型。《Java开发手册》的每次更新,其实都在告诉我们,Java这个生态里哪些老问题又找到新的解决办法了。比如Java新版本引入了var、record、sealed class这些新特性,规矩也会随之演进。
7. 我在实际落地过程中的经验与避坑心得
7.1 团队推广的节奏比内容更重要
如果你们团队打算把阿里规约的强制条款作为标准,我强烈建议不要搞“运动式”的强制执行。第一天贴公告,第二天开始检查老代码,要求所有人修改历史遗留问题,这种搞法一定会激起逆反心理。我当初的做法是:新代码强制遵守,存量代码慢慢消化,每次遇到需要修改的文件就顺手把相关规约问题一起处理掉,相当于把“治理债”摊平到日常开发中。半年以后,存量代码里大部分强制违规项就被自然清理得差不多了。
配套一定要上工具。Java项目的组合是IDEA插件Alibaba Java Coding Guidelines加Maven插件alibaba-p3c-pmd,CI流水线里跑PMD检查,强制级违规直接构建失败。在实际接入中,PMD插件扫描出来的问题比IDEA插件要多一些,比如数据库索引规范、命名规范这类静态检查都能覆盖到。所以建议两边都接入,开发阶段用IDEA插件实时提示,提交阶段用CI的PMD检查兜底。
7.2 从“强制规约”到“团队共识”的转变
有一条路我试过有效:不要光贴规约条款,而是每周挑一条强制规约,让团队里一名成员准备一个十分钟的分享,讲这条规约背后的真实案例或者代码陷阱。比如这周讲“SimpleDateFormat线程不安全”的时候,可以让分享的同学现场写一个多线程调用SimpleDateFormat的demo,跑出一次错误结果给大家看。有了直观的感受,比在评审时说一百遍都管用。
另外,代码评审时的措辞也很关键。“你这代码不符合规范”这种话容易让人防御;换成“如果这个类在高并发下被多个线程调用,这里会不会出问题”,把评语引导到“代码在特定条件下可能出错”的讨论上,大家就会意识到强制规约保护的是自己。
7.3 最终说点实际的:善用官方资源
现在阿里官方除了电子书和插件之外,还有一份在线版的《Java开发手册》可以随时查阅,而且每次版本更新都会在社区同步。我的习惯是每年安排一次集体学习,把新版本和旧版本做diff,重点看看有没有新增的强制条款。因为新增强制规约往往意味着阿里在真实生产环境里又踩了新坑,这种经验别人花钱都买不到。工具链细节上,如果用的是MyBatis还有一种选择,是官方提供的MyBatis Generator配合官方代码风格模板,生成出的实体类天然符合大部分强制规约,能省不少人工调整的精力。
这篇整理到这儿就结束了。最后再唠叨一句,规约是死的,业务是活的。遇到拿不准的场景,强制规约不是限制你解决问题的枷锁,而是帮你兜住底线的安全网。网破了可以补,线上的数据出错了,代价往往就没那么简单了。