常识之外的规范——阿里java开发手册笔记(全章节)
文章目录
说明
这篇文章是我第一次(认真)阅读《阿里巴巴 Java 开发手册(终极版)》的笔记。手册本身对规范的讲解已经非常详细了,如果你已经有一定的开发经验并且有良好的编码习惯和意识,会发现大部分规范是符合常识的。所以本文不会再去做重复的说明,只是对其中一些可能没留意到的或者说不在(我的)常识之内的一些规范进行整理记录。当然每家公司都有自己的一套规范标准,所以大家也没必要过分追究。
其中或许会有遗漏或者理解错误,希望各位担待提点。
- 重点我会用黑体标注。
- 引用部分为《阿里巴巴 Java 开发手册(终极版)》原文
- 更新时间: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 这个长度限制很有意思,如图:
这个长度大概是 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 互转的例子:
1 | private static Date localDateTimeToUDate(LocalDateTime 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 后调整堆大小带来的压力。