2

说明

这篇文章是我第一次(认真)阅读《阿里巴巴 Java 开发手册(终极版)》的笔记。手册本身对规范的讲解已经非常详细了,如果你已经有一定的开发经验并且有良好的编码习惯和意识,会发现大部分规范是符合常识的。所以本文不会再去做重复的说明,只是对其中一些可能没留意到的或者说不在(我的)常识之内的一些规范进行整理记录。当然每家公司都有自己的一套规范标准,所以大家也没必要过分追究。

其中或许会有遗漏或者理解错误,希望各位担待提点。

  1. 重点我会用黑体标注。
  2. 引用部分为《阿里巴巴 Java 开发手册(终极版)》原文
  3. 更新时间:2017-10-17

插件

ide插件已发布:《阿里巴巴Java开发手册》IDEA插件与Eclipse插件使用指南


第一节 编程规约

1 命名规范

8.【强制】POJO 类中布尔类型的变量,都不要加 is,否则部分框架解析会引起序列化错误。

反例:定义为基本数据类型 Boolean isDeleted;的属性,它的方法也是 isDeleted(),RPC框架在反向解析的时候,“以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。

16.【参考】各层命名规约:
A) Service/DAO 层方法命名规约
1) 获取单个对象的方法用 get 做前缀。
2) 获取多个对象的方法用 list 做前缀。(我习惯写成 getXxxList)
3) 获取统计值的方法用 count 做前缀。
4) 插入的方法用 save/insert 做前缀。
5) 删除的方法用 remove/delete 做前缀。
6) 修改的方法用 update 做前缀。

2 常量定义

1.【强制】不允许任何魔法值(即未经定义的常量)直接出现在代码中。
反例:
String key = "Id#taobao_" + tradeId;
cache.put(key, value);

魔法值:是指在代码中直接出现的数值,而只有在这个数值记述的那部分代码中才能明确了解其含义。
也就是我们常说的[硬编码]或者[写死],这类代码需要定义常量来明确其含义。

3 代码格式

5.【强制】采用 4 个空格缩进,禁止使用 tab 字符。
说明:如果使用 tab 缩进,必须设置 1 个 tab 为 4 个空格。IDEA 设置 tab 为 4 个空格时,请勿勾选 Use tab character;而在 eclipse 中,必须勾选 insert spaces for tabs。

有些同学可能会对这一条不以为然。如果是协调开发,两个工程师的格式化规则不一致很可能A同学无意把B同学的代码重新格式化并提交,导致后边查看svn变更记录时傻逼了。

7.【强制】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:
1) 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考示例。
2) 运算符与下文一起换行。
3) 方法调用的点符号与下文一起换行。
4) 方法调用时,多个参数,需要换行时,在逗号后进行。
5) 在括号前不要换行,见反例。

120这个长度限制很有意思,如图:
clipboard.png
这个长度大概是15寸笔记本1080分辨率字体14号左右的最佳可视长度。当然应该也不一定非要这么精准吧。。

4 OOP 规约

7.【强制】所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。
说明:对于 Integer var = ? 在-128 至 127 范围内的赋值,Integer 对象是在IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。

12.【强制】POJO 类必须写 toString 方法。使用 IDE 的中工具:source> generate toString时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。
说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排查问题。

吐槽:"使用 IDE 的中工具" 码字错误哦!

13.【推荐】使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛 IndexOutOfBoundsException 的风险。
说明:
String str = "a,b,c,,";
String[] ary = str.split(",");
// 预期大于 3,结果是 3
System.out.println(ary.length);

最好的做法是对集合类型的变量本身进行判空校验或者大小判断,不要想当然。

5 集合处理

2.【强制】ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常,即java.util.RandomAccessSubList cannot be cast to java.util.ArrayList.
说明:subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList ,而是ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。

5.【强制】使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[] { "you", "wu" };
List list = Arrays.asList(str);
第一种情况:list.add("yangguanbao"); 运行时异常。
第二种情况:str[0] = "gujin"; 那么 list.get(0)也会随之修改。

10.【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。
说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出key 所对应的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用 Map.foreach 方法。
正例:values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合。

java8 是个好东西~

6 并发处理

5.【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为static,必须加锁,或者使用 DateUtils 工具类。
正例:注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。

再说一遍,java8 是个好东西!LocalDateTime相关API
附赠一个java.util.Date和LocalDateTime互转的例子:

   private static Date localDateTimeToUDate(LocalDateTime localDateTime) {
       ZoneId zone = ZoneId.systemDefault();
       Instant instant = localDateTime.atZone(zone).toInstant();
       return Date.from(instant);
   }

   private static LocalDateTime uDateToLocalDate(Date date) {
       Instant instant = date.toInstant();
       ZoneId zone = ZoneId.systemDefault();
       LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone);
       return localDateTime;
   }

14.【参考】 HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在开发过程中可以使用其它数据结构或加锁来规避此风险。

7 控制语句

3.【推荐】表达异常的分支时,少用 if-else 方式,这种方式可以改写成:
if (condition) {
...
return obj;
}
// 接着写 else 的业务逻辑代码;
说明:如果非得使用 if()...else if()...else...方式表达逻辑,【强制】避免后续代码维护困难,请勿超过 3 层。
正例:超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现...

我们公司codeReview时经常看到有些同学的代码是if(){}else if(){} else if(){}else{} 除了看上去low更主要的原因是过多的大括号层级不便于阅读很容易搞混,尤其是跳出代码块的时候,连续几个}}}基本就不知道跳到哪了彻底懵逼,还得折叠代码或者滚上去重新回忆一下。

6.【推荐】接口入参保护,这种场景常见的是用于做批量操作的接口。

解释一下,接口入参保护就是对入参进行校验,包括允许的最大值或者其他范围或边界。防止请求大量数据导致接口“爆炸”。比如限制返回数据最大条数,超过限制直接return或者抛异常。

8 注释规约

感觉没啥好说的。。

9 其它

1.【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
说明:不要在方法体内定义:Pattern pattern = Pattern.compile(规则);

就是说定义成全局变量。


第二节 异常日志

1 异常处理

3.【强制】对大段代码进行try-catch,这是不负责任的表现。catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。

9.【推荐】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。调用方需要进行null判断防止NPE问题。说明:本手册明确防止NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。

需要说明的是是否可以返回null是需要根据接口约定来判断的,如果明确的返回对象的结构类型,一定要返回这个对象,但他的属性值可以是null,比如page对象:{data:null,pageNum:0,count:0}

2 日志规约

4.【强制】对trace/debug/info级别的日志输出,必须使用条件输出形式或者使用占位符的方式。
说明:logger.debug("Processingtradewithid: " + id+ "andsymbol: " + symbol);如果日志级别是warn,上述日志不会打印,但是会执行字符串拼接操作,如果symbol是对象,会执行toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。
正例:(条件)if (logger.isDebugEnabled()) { logger.debug("Processing trade with id: " + id + " and symbol: " + symbol); }
正例:(占位符)logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);


第三节 单元测试

4.【强制】单元测试是可以重复执行的,不能受到外界环境的影响。
说明:单元测试通常会被放到持续集成中,每次有代码check in时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。
正例:为了不受外界环境影响,要求设计代码时就把SUT的依赖改成注入,在测试时用spring这样的DI框架注入一个本地(内存)实现或者Mock实现。

15.【参考】为了更方便地进行单元测试,业务代码应避免以下情况:
构造方法中做的事情过多。存在过多的全局变量和静态方法。
存在过多的外部依赖。
存在过多的条件语句。说明:多层条件语句建议使用卫语句、策略模式、状态模式等方式重构。

和第一节if-else提到的一样,避免多层代码块嵌套

16.【参考】不要对单元测试存在如下误解:
那是测试同学干的事情。本文是开发手册,凡是本文内容都是与开发同学强相关的。
单元测试代码是多余的。汽车的整体功能与各单元部件的测试正常与否是强相关的。
单元测试代码不需要维护。一年半载后,那么单元测试几乎处于废弃状态。
单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障。

测试开发相亲相爱是一家~


第四节 安全规约

4.【强制】用户请求传入的任何参数必须做有效性验证
说明:忽略参数校验可能导致:
pagesize过大导致内存溢出
恶意orderby导致数据库慢查询
任意重定向SQL注入
反序列化注入
正则输入源串拒绝服务ReDoS
说明:Java代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题,但是如果攻击人员使用的是特殊构造的字符串来验证,有可能导致死循环的结果。

老生常谈的问题,但在工作中有时会忽略。


第五节 MySQL数据库

1. 建表规约

8.【强制】varchar是可变长字符串,不预先分配存储空间,长度不要超过5000,如果存储长度大于此值,定义字段类型为text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。

大字段建外连表,避免影响索引效率。难点在于如何说服主工程师(滑稽)。

13.【推荐】字段允许适当冗余,以提高查询性能,但必须考虑数据一致。
冗余字段应遵循:
1)不是频繁修改的字段。
2)不是varchar超长字段,更不能是text字段。
正例:商品类目名称使用频率高,字段长度短,名称基本一成不变,可在相关联的表中冗余存储类目名称,避免关联查询。

讲一个笑话。公司老王出差去拉项目,对方博士生问“这个数据库设计为什么不符合三范式?”
真事。

14.【推荐】单表行数超过500万行或者单表容量超过2GB,才推荐进行分库分表。
说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。

2. 索引规约

3.【强制】在varchar字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为20的索引,区分度会高达90%以上,可以使用count(distinctleft(列名, 索引长度))/count(*)的区分度来确定。

5.【推荐】如果有orderby的场景,请注意利用索引的有序性。orderby最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现file_sort的情况,影响查询性能。
正例:wherea=? andb=? orderbyc;索引:a_b_c
反例:索引中有范围查找,那么索引有序性无法利用,如:WHEREa>10 ORDERBYb;索引a_b无法排序。

6.【推荐】利用覆盖索引来进行查询操作,避免回表。
说明:如果一本书需要知道第11章是什么标题,会翻开第11章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。
正例:能够建立索引的种类:主键索引、唯一索引、普通索引,而覆盖索引是一种查询的一种效果,用explain的结果,extra列会出现:usingindex。

3. SQL语句

1.【强制】不要使用count(列名)或count(常量)来替代count(*),count(*)是SQL92定义的标准统计行数的语法,跟数据库无关,跟NULL和非NULL无关。
说明:count(*)会统计值为NULL的行,而count(列名)不会统计此列为NULL值的行。

乖乖滚回count(*)

【推荐】in操作能避免则避免,若实在避免不了,需要仔细评估in后边的集合元素数量,控制在1000个之内。

4. ORM映射

3.【强制】不要用resultClass当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义;反过来,每一个表也必然有一个与之对应。
说明:配置映射关系,使字段与DO类解耦,方便维护。

编程一时爽,维护两行泪~

5.【强制】iBATIS自带的queryForList(StringstatementName,intstart,intsize)不推荐使用。
说明:其实现方式是在数据库取到statementName对应的SQL语句的所有记录,再通过subList取start,size的子集合
正例:
Map<String, Object> map = new HashMap<String, Object>();
map.put("start", start);
map.put("size", size);

没想到你是这样的iBATIS!


第六节 工程结构

1. 应用分层

2.【参考】(分层异常处理规约)在DAO层,产生的异常类型有很多,无法用细粒度的异常进行catch,使用catch(Exceptione)方式,并thrownewDAOException(e),不需要打印日志,因为日志在Manager/Service层一定需要捕获并打到日志文件中去,如果同台服务器再打日志,浪费性能和存储。在Service层出现异常时,必须记录出错日志到磁盘,尽可能带上参数信息,相当于保护案发现场。如果Manager层与Service同机部署,日志方式与DAO层处理一致,如果是单独部署,则采用与Service一致的处理方式。Web层绝不应该继续往上抛异常,因为已经处于顶层,如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面,加上用户容易理解的错误提示信息。开放接口层要将异常处理成错误码和错误信息方式返回。

2. 二方库依赖

10.【参考】为避免应用二方库的依赖冲突问题,二方库发布者应当遵循以下原则:
1)精简可控原则。移除一切不必要的API和依赖,只包含ServiceAPI、必要的领域模型对象、Utils类、常量、枚举等。如果依赖其它二方库,尽量是provided引入,让二方库使用者去依赖具体版本号;无log具体实现,只依赖日志框架。
2)稳定可追溯原则。每个版本的变化应该被记录,二方库由谁维护,源码在哪里,都需要能方便查到。除非用户主动升级版本,否则公共二方库的行为不应该发生变化。

3. 服务器

4.【推荐】在线上生产环境,JVM的Xms和Xmx设置一样大小的内存容量,避免在GC后调整堆大小带来的压力。


TheEmbers
27 声望3 粉丝

全栈吧 嗯全沾..