《阿里巴巴 Java 开发手册》读书笔记
偶然看到阿里巴巴居然出书了???趁着满减活动(节约节约....)我赶紧买来准备看看,刚拿到的时候掂量了好多下,总觉得商家给我少发了一本书,结果打开才知道..原来这本书这么小....
编码规范的重要性
别人都说我们是搬砖的码农,但我们知道自己是追求个性的艺术家。也许我们不会过多在意自己的外表和穿着,但在我们不羁的外表下,骨子里追求着代码的美、系统的美、设计的美,代码规范其实就是一个对程序美的定义。—— 引自 序
如果有一天在我们的项目中看到了这样的代码:
或者是这样的代码:
这样美不美呢?或许看着是还挺美的,但是如果需要修改,是不是人傻啦?
那这样的代码呢?
作为一个对自己有一定要求的程序猿,是不是第一反应就是:
- 重写!
- 原作者是谁?锤他!
规范不一,就会像下图中的小鸭和小鸡对话一样,语言不通,一脸囧相。鸡同鸭讲也恰恰形容了人与人之间沟通的痛点,自说自话,无法达成一致意见。再举一个生活中的例子,交通规则靠左行驶还是靠右行驶,两者孰好孰坏并不重要,重要的是必须要在统一的方向上通行,表面上限制了自由,但实际上是保障了公众的人身安全。试想,如果没有规定靠右行驶,那样的路况肯定拥堵不堪,险象环生。同样,过分自由随意、天马行空的代码会严重的伤害系统的健康,影响到可扩展性以及可维护性。
- 总结:代码规范很重要!
关于编码规范的三大圣战
众所周知,互联网公司的优势在于效率,它是企业核心竞争力。体现在产品开发领域,就是够沟通效率和研发效率。对于沟通效率的重要性,可以从程序猿三大 “编码理念之争” 说起:
- 缩进采用空格键,还是 Tab 键
- if 单行语句需要大括号还是不需要大括号
- 左大括号不换行,还是单独另起一行
在美剧《硅谷》中,有这样的一个经典镜头:
- 程序媛:Kid? 我们似乎很久没有一起睡了。
- 程序猿:现在?不可能!我永远不会和使用空格来缩进的人睡在一起!
- 程序媛:(疯狂敲 space 气走了程序猿)
- 程序猿:(甩了一句)一个 Tab 可以代替 8个 空格!
之后程序猿就因为视图一步跨下八个阶梯而摔了....
Tab 键和空格键的争议确实存在,并且在知乎上讨论得火热:写代码时,缩进使用 tab 还是空格?
- 总结:使用 4 个空格好,在《阿里巴巴 Java 开发手册》中也明确支持了这样的做法。下面也引用一张图来调侃一下。
if 单语句是否需要换行,也是争论不休的话题。相对来说,写过格式缩进类编程语言的开发者, 更加习惯于不加大括号。《手册》中明确 if/for 单行语句必须加大括号,因为单行语句的写法,容易在添加逻辑时引起视觉上的错误判断。此外,if 不加大括号还会有局部变量作用域的问题。
左大括号是否单独另起一行?因为 Go 语言的强制不换行,在这点上,“编程理念之争” 的硝烟味似乎没有那么浓。如果一定要给一个理由,那么换行的代码可以增加一行,对于按代码行数考核工作量的公司员工,肯定倾向于左大括号前换行。《手册》明确左大括号不换行!
- 总结: 其实,很多编程方式客观上没有对错之分,一致性很重要,可读性很重要,团队沟通效率很重要。
第1章:编程规约
这一章是对传统意义上的代码规范,包括变量命名、代码风格、控制语句、代码注释等基本的变成习惯,以及从高并发场景中提炼出来的集合处理技巧与并发多线程的注意事项。
1.1 命名风格
第一条:【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
- 反例:
_name
/$name
/name_
/name$
尽管 $
可以作为标识符使用,然而我们应该尽量避免对其使用。
- 原因:
$
通常在编译器生成的标识符名称中使用,如果我们也使用这个符号,可能会有一些意想不到的错误发生.... - 意想不到的错误示例:
package test;
public class User$VIP {
public static void main(String[] args) {
User user = new User();
User.VIP vip = user.new VIP();
vip.print();
}
}
class User{
class VIP{
void print(){
System.out.println("成员类");
}
}
}
仔细阅读以下,似乎并没有什么问题,代码也比较简单,但正在我们编译的时候,IDEA提示我们:
定义了重复的代码?归根到底,都是 $
惹的祸!因为 $
被编译器所使用,在源文件(.java 文件)编译成字节码(.class 文件)后,会称为顶层类型与嵌套类型之间的连接符。例如,如果存在一个顶层类 A,在其内声明了一个成员类 B,那么编译之后就会产生两个 class 文件,分别为 A.class
与 A$B.class
。
就本程序来说,会生成 3 个 class 文件(如果可以编译的话),分别是 User$VIP.class
(顶层类)、User.class
与 User$VIP.class
(User 类的成员类,也就是类 VIP)。由于试图存在两个 User$VIP.class
所以才会报错!
第三至第六条:【强制】
- 类名使用 UpperCamelCase 风格,方法名、参数名、成员变量、局部变量都同意使用 lowerCamelCase 风格,必须遵从驼峰形式。
- 变量命名全部大写,单词兼用下划线隔开,力求予以表达完整清楚,不要嫌名字太长。
正例:MAX_STOCK_COUNT / PRIZE_NUMBER_EVERYDAY
反例:MAX_COUNT / PRIZE_NUMBER
- 抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类命名以它要测试的类名开始,以 Test 结尾。
第八条:【强制】 POJO 类中布尔类型的变量都不要加 is 前缀,否则部分框架解析会引起序列化错误。
反例:定义为基本数据类型
Boolen isDeleted;
的属性,它的方法名称也是isDeleted()
,RPC 框架在反向解析的时候,“误以为” 对应的属性名称是deleted
,导致属性获取不到抛出异常。
第十二条:【推荐】 如果模块、类、方法使用了设计模式,应在命名时体现出具体模式
- 说明: 将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。
正例:
public class OrderFactory;
public class LoginProxy;
public class ResourceObserver;
第十三条:【推荐】 接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的间接性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,必须是与接口方法相关的,并且是整个应用的基础变量。
正例:
接口方法签名:void commit();
接口基础变量:String COMPANY = "alibaba";
反例:
接口定义方法:public abstract void commit();
- 说明: 如果 JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默认实现。
第十四条:接口和实现类的命名规则
- 1):【强制】 对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 后缀与接口区别。
正例: CacheServiceImpl 实现 CacheServcie 接口
- 2):【推荐】 如果是形容能力的接口名称,取对应的形容词为接口名(通常是 -able 的形式)。
正例: AbstractTranslator 实现 Translatable。
1.2 常量定义
第二条:【强制】 long 或者 Long 初始赋值时,使用大写的 L,不能是小写的 l。小写的 l 容易跟数字 1 混淆,造成误解。
- 说明:
Long a = 2l;
写得是数字的 21 还是 Long 型的 2?
第三条:【推荐】 不要使用一个常量类维护所有变量,要按常量功能进行归类,分开维护。
- 说明: 大而全的变量类,非得使用查找功能才能定位到修改的常量,不利于理解和维护。
正例:缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在 ConfigConsts 下。
1.3 代码格式
public static void main(String[] args){
// 注释的双斜线与注释内容之间有且仅有一个空格
// 缩进 4 个空格
String say = "hello";
// 运算符的左右必须有 1 个空格
int flag = 0;
// 关键字 if 与括号之间必须有 1 个空格,括号内的 f与左括号、
// 0 与右括号之间不需要空格
if (flag == 0) {
System.out.println(say);
}
// 左大括号前加空格且不换行;左大括号后换行
if (flag == 1) {
System.out.println("world");
// 右大括号前换行,右大括号后有 else,不用换行
} else {
System.out.println("ok");
// 在右大括号后直接结束,则必须换行
}
}
第八条:【强制】 方法参数在定义和传入时,多个参数逗号后边必须加空格。
正例:下例中实参的“one”,后边必须要有一个空格。
method("one", "two", "three");
1.4 OOP 规约
第二条:【强制】 所有的复写方法,必须加 @Override 注解。
- 说明: getObject() 与 get0bject() 的问题。一个是字母 O,一个是数字 0,
加 @Override 注解可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。
第七条:【强制】 所有相同类型的包装类对象之间值得比较,全部使用 equals 方法
- 说明: 对于
Intergre var = ?
在 -128~127 范围内的赋值, Integer 对象是在 IntegerCache.cache 中产生的,会复用已有的对象,这个区间内的 Integer 值可以直接使用 == 进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象。这是一个大坑,推荐使用 equals 方法进行判断。
第十二条:【强制】 POJO 类必须写 toString 方法。在使用 IDE 中的工具 source>generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。
- 说明: 在方法执行抛出异常时,可以直接调用 POJO 的 toString() 方法打印其属性值,便于排查问题。
1.5 集合处理
第七条:【强制】 不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
// 正例
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
// 反例
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
// 如果把 1 改为 2 再试一下看看是否相同
if ("1".equals(item)) {
list.remove(item);
}
}
第十一条:【推荐】 高度注意 Map 类集合 K/V 能不能存储 null 值得情况
1.6 并发处理
第三条:【强制】 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
- 说明: 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源,解决资源不足的问题。如果不适用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者 “过渡切换” 的问题。
1.7 控制语句
第二条:【强制】 在 if / else / for / while / do 语句中,必须使用大括号。即使只有一行代码,也应该避免采用单行的编码方式:if (condition) statements;
第三条:【强制】 在高并发场景中,避免使用 “等于” 判断作为终端或退出的条件
- 说明: 如果并发控制没有处理好,容易产生等值判断被 “击穿” 的情况,应使用大于或小于的区间判断条件来代替。
反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止。
第四条:【推荐】 在表达异常的分支时,尽量少用 if-else 方式
- 说明: 如果不得不使用 if()...else if()...else... 方式表达逻辑,【强制】 避免后续代码维护困难,请勿超过 3 层。
// 正例:超过 3 层的 if-else 逻辑判断代码可以使用卫语句、策略模式
// 状态模式等来实现,其中卫语句实例如下:
public void today() {
if (isBusy()) {
System.out.println("change time,");
return;
}
if (isFree()) {
System.out.println("go to travel.");
return;
}
System.out.println("stay at home to learn Java");
return;
}
1.8 注释规约
第一条:【强制】 类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /**内容*/
格式,不得使用 //xxx
方式
第二条:【强制】 所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释,除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
- 说明: 对子类的实现要求,或者调用注意事项,请一并说明。
第三条:【强制】 所有的类都必须添加创建者和创建日期。
1.9 其他
第三条:【强制】 后台输送给网页的变量必须加$!{var}——中间是感叹号
- 说明: 如果 var=null 或者不存在,那么 ${var} 会直接显示在页面上。
第四条:【强制】 注意 Math.random() 这个方法返回的是 double 类型,取值的范围 0≤x<1(能够取到零值,注意除零异常),如果向获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法。
第六条:【推荐】 不要在视图模板中加入任何复杂的逻辑。
- 说明: 根据 MVC 理论,视图的职责是展示,不要抢模型和控制器的工作。
第4章:安全规约
“安全生产,责任重于泰山。” 这句话同样适用于软件生产,本章主要说明编程中需要注意的比较基础的安全准则。
第一条:【强制】 隶属于用户个人的页面或者功能必须进行权限控制校验
- 说明: 放置皆有做水平权限校验就可以随意访问、修改、删除别人的数据,比如查看他人的私信内容、修改他人的订单。
第二条:【强制】 用户敏感数据禁止直接展示,必须对展示数据进行脱敏。
- 说明: 个人手机号码会显示为 158****9119,隐藏中间 4 位,防止个人隐私泄露。
第三条:【强制】 用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入,禁止字符串拼接 SQL 访问数据库。
第四条:【强制】 用户请求传入的任何参数必须做有效性验证
- 说明: 忽略参数校验可能导致如下情况。
1)page size 过大导致内存溢出
2)恶意 order by 导致数据库慢查询
3)任意重定向
4)SQL 注入
5)反序列化注入
6)正则输入源串拒绝服务 ReDoS
Java 代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题,但是如果攻击人员使用的是特殊构造的字符串来验证,则有可能导致死循环。
第五条:【强制】 禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据。
第六条:【强制】 表单、AJAX 提交必须执行 CSRF 安全过滤
第七条:【强制】 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放限制,如数量限制、疲劳度控制、验证码校验,避免被滥刷、资损。
- 说明:如注册时发送验证码到手机,如果没有限制次数和频率,那么可以利用此功能骚扰到其它用户,并造成短信平台资源浪费。
第5章:MySQL 数据库
5.1 建表规约
第二条:【强制】 表名、字段名必须使用小写字母或数字 , 禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
- 说明: MySQL 在 Windows 下不区分大小写,但在 Linux 下默认区分大小写。因此,数据库名、表明、字段名都不允许出现任何大写字母,避免节外生枝。
正例: getter _ admin , task _ config , level 3_ name
反例: GetterAdmin , taskConfig , level 3 name
第四条:【强制】禁用保留字,如 desc 、 range 、 match 、 delayed 等,请参考 MySQL 官方保留字。
第五条: 【强制】主键索引名为 pk_ 字段名;唯一索引名为 uk _字段名 ; 普通索引名则为 idx _字段名。
- 说明: pk_ 即 primary key;uk _ 即 unique key;idx _ 即 index 的简称。
第六条:【强制】小数类型为 decimal ,禁止使用 float 和 double 。
- 说明:float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。
第八条:【强制】 varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text ,独立出来一张表,用主键来对应,避免影响其它字段索引效率。
第九条:【强制】表必备三字段: id , gmt _ create , gmt _ modified
- 说明:其中 id 必为主键,类型为 unsigned bigint 、单表时自增、步长为 1。 gmt _ create ,gmt _ modified 的类型均为 date _ time 类型。
第十条: 【推荐】表的命名最好是加上“业务名称_表的作用”。
正例: tiger _ task / tiger _ reader / mpp _ config
第十五条:【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。
正例:如下表,其中无符号值可以避免误存负数,且扩大了表示范围。
对象 | 年龄区间 | 类型 | 表示范围 |
---|---|---|---|
人 | 150 岁之内 | unsigned tinyint | 无符号值:0 到 255 |
龟 | 数百岁 | unsigned smallint | 无符号值:0 到 65535 |
恐龙化石 | 数千万年 | unsigned int | 无符号值:0 到约 42.9 亿 |
太阳 | 约 50 亿年 | unsigned bigint | 无符号值:0 到约 10 的 19 次方 |
5.2 索引规约
第五条: 【推荐】如果有 order by 的场景,请注意利用索引的有序性。 order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file _ sort 的情况,影响查询性能。
正例: where a =? and b =? order by c; 索引: a _ b _ c
反例:索引中有范围查找,那么索引有序性无法利用,如: WHERE a >10 ORDER BY b; 索引 a _ b 无法排序。
第九条: 【推荐】建组合索引的时候,区分度最高的在最左边。
正例:如果 where a =? and b =? , a 列的几乎接近于唯一值,那么只需要单建 idx _ a 索引即可。
- 说明: 存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如: where a >? and b = ? 那么即使 a 的区分度更高,也必须把 b 放在索引的最前列。
5.3 SQL 语句
第一条:【强制】不要使用 count( 列名 ) 或 count( 常量 ) 来替代 count( * ) , count( * ) 是 SQL 92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
- 说明: count( * ) 会统计值为 NULL 的行,而 count( 列名 ) 不会统计此列为 NULL 值的行。
第六条: 【强制】不得使用外键与级联,一切外键概念必须在应用层解决。
- 说明: ( 概念解释 ) 学生表中的 student _ id 是主键,那么成绩表中的 student _ id 则为外键。如果更新学生表中的 student _ id ,同时触发成绩表中的 student _ id 更新,则为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群 ; 级联更新是强阻塞,存在数据库更新风暴的风险 ; 外键影响数据库的插入速度。
第八条: 【强制】数据订正时,删除和修改记录时,要先 select ,避免出现误删除,确认无误才能执行更新语句。
5.4 ORM 映射
整个规约对自己来说都挺有用的,因为正好涉及到这方面,幸好感觉脸不怎么疼。
第一条:【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
- 说明: 1 ) 增加查询分析器解析成本。2 ) 增减字段容易与 resultMap 配置不一致。
第二条:【强制】 POJO 类的 布尔 属性不能加 is ,而数据库字段必须加 is _,要求在 resultMap 中进行字段与属性之间的映射。
- 说明: 参见定义 POJO 类以及数据库字段定义规定,在
中 增加映射,是必须的。在 MyBatis Generator 生成的代码中,需要进行对应的修改。
第三条:【强制】不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义 ; 反过来,每一个表也必然有一个与之对应。
- 说明: 配置映射关系,使字段与 DO 类解耦,方便维护。
第七条:【强制】更新数据表记录时,必须同时更新记录对应的 gmt _ modified 字段值为当前时间。
第九条:【参考】@ Transactional 事务不要滥用。事务会影响数据库的 QPS ,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
总结
浏览了一遍,还是学习到了很多东西吧,上面也仅仅只是总结了对我自己比较收益,现阶段我能吸收能实际感受得到的规约,如果想要 PDF 版的可以在这里下载:戳这里
欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz
欢迎关注公众微信号:wmyskxz_javaweb
分享自己的Java Web学习之路以及各种Java学习资料