阿里巴巴 Java 开发手册 终极版 v1.3.0 阅读笔记

140 阅读20分钟

阿里巴巴 Java 开发手册 终极版 v1.3.0 阅读笔记

一、编程规范

(一)命名规范

  1. 代码中的所有命名都不能以下划线或者美元符号开始或结尾,这点与 Python 很不相同

  2. 类名使用 UpperCamelCase 风格,除了 DO / BO / DTO / VO / AO ;方法名、参数名、成员变量、局部变量的命名都统一使用 LowerCamelCase 风格;常量命名全都大写,单词之间用下划线隔开,可以多些几个单词以求词义清楚;抽象类命名使用 Abstract 或者 Base 开头;异常类命名使用 Exception 结尾;测试类命名以它要测试的类的名称开始,以 Test 结尾;包名统一使用小写、单数形式,点分隔符之间有且仅有一个单词,最后的类名可以使用复数形式;枚举类名应该加上 Enum 后缀,枚举成员需要全大写,单词之间用下划线隔开

  3. POJO 类中的布尔类型的变量,都不要以 is 开头,否则部分框架解析时会引起序列化错误,定义为 Boolean 类型的变量 isDeleted ,它的方法也是 isDeleted() ,RPC框架在反向解析的时候会误判对应的属性是 deleted ,导致属性获取不到,抛出异常。

  4. 接口类中的方法和属性都不要加任何修饰符,尽量不要在接口中定义变量

  5. 接口和实现类有两套命名规则:

    1. 对于 Service 和 DAO 类,基于 SOA 的理念,需要对外暴露的一定是接口,内部的实现类使用 Impl 后缀
    2. 如果是形容能力的接口,应该取其对应的形容词作为接口名,通常是 -able 结尾
  6. Service/DAO层方法命名规约:

    1. 获取单个对象的方法用get做前缀
    2. 获取多个对象的方法用list做前缀
    3. 获取统计值的方法用count做前缀
    4. 插入的方法用save/insert做前缀
    5. 删除的方法用remove/delete做前缀
    6. 修改的方法用update做前缀
  7. 领域模型命名规约:1) 数据对象:xxxDO,xxx即为数据表名。 2) 数据传输对象:xxxDTO,xxx为业务领域相关的名称 。 3) 展示对象:xxxVO,xxx一般为网页名称。 4)POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO

(二)常量定义

  1. 不允许任何魔法值出现在代码中,魔法值是代码中莫名其妙出现的数字,其数字意义必须经过阅读其他代码进行推理才能理解,这给其他开发人员阅读代码和后期维护都造成了很大的不便
  2. 使用 Long 或者 long 类型进行初始赋值时,结尾使用大写的 L ,因为小写的 l 容易与数字 1 混淆
  3. 常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包 内共享常量、类内共享常量。1)跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下。 2) 应用内共享常量:放置在一方库中,通常是 modules 中的 constant 目录下。 3) 子工程内部共享常量:即在当前子工程的 constant 目录下。4)包内共享常量:即在当前包下单独的 constant 目录下 5)类内共享常量:直接在类内部 private static final 定义

(三)代码格式

  1. 如果大括号内为空,则简洁地写成 {} ,不需要换行;非空代码块做大括号前不换行,左大括号后换行,右大括号前换行,右大括号后如果还有 else 等代码则不换行,表示终止的右大括号后必须换行
  2. 左、右小括号和字符之间不出现空格,if / for / while /switch / do 等保留字与括号之间必须加空格,任何二目运算符、三目运算符两边都要加一个空格
  3. 禁止使用tab字符来缩进,使用四个空格缩进
  4. 注释的双斜线和注释内容之间有且仅有一个空格
  5. 单行字符数限制不超过120个,超出需要换行,换行要有4个空格的缩进,运算符、方法调用的点号、逗号要与下文一起换行,括号前不要换行
  6. 方法参数定义和传入时,多个参数逗号后边必须加个空格
  7. IDE 的 text file encoding 设置为 UTF-8 ; IDE 中文件的换行符使用 Unix 格式, 不要使用 Windows 格式。

(四)OOP规约

  1. 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可
  2. 所有的覆写方法,必须加 @Override 注解
  3. 尽量不用可变参数编程
  4. 正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加 @Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。不能使用过时的类或方法。接口提供方既然明确是过时接口,那么有义务同时提供新的接口,作为调用方来说,有义务去考证过时方法的新实现是什么
  5. Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals 。所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较,对于 Integer var = ? 在 -128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用 == 进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑, 推荐使用 equals 方法进行判断
  6. 关于基本数据类型与包装数据类型的使用标准如下: 1) 所有的 POJO 类属性必须使用包装数据类型。 2) RPC 方法的返回值和参数必须使用包装数据类型。 3) 所有的局部变量使用基本数据类型。定义 DO / DTO / VO 等 POJO 类时,不要设定任何属性默认值
  7. 序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。serialVersionUID 不一致会抛出序列化运行时异常
  8. 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中
  9. POJO 类必须写 toString 方法。使用IDE的中工具:source > generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString
  10. 使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛 IndexOutOfBoundsException 的风险
  11. 当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起, 便于阅读,此条规则优先于第15条规则。类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter 方法
  12. setter方法中,参数名称与类成员变量名称一致,this.成员名 = 参数名。在 getter/setter 方法中,不要增加业务逻辑,增加排查问题的难度
  13. 循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。反编译出的字节码文件显示每次循环都会new出一个 StringBuilder对象,然后进行 append 操作,最后通过toString 方法返回 String 对象,造成内存资源浪费
  14. final 可以声明类、成员变量、方法、以及本地变量,下列情况使用 final 关键字: 1) 不允许被继承的类,如:String 类。 2) 不允许修改引用的域对象,如:POJO 类的域变量。 3) 不允许被重写的方法,如:POJO 类的setter 方法。 4) 不允许运行过程中重新赋值的局部变量。 5) 避免上下文重复使用一个变量,使用 final 描述可以强制重新定义一个变量,方便更好 地进行重构
  15. Object 的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现属性对象的拷贝。
  16. 类成员与方法访问控制从严: 1) 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private 。 2) 工具类不允许有 public 或 default 构造方法。 3) 类非 static 成员变量并且与子类共享,必须是 protected 。 4) 类非 static 成员变量并且仅在本类使用,必须是 private 。 5) 类 static 成员变量如果仅在本类使用,必须是 private 。6) 若是 static 成员变量,必须考虑是否为 final 。 7) 类成员方法只供类内部调用,必须是 private 。 8) 类成员方法只对继承类公开,那么限制为 protected

(五)集合处理

  1. 只要重写 equals 就必须重写 hashCode;因为 Set 存储的是不重复对象,需要根据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法;自定义对象作为 Map 的 key ,那么必须重写 hashCode 和 equals
  2. ArrayList 的 subList 结果不可强转成 ArrayList ,否则会抛出 ClassCastException 异常,即java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList ,而是 ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。在 subList 场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、 删除均会产生 ConcurrentModificationException 异常
  3. 使用 toArray 带参方法,入参分配的数组空间不够大时,toArray 方法内部将重新分配内存空间,并返回新数组地址;如果数组元素大于实际所需,下标为 [ list.size() ] 的数组元素将被置为null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致
  4. 使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法,它的 add / remove / clear 方法会抛出 UnsupportedOperationException 异常,asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组
  5. 泛型通配符来接收返回的数据,此写法的泛型集合不能使用 add 方法,而不能使用 get 方法,做为接口调用赋值时易出错。PECS(Producer Extends Consumer Super) 原则:第一、频繁往外读取内 容的,适合用 <? extends T>。第二、经常往里插入的,适合用 <? super T>
  6. 不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator 方式,如果并发操作,需要对Iterator对象加锁
  7. 在 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 比较结果相同
  8. 集合初始化时,指定集合初始值大小。 说明:HashMap 使用 HashMap(int initialCapacity) 初始化, 正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)
  9. 使用 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 值组合集合。
  10. 有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次 序是一定的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet是 order/sort
  11. 利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的 contains 方法进行遍历、对比、去重操作

(六)并发处理

  1. 创建线程或者线程池时必须指定有意义的线程名称,方便回溯错误
  2. 线程资源必须通过线程池提供,不允许应用中显示的创建线程,便于减少线程创建和销毁的开销,如果不使用线程池,可能会出现创建大量同类线程而导致消耗完内存或者“过度切换”问题
  3. 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。Executors 返回的线程池对象的弊端如下: 1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。 2)CachedThreadPool 和 ScheduledThreadPool : 允许的创建线程数量为 Integer.MAX_VALUE ,可能会创建大量的线程,从而导致 OOM
  4. SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static ,必须加锁,或者使用 DateUtils 工具类。如果是 JDK8 的应用,可使用 Instant 代替 Date,LocalDateTime代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat
  5. 高并发时,能用无锁的数据结构,就不要加锁,能锁区块,就不要锁整个方法体,能用对象锁,就不要用类锁,避免在所代码块里调用RPC服务
  6. 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁
  7. 多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题
  8. 使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果
  9. 避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致的性能下降。在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7之前,需要编码保证每个线程持有一个实例
  10. HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在 开发过程中可以使用其它数据结构或加锁来规避此风险

(七)控制语句

  1. 在 if / else / for / while / do 语句中必须使用大括号。即使只有一行代码,避免采用 行的编码方式
  2. 表达异常的分支时,少用 if-else 方式,避免后续代码维护困难
  3. 循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、 获取数据库连接

(八)注释规约

  1. 类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /*内容/ 格式,不得使用 // xxx方式,所有的类都必须添加创建者和创建日期。所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。所有的枚举类型字段必须要有注释,说明每个数据项的用途。
  2. 特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描, 经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。 1) 待办事宜(TODO):( 标记人,标记时间,[预计处理时间]) 表示需要实现,但目前还未实现的功能。这实际上是一个Javadoc的标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个 Javadoc标签)。 2) 错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间]) 在注释中用 FIXME标记某代码是错误的,而且不能工作,需要及时纠正的情况

(九)其他

  1. 如果是 Boolean 包装类对象,优先调用 getXxx() 的方法
  2. 获取当前毫秒数 .currentTimeMillis() ; 而不是 new Date().getTime() ; 说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime() 的方式。在 JDK8 中, 针对统计时间等场景,推荐使用 Instant 类
  3. 注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x<1(能够 取到零值,注意除零异常),如果想获取整数类型的随机数,不要将x放大10的若干倍然后 取整,直接使用 Random对象的 nextInt 或者 nextLong 方法
  4. 任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存
  5. 及时清理不再使用的代码段或配置信息

二、异常日志

(一)异常处理

  1. 异常不要用来做流程控制,条件控制,因为异常的处理效率比条件分支低
  2. 不能在finally块中使用 return ,finally 块中的 return 返回后方法结束执行,不 会再执行 try 块中的 return 语句
  3. 方法的返回值可以为 null ,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。调用方需要进行 null 判断防止 NPE 问题
  4. 定义时区分 unchecked / checked 异常,避免直接抛出new RuntimeException(), 更不允许抛出 Exception 或者 Throwable ,应使用有业务含义的自定义异常。推荐业界已定义 过的自定义异常,如:DAOException / ServiceException等
  5. 在代码中使用“抛异常”还是“返回错误码”,对于公司外的 http / api 开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用 Result 方式,封装 isSuccess() 方法、“错误码”、“错误简短信息”

(二)日志规约

  1. 应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架 SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一
  2. 日志文件推荐至少保存 15 天
  3. 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式: appName_logType_logName.log
  4. 对 trace / debug / info 级别的日志输出,必须使用条件输出形式或者使用占位符的方
  5. 避免重复打印日志,浪费磁盘空间,务必在log4j.xml中设置additivity=false

三、单元测试

  1. 单元测 试中不准使用 System.out 来进行人肉验证,必须使用assert来验证
  2. 为了保证单元测试稳定可靠且便于维护,单元测试用例之间 决不能互相调用,也不能依赖执行的先后次序
  3. 对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级 别,一般是方法级别
  4. 单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下
  5. 单元测试的基本目标:语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率都 要达到100%
  6. 对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的, 或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者 对单元测试产生的数据有明确的前后缀标识
  7. 在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好 覆盖所有测试用例(UC)

四、安全规约

  1. 隶属于用户个人的页面或者功能必须进行权限控制校验
  2. 用户敏感数据禁止直接展示,必须对展示数据进行脱敏。禁止向HTML页面输出未经安全过滤或未正确转义的用户数据
  3. 用户输入的SQL参数严格使用参数绑定或者METADATA字段值限定,防止SQL注入, 禁止字符串拼接SQL访问数据库
  4. 用户请求传入的任何参数必须做有效性验证
  5. 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放限制, 如数量限制、疲劳度控制、验证码校验,避免被滥刷、资损。发贴、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过 滤等风控策略

五、MySQL数据库

(一)建表规约

  1. 表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint ( 1表示是,0表示否), 主键索引名为 pk下划线字段名;唯一索引名为 uk下划线字段名;普通索引名则为 idx下划线字段名。pk 即 primary key;uk 即 unique key;idx 即 index 的简称。
  2. 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。表名不使用复数名词。
  3. 小数类型为 decimal,禁止使用 float 和 double
  4. 如果存储的字符串长度相等,使用 char 定长字符串类型
  5. varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率
  6. 表必备三字段:id, gmt_create, gmt_modified
  7. 表必备三字段:id, gmt_create, gmt_modified。 说明:其中id必为主键,类型为unsigned bigint、单表时自增、步长为1。gmt_create, gmt_modified 的类型均为 date_time 类型,前者现在时表示主动创建,后者过去分词表示被动更新
  8. 表的命名最好是加上“业务名称_表的作用”。 库名与应用名称尽量一致
  9. 单表行数超过500万行或者单表容量超过2GB,才推荐进行分库分表