本文对阿里巴巴java开发手册中需要注意的点予以记录
1.编程规约
-
类名中包含领域模型如DO/BO/DTO/VO时要 全部大写,如UserDTO.
-
抽象类要以Abstract或Base开头,异常类以Exception结尾,测试类要以所测试的类开头,Test结尾。
-
杜绝不规范的缩写
-
将设计模式体现在类名中,有利于阅读者快速理解架构,如OrderFactory LoginProxy ResourceObsever
-
service和dao层的方法命名推荐:get list count save/insert remove/delete update作为前缀
-
不要使用一个常量类来维护所有常量,应该分类分开维护
-
任何运算符前后都应该有一个空格
-
所有覆写方法,必须添加@Override注解
-
POJO类必须写toString方法,便于排查问题(抛出异常时会调用该类的toString打印属性值)
-
final可以提高程序效率,多考虑属性,方法,类是否可以定义成final
-
关于 hashCode 和 equals 的处理,遵循如下规则:
1) 只要重写 equals ,就必须重写 hashCode 。
2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的
对象必须重写这两个方法。
3) 如果自定义对象做为 Map 的键,那么必须重写 hashCode 和 equals 。
正例: String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象
作为 key 来使用。
-
ArrayList 的 subList 结果不可强转成 ArrayList ,否则会抛出 ClassCastException异常: java . util . RandomAccessSubList cannot be cast to java . util . ArrayList ;说明: subList 返回的是 ArrayList 的内部类 SubList ,并不是 ArrayList ,而是ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。 subList 场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均产生 ConcurrentModificationException 异常。但实际开发中subList基本不会使用。
-
使用集合转数组的方法,必须使用集合的 toArray(T[] array) ,传入的是类型完全一样的数组,大小就是 list . size() 。
反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[] 类,若强转其它类型数组将出现 ClassCastException 错误。
正例:
List<String> list = new ArrayList<String>(2);
list.add("guan");
list.add("bao");
String[] array = new String[list.size()];
array = list.toArray(array);
说明:使用 toArray 带参方法,入参分配的数组空间不够大时, toArray 方法内部将重新分配内存空间,并返回新数组地址 ; 如果数组元素大于实际所需,下标为 [ list . size() ] 的数组元素将被置为 null ,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。
- 使用工具类 Arrays . asList() 把数组转换成集合时,不能使用其修改集合相关的方法,它的 add / remove / clear 方法会抛出 UnsupportedOperationException 异常。
说明: asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。 Arrays . asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[] { “a”, “b” };
List list = Arrays.asList(str);
第一种情况: list.add(“c”); 运行时异常。
第二种情况: str[0]= “gujin”; 那么 list.get(0) 也会随之修改。
如图:返回的是Arrays的内部类
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
- 【强制】不要在 foreach 循环里进行元素的 remove / add 操作。 remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
反例:
List< String> a = new ArrayList<>();
a.add("1");
a.add("2");
for (String temp : a) {
if("1".equals(temp)){
a.remove(temp);
}
}
说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?会报ConcurrentModificationException并发修改异常。
正例:
Iterator< String> it = a.iterator();
while(it.hasNext()){
String temp = it.next();
if(删除元素的条件){
it.remove();
}
}
- 集合初始化时,尽量指定集合初始值大小。
说明: ArrayList 尽量使用 ArrayList(int initialCapacity) 初始化
- 使用 entrySet 遍历 Map 类集合 KV ,而不是 keySet 方式进行遍历。
说明: keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出key 所对应的 value 。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。 values() 返回的是 V 值集合,是一个 list 集合对象 ;keySet() 返回的是 K 值集合,是一个 Set 集合对象 ;entrySet() 返回的是 K - V 值组合集合。
map的四种遍历方式
如果是 JDK 8,使用 Map . foreach 方法。
Map<String,Integer> m = new HashMap<>();
m.put("hello", 11);
m.put("world", 11);
m.put("nihao", 21);
m.put("shijie", 22);
m.forEach((key,value)-> System.out.println(key+" "+value));
- 由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,注意存储null 值时会抛出 NPE 异常
集合类 |
key |
value |
super |
说明 |
Hashtable |
不允许为 null |
不允许为 null |
Dictionary |
线程安全 |
ConcurrentHashMap |
不允许为 null |
不允许为 null |
AbstractMap |
分段锁技术 |
TreeMap |
不允许为 null |
允许为 null |
AbstractMap |
线程不安全 |
HashMap |
不允许为 null |
不允许为 null |
AbstractMap |
线程不安全 |
2.并发处理
- 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明: Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool :
允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。
2) CachedThreadPool 和 ScheduledThreadPool :
允许的创建线程数量为 Integer.MAX_VALUE ,可能会创建大量的线程,从而导致 OOM
而且注意自定义的线程池一定要定义有意义的线程池名称,便于查看日志排查问题。
@Bean
public TaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(5);
taskExecutor.setMaxPoolSize(15);
taskExecutor.setQueueCapacity(6000);
taskExecutor.setThreadNamePrefix("demo Thread-");
taskExecutor.initialize();
return taskExecutor;
}
- SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为static ,必须加锁,或者使用 DateUtils 工具类。
正例:注意线程安全,使用 DateUtils 。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@ Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
说明:如果是 JDK 8 ,可以使用 Instant 代替 Date , LocalDateTime 代替 Calendar ,DateTimeFormatter 代替 Simpledateformatter ,官方给出的解释: simple beautiful strong immutable thread - safe 。
3.逻辑处理
- 在一个 switch 块内,每个 case 要么通过 break / return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止 ; 在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有
- 推荐尽量少用 else , if - else 的方式可以改写成:
if(condition){
…
return obj;
}
// 接着写 else 的业务逻辑代码;
说明:如果非得使用 if()…else if()…else… 方式表达逻辑,【强制】请勿超过 3 层,超过请使用状态设计模式。
正例:逻辑上超过 3 层的 if-else 代码可以使用switch语句,或者状态模式来实现
- 方法中需要进行参数校验的场景:
1 ) 调用频次低的方法。
2 ) 执行时间开销很大的方法,参数校验时间几乎可以忽略不计,但如果因为参数错误导致
中间执行回退,或者错误,那得不偿失。
3 ) 需要极高稳定性和可用性的方法。
4 ) 对外提供的开放接口,不管是 RPC / API / HTTP 接口。
5) 敏感权限入口
- 方法中不需要参数校验的场景:
1 ) 极有可能被循环调用的方法,不建议对参数进行校验。但在方法说明里必须注明外部参数检查。
2 ) 底层的方法调用频度都比较高,一般不校验。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般 DAO 层与 Service 层都在同一个应用中,部署在同一台服务器中,所以 DAO 的参数校验,可以省略。
3 ) 被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数
- 所有的枚举类型字段必须要有注释,说明每个数据项的用途.但一般枚举都有description属性来描述意义。
- 获取当前毫秒数 System . currentTimeMillis(); 而不是 new Date() . getTime();
说明:如果想获取更加精确的纳秒级时间值,用 System . nanoTime() 。在 JDK 8 中,针对统计时间等场景,推荐使用 Instant 类。
4.异常处理和日志记录
- 有 try 块放到了事务代码中, catch 异常后,如果需要回滚事务,一定要注意手动回滚事务。
- 不能在 finally 块中使用 return , finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句
- 方法的返回值可以为 null ,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。调用方需要进行 null 判断防止 NPE 问题。
说明:本规约明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败,运行时异常等场景返回 null 的情况
- 【推荐】防止 NPE ,是程序员的基本修养,注意 NPE 产生的场景:
1 ) 返回类型为包装数据类型,有可能是 null ,返回 int 值时注意判空。
反例: public int f() { return Integer 对象}; 如果为 null ,自动解箱抛 NPE 。
2 ) 数据库的查询结果可能为 null 。注:其实现在很多框架在返回查询结果时已经进行处理不会返回null
3 ) 集合里的元素即使 isNotEmpty ,取出的数据元素也可能为 null 。
4 ) 远程调用返回对象,一律要求进行 NPE 判断。
5 ) 对于 Session 中获取的数据,建议 NPE 检查,避免空指针。
6 ) 级联调用 obj . getA() . getB() . getC(); 一连串调用,易产生 NPE
- 在代码中使用“抛异常”还是“返回错误码”,对于公司外的 http / api 开放接口必须使用错误码 ; 而应用内部推荐异常抛出 ; 跨应用间 RPC 调用优先考虑使用 Result 方式,封装success 、code、msg。
说明:关于 RPC 方法返回方式使用 Result 方式的理由:
1 ) 使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
2 ) 如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message ,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题
- 应用中不可直接使用日志系统 (Log 4 j 、 Logback) 中的 API ,而应依赖使用日志框架SLF 4 J 中的 API ,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class)
注:这里变量名logger也并没有大写。
- 日志文件推荐至少保存 15 天,因为有些异常具备以“周”为频次发生的特点
- 应用中的扩展日志 ( 如打点、临时监控、访问日志等 ) 命名方式:appName _ logType _ logName . log 。 logType :日志类型,推荐分类有stats / desc / monitor / visit 等 ;logName :日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
正例: mppserver 应用中单独监控时区转换异常,如:mppserver _ monitor _ timeZoneConvert . log
说明:推荐对日志进行分类,错误日志和业务日志尽量分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
- 对 trace / debug / info 级别的日志输出,必须使用条件输出形式或者使用占位符的方式。
说明: logger . debug( " Processing trade with id : " + id + " symbol : " + symbol);**如果日志级别是 warn ,上述日志不会打印,但是会执行字符串拼接操作,如果 symbol 是对象,会执行 toString() 方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。**原来如此
正例: ( 条件 )
if (logger.isDebugEnabled()) {
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
}
正例: ( 占位符 )
logger.debug("Processing trade with id: {} symbol : {} ", id, symbol);
- 异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么往上抛。
正例: logger.error(各类参数或者对象 toString + "_" + e.getMessage(), e);
e会显示堆栈信息。
- 谨慎地记录日志。生产环境禁止输出 debug 日志 ; 有选择地输出 info 日志 ; 如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
5.数据库规约
- 单表行数超过 500 万行或者单表容量超过 2 GB ,才推荐进行分库分表。
说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表
- 业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的 ; 另外,即使在应用层做了非常完善的校验和控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。注:这个值得考虑
- 超过三个表禁止 join 。需要 join 的字段,数据类型保持绝对一致 ; 多表关联查询时,保证被关联的字段需要有索引。
说明:即使双表 join 也要注意表索引、 SQL 性能。注:实际上,现在很多都全部采取单表,禁止关联,简化数据库维护。
- 页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
说明:索引文件具有 B - Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。这个很难做到,如设备名称。
- 创建索引时避免有如下极端误解:
1 ) 误认为一个查询就需要建一个索引。
2 ) 误认为索引会消耗空间、严重拖慢更新和新增速度。
3 ) 误认为唯一索引一律需要在应用层通过“先查后插”方式解决
- 在代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句
- 不得使用外键与级联,一切外键概念必须在应用层解决。
说明: ( 概念解释 ) 学生表中的 student _ id 是主键,那么成绩表中的 student _ id 则为外键。
如果更新学生表中的 student _ id ,同时触发成绩表中的 student _ id 更新,则为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群 ; 级联更新是强阻塞,存在数据库更新风暴的风险 ; 外键影响数据库的插入速度。现在基本都设计成单表,在应用中处理关联逻辑。
- 禁止使用存储过程,存储过程难以调试和扩展,更没有移植性
- 数据订正时,删除和修改记录时,要先 select ,避免出现误删除,确认无误才能执行更新语句。没毛病更新一般是先根据id查询出值,再去修改。
- 不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义 ; 反过来,每一个表也必然有一个与之对应。
说明:配置映射关系,使字段与 DO 类解耦,方便维护。不能将entity直接作为返回值!
- 更新数据表记录时,必须同时更新记录对应的 gmt _ modified 字段值为当前时间。规范意思是所有表都应有创建时间和修改时间两个字段。
- @ Transactional 事务不要滥用。事务会影响数据库的 QPS ,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等
其他
- 所有 pom 文件中的依赖声明放在< dependencies >语句块中,所有版本仲裁放在< dependencyManagement >语句块中。
说明:< dependencyManagement >里只是声明版本,并不实现引入,因此子项目需要显式的声明依赖, version 和 scope 都读取自父 pom 。而< dependencies >所有声明在主 pom 的< dependencies >里的依赖都会自动引入,并默认被所有的子项目继承。