阿里巴巴 Java 开发手册 终极版 v1.3.0 阅读笔记
一、编程规范
(一)命名规范
-
代码中的所有命名都不能以下划线或者美元符号开始或结尾,这点与 Python 很不相同
-
类名使用 UpperCamelCase 风格,除了 DO / BO / DTO / VO / AO ;方法名、参数名、成员变量、局部变量的命名都统一使用 LowerCamelCase 风格;常量命名全都大写,单词之间用下划线隔开,可以多些几个单词以求词义清楚;抽象类命名使用 Abstract 或者 Base 开头;异常类命名使用 Exception 结尾;测试类命名以它要测试的类的名称开始,以 Test 结尾;包名统一使用小写、单数形式,点分隔符之间有且仅有一个单词,最后的类名可以使用复数形式;枚举类名应该加上 Enum 后缀,枚举成员需要全大写,单词之间用下划线隔开
-
POJO 类中的布尔类型的变量,都不要以 is 开头,否则部分框架解析时会引起序列化错误,定义为 Boolean 类型的变量 isDeleted ,它的方法也是 isDeleted() ,RPC框架在反向解析的时候会误判对应的属性是 deleted ,导致属性获取不到,抛出异常。
-
接口类中的方法和属性都不要加任何修饰符,尽量不要在接口中定义变量
-
接口和实现类有两套命名规则:
- 对于 Service 和 DAO 类,基于 SOA 的理念,需要对外暴露的一定是接口,内部的实现类使用 Impl 后缀
- 如果是形容能力的接口,应该取其对应的形容词作为接口名,通常是 -able 结尾
-
Service/DAO层方法命名规约:
- 获取单个对象的方法用get做前缀
- 获取多个对象的方法用list做前缀
- 获取统计值的方法用count做前缀
- 插入的方法用save/insert做前缀
- 删除的方法用remove/delete做前缀
- 修改的方法用update做前缀
-
领域模型命名规约:1) 数据对象:xxxDO,xxx即为数据表名。 2) 数据传输对象:xxxDTO,xxx为业务领域相关的名称 。 3) 展示对象:xxxVO,xxx一般为网页名称。 4)POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO
(二)常量定义
- 不允许任何魔法值出现在代码中,魔法值是代码中莫名其妙出现的数字,其数字意义必须经过阅读其他代码进行推理才能理解,这给其他开发人员阅读代码和后期维护都造成了很大的不便
- 使用 Long 或者 long 类型进行初始赋值时,结尾使用大写的 L ,因为小写的 l 容易与数字 1 混淆
- 常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包 内共享常量、类内共享常量。1)跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下。 2) 应用内共享常量:放置在一方库中,通常是 modules 中的 constant 目录下。 3) 子工程内部共享常量:即在当前子工程的 constant 目录下。4)包内共享常量:即在当前包下单独的 constant 目录下 5)类内共享常量:直接在类内部 private static final 定义
(三)代码格式
- 如果大括号内为空,则简洁地写成 {} ,不需要换行;非空代码块做大括号前不换行,左大括号后换行,右大括号前换行,右大括号后如果还有 else 等代码则不换行,表示终止的右大括号后必须换行
- 左、右小括号和字符之间不出现空格,if / for / while /switch / do 等保留字与括号之间必须加空格,任何二目运算符、三目运算符两边都要加一个空格
- 禁止使用tab字符来缩进,使用四个空格缩进
- 注释的双斜线和注释内容之间有且仅有一个空格
- 单行字符数限制不超过120个,超出需要换行,换行要有4个空格的缩进,运算符、方法调用的点号、逗号要与下文一起换行,括号前不要换行
- 方法参数定义和传入时,多个参数逗号后边必须加个空格
- IDE 的 text file encoding 设置为 UTF-8 ; IDE 中文件的换行符使用 Unix 格式, 不要使用 Windows 格式。
(四)OOP规约
- 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可
- 所有的覆写方法,必须加 @Override 注解
- 尽量不用可变参数编程
- 正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加 @Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。不能使用过时的类或方法。接口提供方既然明确是过时接口,那么有义务同时提供新的接口,作为调用方来说,有义务去考证过时方法的新实现是什么
- Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals 。所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较,对于 Integer var = ? 在 -128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用 == 进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑, 推荐使用 equals 方法进行判断
- 关于基本数据类型与包装数据类型的使用标准如下: 1) 所有的 POJO 类属性必须使用包装数据类型。 2) RPC 方法的返回值和参数必须使用包装数据类型。 3) 所有的局部变量使用基本数据类型。定义 DO / DTO / VO 等 POJO 类时,不要设定任何属性默认值
- 序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。serialVersionUID 不一致会抛出序列化运行时异常
- 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中
- POJO 类必须写 toString 方法。使用IDE的中工具:source > generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString
- 使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛 IndexOutOfBoundsException 的风险
- 当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起, 便于阅读,此条规则优先于第15条规则。类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter 方法
- setter方法中,参数名称与类成员变量名称一致,this.成员名 = 参数名。在 getter/setter 方法中,不要增加业务逻辑,增加排查问题的难度
- 循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。反编译出的字节码文件显示每次循环都会new出一个 StringBuilder对象,然后进行 append 操作,最后通过toString 方法返回 String 对象,造成内存资源浪费
- final 可以声明类、成员变量、方法、以及本地变量,下列情况使用 final 关键字: 1) 不允许被继承的类,如:String 类。 2) 不允许修改引用的域对象,如:POJO 类的域变量。 3) 不允许被重写的方法,如:POJO 类的setter 方法。 4) 不允许运行过程中重新赋值的局部变量。 5) 避免上下文重复使用一个变量,使用 final 描述可以强制重新定义一个变量,方便更好 地进行重构
- Object 的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现属性对象的拷贝。
- 类成员与方法访问控制从严: 1) 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private 。 2) 工具类不允许有 public 或 default 构造方法。 3) 类非 static 成员变量并且与子类共享,必须是 protected 。 4) 类非 static 成员变量并且仅在本类使用,必须是 private 。 5) 类 static 成员变量如果仅在本类使用,必须是 private 。6) 若是 static 成员变量,必须考虑是否为 final 。 7) 类成员方法只供类内部调用,必须是 private 。 8) 类成员方法只对继承类公开,那么限制为 protected
(五)集合处理
- 只要重写 equals 就必须重写 hashCode;因为 Set 存储的是不重复对象,需要根据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法;自定义对象作为 Map 的 key ,那么必须重写 hashCode 和 equals
- ArrayList 的 subList 结果不可强转成 ArrayList ,否则会抛出 ClassCastException 异常,即java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList ,而是 ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。在 subList 场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、 删除均会产生 ConcurrentModificationException 异常
- 使用 toArray 带参方法,入参分配的数组空间不够大时,toArray 方法内部将重新分配内存空间,并返回新数组地址;如果数组元素大于实际所需,下标为 [ list.size() ] 的数组元素将被置为null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致
- 使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法,它的 add / remove / clear 方法会抛出 UnsupportedOperationException 异常,asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组
- 泛型通配符来接收返回的数据,此写法的泛型集合不能使用 add 方法,而不能使用 get 方法,做为接口调用赋值时易出错。PECS(Producer Extends Consumer Super) 原则:第一、频繁往外读取内 容的,适合用 <? extends T>。第二、经常往里插入的,适合用 <? super T>
- 不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator 方式,如果并发操作,需要对Iterator对象加锁
- 在 JDK7 版本及以上,Comparator 要满足如下三个条件,不然 Arrays.sort , Collections.sort 会报 IllegalArgumentException 异常。三个条件如下 1) x,y的比较结果和y,x的比较结果相反。2) x>y,y>z,则x>z。 3) x=y,则 x,z 比较结果和 y,z 比较结果相同
- 集合初始化时,指定集合初始值大小。 说明:HashMap 使用 HashMap(int initialCapacity) 初始化, 正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)
- 使用 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 值组合集合。
- 有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次 序是一定的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet是 order/sort
- 利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的 contains 方法进行遍历、对比、去重操作
(六)并发处理
- 创建线程或者线程池时必须指定有意义的线程名称,方便回溯错误
- 线程资源必须通过线程池提供,不允许应用中显示的创建线程,便于减少线程创建和销毁的开销,如果不使用线程池,可能会出现创建大量同类线程而导致消耗完内存或者“过度切换”问题
- 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。Executors 返回的线程池对象的弊端如下: 1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。 2)CachedThreadPool 和 ScheduledThreadPool : 允许的创建线程数量为 Integer.MAX_VALUE ,可能会创建大量的线程,从而导致 OOM
- SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static ,必须加锁,或者使用 DateUtils 工具类。如果是 JDK8 的应用,可使用 Instant 代替 Date,LocalDateTime代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat
- 高并发时,能用无锁的数据结构,就不要加锁,能锁区块,就不要锁整个方法体,能用对象锁,就不要用类锁,避免在所代码块里调用RPC服务
- 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁
- 多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题
- 使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果
- 避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致的性能下降。在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7之前,需要编码保证每个线程持有一个实例
- HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在 开发过程中可以使用其它数据结构或加锁来规避此风险
(七)控制语句
- 在 if / else / for / while / do 语句中必须使用大括号。即使只有一行代码,避免采用 行的编码方式
- 表达异常的分支时,少用 if-else 方式,避免后续代码维护困难
- 循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、 获取数据库连接
(八)注释规约
- 类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /*内容/ 格式,不得使用 // xxx方式,所有的类都必须添加创建者和创建日期。所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。所有的枚举类型字段必须要有注释,说明每个数据项的用途。
- 特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描, 经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。 1) 待办事宜(TODO):( 标记人,标记时间,[预计处理时间]) 表示需要实现,但目前还未实现的功能。这实际上是一个Javadoc的标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个 Javadoc标签)。 2) 错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间]) 在注释中用 FIXME标记某代码是错误的,而且不能工作,需要及时纠正的情况
(九)其他
- 如果是 Boolean 包装类对象,优先调用 getXxx() 的方法
- 获取当前毫秒数 .currentTimeMillis() ; 而不是 new Date().getTime() ; 说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime() 的方式。在 JDK8 中, 针对统计时间等场景,推荐使用 Instant 类
- 注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x<1(能够 取到零值,注意除零异常),如果想获取整数类型的随机数,不要将x放大10的若干倍然后 取整,直接使用 Random对象的 nextInt 或者 nextLong 方法
- 任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存
- 及时清理不再使用的代码段或配置信息
二、异常日志
(一)异常处理
- 异常不要用来做流程控制,条件控制,因为异常的处理效率比条件分支低
- 不能在finally块中使用 return ,finally 块中的 return 返回后方法结束执行,不 会再执行 try 块中的 return 语句
- 方法的返回值可以为 null ,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。调用方需要进行 null 判断防止 NPE 问题
- 定义时区分 unchecked / checked 异常,避免直接抛出new RuntimeException(), 更不允许抛出 Exception 或者 Throwable ,应使用有业务含义的自定义异常。推荐业界已定义 过的自定义异常,如:DAOException / ServiceException等
- 在代码中使用“抛异常”还是“返回错误码”,对于公司外的 http / api 开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用 Result 方式,封装 isSuccess() 方法、“错误码”、“错误简短信息”
(二)日志规约
- 应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架 SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一
- 日志文件推荐至少保存 15 天
- 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式: appName_logType_logName.log
- 对 trace / debug / info 级别的日志输出,必须使用条件输出形式或者使用占位符的方
- 避免重复打印日志,浪费磁盘空间,务必在log4j.xml中设置additivity=false
三、单元测试
- 单元测 试中不准使用 System.out 来进行人肉验证,必须使用assert来验证
- 为了保证单元测试稳定可靠且便于维护,单元测试用例之间 决不能互相调用,也不能依赖执行的先后次序
- 对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级 别,一般是方法级别
- 单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下
- 单元测试的基本目标:语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率都 要达到100%
- 对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的, 或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者 对单元测试产生的数据有明确的前后缀标识
- 在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好 覆盖所有测试用例(UC)
四、安全规约
- 隶属于用户个人的页面或者功能必须进行权限控制校验
- 用户敏感数据禁止直接展示,必须对展示数据进行脱敏。禁止向HTML页面输出未经安全过滤或未正确转义的用户数据
- 用户输入的SQL参数严格使用参数绑定或者METADATA字段值限定,防止SQL注入, 禁止字符串拼接SQL访问数据库
- 用户请求传入的任何参数必须做有效性验证
- 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放限制, 如数量限制、疲劳度控制、验证码校验,避免被滥刷、资损。发贴、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过 滤等风控策略
五、MySQL数据库
(一)建表规约
- 表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint ( 1表示是,0表示否), 主键索引名为 pk下划线字段名;唯一索引名为 uk下划线字段名;普通索引名则为 idx下划线字段名。pk 即 primary key;uk 即 unique key;idx 即 index 的简称。
- 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。表名不使用复数名词。
- 小数类型为 decimal,禁止使用 float 和 double
- 如果存储的字符串长度相等,使用 char 定长字符串类型
- varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率
- 表必备三字段:id, gmt_create, gmt_modified
- 表必备三字段:id, gmt_create, gmt_modified。 说明:其中id必为主键,类型为unsigned bigint、单表时自增、步长为1。gmt_create, gmt_modified 的类型均为 date_time 类型,前者现在时表示主动创建,后者过去分词表示被动更新
- 表的命名最好是加上“业务名称_表的作用”。 库名与应用名称尽量一致
- 单表行数超过500万行或者单表容量超过2GB,才推荐进行分库分表