ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

阿里巴巴Java开发手册强制规约详解:从代码规范到生产防坑指南

阿里巴巴Java开发手册强制规约详解:从代码规范到生产防坑指南 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 isPaidMyBatis映射的时候结果集字段明明叫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的问题在于线程创建和销毁的代价高而且无法限制创建数量高并发下无限创建线程就会导致OOMOutOfMemoryError。我记得有一个真实案例某个服务的异常处理逻辑里手动new了线程去发送告警通知结果异常大量发生时线程数暴增直接把整个服务搞挂。线程池的意义不在于省那点创建线程的开销而在于线程数量是可控的队列是可控的拒绝策略是可控的说到底是一种资源保护机制。“SimpleDateFormat是线程不安全的类不要定义为static变量如果定义为static必须加锁或者使用DateUtils工具类。”这条我已经在多个项目里强调过无数遍了。SimpleDateFormat的线程不安全问题出在它的Calendar内部状态会被多线程修改。正解有三个用ThreadLocal每个线程持有一份SimpleDateFormat实例或者用DateTimeFormatterJava 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 tinyint1表示是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-pmdCI流水线里跑PMD检查强制级违规直接构建失败。在实际接入中PMD插件扫描出来的问题比IDEA插件要多一些比如数据库索引规范、命名规范这类静态检查都能覆盖到。所以建议两边都接入开发阶段用IDEA插件实时提示提交阶段用CI的PMD检查兜底。7.2 从“强制规约”到“团队共识”的转变有一条路我试过有效不要光贴规约条款而是每周挑一条强制规约让团队里一名成员准备一个十分钟的分享讲这条规约背后的真实案例或者代码陷阱。比如这周讲“SimpleDateFormat线程不安全”的时候可以让分享的同学现场写一个多线程调用SimpleDateFormat的demo跑出一次错误结果给大家看。有了直观的感受比在评审时说一百遍都管用。另外代码评审时的措辞也很关键。“你这代码不符合规范”这种话容易让人防御换成“如果这个类在高并发下被多个线程调用这里会不会出问题”把评语引导到“代码在特定条件下可能出错”的讨论上大家就会意识到强制规约保护的是自己。7.3 最终说点实际的善用官方资源现在阿里官方除了电子书和插件之外还有一份在线版的《Java开发手册》可以随时查阅而且每次版本更新都会在社区同步。我的习惯是每年安排一次集体学习把新版本和旧版本做diff重点看看有没有新增的强制条款。因为新增强制规约往往意味着阿里在真实生产环境里又踩了新坑这种经验别人花钱都买不到。工具链细节上如果用的是MyBatis还有一种选择是官方提供的MyBatis Generator配合官方代码风格模板生成出的实体类天然符合大部分强制规约能省不少人工调整的精力。这篇整理到这儿就结束了。最后再唠叨一句规约是死的业务是活的。遇到拿不准的场景强制规约不是限制你解决问题的枷锁而是帮你兜住底线的安全网。网破了可以补线上的数据出错了代价往往就没那么简单了。
返回列表