新增11条新规约!阿里Java开发手册|黄山版,拥抱规范,远离伤害

Java
280
0
0
2023-12-10

前言

阿里开发手册是 阿里 近万名开发同学集体智慧的结晶,以开发视角为中心,详细列举如何开发更加高效、更加容错、更加有协作性,力求知其然,更知其不然,结合正反例,让 java 开发者能够提升协作效率、提高代码质量。

码出高效,码出质量!

  • 你是否曾因Java代码规范版本纷杂而无所适从?
  • 你是否想过代码规范能将系统故障率降低20%?
  • 你是否曾因团队代码风格迥异而协同困难?
  • 你是否正在review一些原本可以避免的故障?
  • 你是否无法确定自己的代码足够健壮?

一、编程规约

(一) 命名风格

1. 【强制】 所有编程相关的命名均不能以 下划线或美元符号 开始,也不能以 下划线或美元符号 结束。

  • 反例:_name / __name / $Object / name_ / name$ / Object$

2. 【强制】 所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。

  • 说明: 正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,即使纯拼音命名方式也要避免采用。
  • 正例: ali / alibaba / taobao / kaikeba / aliyun / youku / hangzhou 等国际通用的名称,可视同英文。
  • 反例: DaZhePromotion【打折】/ getPingfenByName()【评分】 / String fw【福娃】/ int 变量名 = 3

3. 【强制】 代码和注释中都要避免使用任何人类语言中的种族歧视性或侮辱性词语。

  • 正例: blockList / allowList / secondary
  • 反例: blackList / whiteList / slave / SB / WTF

4. 【强制】 类名使用 UpperCamelCase 风格,以下情形例外:DO / PO / DTO / BO / VO / UID 等。

  • 正例: ForceCode / UserDO / HtmlDTO / XmlService / TcpUdpDeal / TaPromotion
  • 反例: forcecode / UserDo / HTMLDto / XMLService / TCPUDPDeal / TAPromotion

5. 【强制】 方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格。

  • 正例: localValue / getHttpMessage() / inputUserId

6. 【强制】 常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。

  • 正例: MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
  • 反例: MAX_COUNT / EXPIRED_TIME

7. 【强制】 抽象类 命名使用 abstract 或 Base 开头;异常类命名使用 Exception 结尾,测试类命名以它要

  • 测试的类的名称开始,以 Test 结尾。

8. 【强制】 类型与中括号紧挨相连来定义数组

  • 正例: 定义整形数组 int[] arrayDemo。
  • 反例: 在 main 参数中,使用 String args[] 来定义。

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

  • 说明: 本文 MySQL 规约中的建表约定第 1 条,表达是与否的变量采用 is_xxx 的命名方式,所以需要在<resultMap>设置从 is_xxx 到 xxx 的映射关系。
  • 反例: 义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),框架在反向解析时,“误以为 ”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。

10. 【强制】 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用 单数 形式,但是类名如果有复数含义,类名可以使用复数形式。

  • 正例: 应用工具类包名为 com.alibaba.ei.kunlun.aap.util;类名为 MessageUtils(此规则参考 spring 的框架结构)。

11. 【强制】 避免在子父类的成员变量之间 或者不同代码块的局部变量之间采用完全相同的命名 使可理解性降低

  • 说明: 子类、父类成员变量名相同,即使是 public 也是能够通过编译,而 局部变量 在同一方法内的不同代码块中同名也是合法的,但是要避免使用。对于非 setter / getter 的参数名称也要避免与成员变量名称相同。
  • 反例:
 public class ConfusingName {
    protected int stock;
    protected String alibaba;
    // 非 setter/getter 的参数名称,不允许与本类成员变量同名
    public  void  access(String alibaba) {
        if (condition) {
            final int money =;
            // ...
        }
        for (int i =; i < 10; i++) {
            // 在同一方法体中,不允许与其它代码块中的 money 命名相同
            final int money =;
            // ...
        }
    }
}
class Son  extends  ConfusingName {
    // 不允许与父类的成员变量名称相同
 private  int stock;
} 

12. 【强制】 杜绝完全不规范的英文缩写,避免望文不知义。

  • 反例: Abstract Class“缩写”成 AbsClass;condition“缩写”成 condi; Function “缩写”成 Fu,此类随意缩写严重降低了代码的可阅读性。

13. 【推荐】 为了达到代码自解释的目标,任何自定义编程元素在命名时,使用完整的单词组合来表达。

  • 正例: 在 JDK 中,对某个对象引用的 volatile 字段进行原子更新的类名为 AtomicReferenceFieldUpdater。
  • 反例: 常见的方法内变量为 int a; 的定义方式。

14. 【推荐】 在常量与变量命名时,表示类型的名词放在词尾,以提升辨识度

  • 正例: startTime / workQueue / nameList / TERMINATED_THREAD_COUNT
  • 反例: startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD

15. 【推荐】 如果模块、接口、类、方法使用了设计模式,在命名时要体现出具体模式。

  • 说明: 将设计模式体现在名字中,有利于阅读者快速理解 架构设计 思想。
  • 正例:
 public class OrderFactory;
public class LoginProxy;
public class ResourceObserver; 

16. 【推荐】 接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义常量,如果一定要定义,最好确定该常量与接口的方法相关,并且是整个应用的基础常量。

  • 正例: 接口方法签名 void commit();接口基础常量 String COMPANY = “alibaba”;
  • 反例: 接口方法定义 public abstract void commit();
  • 说明: JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默认实现。

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

1) 【强制】 对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 的后缀与接口区别。

  • 正例: CacheServiceImpl 实现 CacheService 接口。

2) 【推荐】 如果是形容能力的接口名称,取对应的形容词为接口名(通常是 –able 结尾的形容词)。

  • 正例: AbstractTranslator 实现 Translatable。

18. 【参考】 枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。

  • 说明: 枚举其实就是特殊的常量类,且 构造方法 被默认强制是私有。
  • 正例: 枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKNOWN_REASON

19. 【参考】 各层命名规约:

A)Service / DAO 层方法命名规约:

  • 1)获取单个对象的方法用 get 做前缀。
  • 2)获取多个对象的方法用 list 做前缀,复数结尾,如:listObjects
  • 3)获取统计值的方法用 count 做前缀。
  • 4)插入的方法用 save / insert 做前缀。
  • 5)删除的方法用 remove / delete 做前缀。
  • 6)修改的方法用 update 做前缀。

B)领域模型命名规约:

  • 1)数据对象:xxxDO,xxx 即为数据表名。
  • 2)数据传输对象:xxxDTO,xxx 为业务领域相关的名称。
  • 3)展示对象:xxxVO,xxx 一般为网页名称。
  • 4)POJO 是 DO / DTO / BO / VO 的统称,禁止命名成 xxxPOJO。

(二) 常量定义

1. 【强制】 不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。

反例:

 // 开发者 A 定义了缓存的 key。
String key = "Id#taobao_" + tradeId;
cache.put(key, value);
// 开发者 B 使用缓存时直接复制少了下划线,即 key 是"Id#taobao" + tradeId,导致出现故障。
String key = "Id#taobao" + tradeId;
cache.get(key); 

2. 【强制】 long 或 Long 赋值时,数值后使用大写 L,不能是小写 l ,小写容易跟数字混淆,造成误解。

  • 说明: public static final Long NUM = 2l; 写的是数字的 21,还是 Long 型的 2?

3. 【强制】 浮点数 类型的数值后缀统一为大写的 D 或 F。

正例:

 public static final double HEIGHT =.5D;
 public static final float WEIGHT =.3F; 

4. 【推荐】 不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。

  • 说明: 大而全的常量类,杂乱无章,使用查找功能才能定位到要修改的常量,不利于理解,也不利于维护。
  • 正例: 缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类 SystemConfigConsts 下。

5. 【推荐】 常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量。

  • 1)跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下。
  • 2)应用内共享常量:放置在一方库中,通常是子模块中的 constant 目录下。

反例: 易懂常量也要统一定义成应用内共享常量,两个程序员在两个类中分别定义了表示“是”的常量:

 类 A 中:public static final String YES = "yes";
类 B 中:public static final String YES = "y"; 
  • 3)子工程内部共享常量:即在当前子工程的 constant 目录下。
  • 4)包内共享常量:即在当前包下单独的 constant 目录下。
  • 5)类内共享常量:直接在类内部 private static final 定义。

6. 【推荐】 如果变量值仅在一个固定范围内变化用 enum 类型来定义

说明: 如果存在名称之外的延伸属性应使用 enum 类型,下面正例中的数字就是延伸信息,表示一年中的第几个季节。

正例:

 public enum SeasonEnum {
    SPRING(), SUMMER(2), AUTUMN(3),  WINTER (4);
    private int seq;
    SeasonEnum(int seq) {
        this.seq = seq;
    }
    public int getSeq() {
        return seq;
    }
} 

(三) 代码格式

9. 【强制】 方法参数在定义和传入时,多个参数逗号后面必须加空格。

  • 正例: 下例中实参的 args1 逗号后边必须要有一个空格。
 method(args, args2, args3); 

10. 【强制】 IDE 的 text file encoding 设置为 UTF-8;IDE 中文件的换行符使用 Unix 格式,不要使用Windows 格式。

11. 【推荐】 单个方法的总行数不超过 80 行

  • 说明: 除注释之外的方法签名、左右大括号、方法内代码、空行、回车及任何不可见字符的总行数不超过 80 行。
  • 正例: 代码逻辑分清红花和绿叶,个性和共性,绿叶逻辑单独出来成为额外方法,使主干代码更加晰;共性逻辑抽取成为共性方法,便于复用和维护。

12. 【推荐】 没有必要增加若干空格来使变量的赋值等号与上一行对应位置的等号对齐。

正例:

 int one =;
long two =L;
float three =F;
StringBuilder builder = new  StringBuilder (); 
  • 说明: 增加 builder 这个变量,如果需要对齐,则给 one、two、three 都要增加几个空格,在变量比较多的情况下,是非常累赘的事情。

13. 【推荐】 不同逻辑、不同语义、不同业务的代码之间插入一个空行,分隔开来以提升可读性。

  • 说明: 任何情形,没有必要插入 多个空行 进行隔开。

(四) OOP规约

23. 【推荐】 循环体内, 字符串 的连接方式,使用 StringBuilder 的 append 方法进行扩展。

反例:

 String str = "start";
for (int i =; i < 100; i++) {
    str = str + "hello";
} 
  • 说明: 反编译出的 字节码 文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过 toString () 返回 String 对象,造成内存资源浪费。

24. 【推荐】 final 可以声明类、成员变量、方法、以及本地变量,下列情况使用 final 关键字:

  • 1)不允许被继承的类,如: String 类 。
  • 2)不允许修改引用的域对象,如:POJO 类的域变量。
  • 3)不允许被覆写的方法,如:POJO 类的 setter 方法。
  • 4)不允许运行过程中重新赋值的局部变量。
  • 5)避免上下文重复使用一个变量,使用 final 关键字可以强制重新定义一个变量,方便更好地进行重构。

25. 【推荐】 慎用 Object 的 clone 方法来拷贝对象。

  • 说明: 对象 clone 方法默认是浅拷贝,若想实现深拷贝需覆写 clone 方法实现域对象的深度遍历式拷贝。

26. 【推荐】 类成员与方法 访问控制 从严:

  • 1)如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private。
  • 2)工具类不允许有 public 或 default 构造方法。
  • 3)类非 static 成员变量并且与子类共享,必须是 protected 。
  • 4)类非 static 成员变量并且仅在本类使用,必须是 private。
  • 5)类 static 成员变量如果仅在本类使用,必须是 private。
  • 6)若是 static 成员变量,考虑是否为 final。
  • 7)类成员方法只供类内部调用,必须是 private。
  • 8)类成员方法只对继承类公开,那么限制为 protected。

说明: 任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块 解耦 。思考:如果是一个private 的方法,想删除就删除,可是一个 public 的 service 成员方法或成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你会担心的。

(五) 日期时间

1. 【强制】 日期格式化时,传入 pattern 中表示年份统一使用小写的 y。

  • 说明: 日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7 之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY 就是下一年。
  • 正例: 表示日期和时间的格式如下所示:
 new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") 
  • 反例: 某程序员因使用 YYYY/MM/dd 进行日期格式化,2017/12/31 执行结果为 2018/12/31,造成线上故障。

2. 【强制】 在日期格式中分清楚大写的 M 和小写的 m,大写的 H 和小写的 h 分别指代的意义。

  • 说明: 日期格式中的这两对 字母表 意如下:
  • 1)表示月份是大写的 M
  • 2)表示分钟则是小写的 m
  • 3) 24 小时制 的是大写的 H
  • 4)12 小时制的则是小写的 h

3. 【强制】 获取当前毫秒数:System.currentTimeMillis();而不是 new Date().getTime()。

  • 说明: 获取纳秒级时间,则使用 System.nanoTime 的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。

4. 【强制】 不允许在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp。

  • 说明: 第 1 个不记录时间,getHours() 抛出异常;第 2 个不记录日期,getYear() 抛出异常;第 3 个在构造方法
 super((time /) * 1000),在  Timestamp  属性 fastTime 和 nanos 分别存储秒和纳秒信息。 
  • 反例: java.util.Date.after(Date) 进行时间比较时,当入参是 java.sql.Timestamp 时,会触发 JDK BUG (JDK9 已修复) ,可能导致比较时的意外结果。

5. 【强制】 禁止在程序中写死一年为 365 天,避免在公历闰年时出现日期转换错误或程序逻辑错误。

正例:

 // 获取今年的天数
int daysOfThisYear = LocalDate.now().lengthOfYear();
// 获取指定某年的天数
LocalDate.of(, 1, 1).lengthOfYear(); 

反例:

 // 第一种情况:在闰年 天时,出现数组越界异常
int[] dayArray = new int[];
// 第二种情况:一年有效期的会员制, 年 1 月 26 日注册,硬编码 365 返回的却是 2021 年 1 月 25 日
Calendar calendar = Calendar. getInstance ();
calendar.set(, 1, 26);
calendar.add(Calendar.DATE,); 

6. 【推荐】 避免公历闰年 2 月问题。闰年的 2 月份有 29 天,一年后的那一天不可能是 2 月 29 日。

7. 【推荐】 使用 枚举值 来指代月份。如果使用数字,注意 Date,Calendar 等日期相关类的月份 month 取值范围从 0 到 11 之间。

  • 说明: 参考 JDK 原生注释,Month value is 0-based. e.g., 0 for January.
  • 正例: Calendar.JANUARY,Calendar.FEBRUARY,Calendar.MARCH 等来指代相应月份来进行传参或比较

(六) 集合处理

15. 【强制】 在 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 比较结果相同。

反例: 下例中没有处理相等的情况,交换两个对象判断结果并不互反,不符合第一个条件,在实际使用中可能会出现异常。

 new Comparator<Student>() {
    @Override
    public int compare(Student o, Student o2) {
        return o.getId() > o2.getId() ? 1 : -1;
    }
}; 

16. 【推荐】 泛型集合使用时,在 JDK7 及以上,使用 diamond 语法或全省略。

说明: 菱形 泛型 ,即 diamond,直接使用<>来指代前边已经指定的类型。

正例:

 // diamond 方式,即<>
 HashMap <String, String> userCache = new HashMap<>(16);
// 全省略方式
ArrayList<User> users = new ArrayList(); 

17. 【推荐】 集合初始化时,指定集合初始值大小。

  • 说明: HashMap 使用构造方法 HashMap(int initialCapacity) 进行初始化时,如果暂时无法确定集合大小,那么指定默认值(16)即可。
  • 正例: initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loaderfactor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
  • 反例: HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素增加而被迫不断扩容,resize() 方法总共会调用 8 次,反复重建哈希表和数据迁移。当放置的集合元素个数达千万级时会影响程序性能。

18. 【推荐】 使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。

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

(七) 并发处理

13. 【推荐】 资金相关的金融敏感信息 使用悲观锁策略

  • 说明: 乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新
  • 正例: 悲观锁遵循 一锁二判三更新四释放 的原则。

14. 【推荐】 使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果。

  • 说明: 注意,子线程抛出异常堆栈,不能在主线程 try-catch 到。

15. 【推荐】 避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致的性能下降。

  • 说明: Random 实例包括 java.util.Random 的实例或者 Math.random() 的方式。
  • 正例: 在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要编码保证每个线程持有一个单独的 Random 实例。

16. 【推荐】 通过双重检查锁(double-checked locking),实现延迟初始化需要将目标属性声明为volatile 型,(比如修改 helper 的属性声明为 private volatile Helper helper = null;)。

正例:

 public class LazyInitDemo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }
    // other methods and fields...
} 

17. 【参考】 volatile 解决多线程内存不可见问题对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。

说明: 如果是 count++操作,使用如下类实现:

 AtomicInteger count = new AtomicInteger();
count.addAndGet(); 

如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。

18. 【参考】 HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在开发过程中注意规避此风险。

19. 【参考】 ThreadLocal 对象使用 static 修饰,ThreadLocal 无法解决共享对象的更新问题。

  • 说明: 这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在
  • 类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。

(八) 控制语句

10. 【推荐】 循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)。

11. 【推荐】 避免采用取反逻辑运算符。

  • 说明: 取反逻辑不利于快速理解,并且取反逻辑写法一般都存在对应的正向逻辑写法。
  • 正例: 使用 if(x < 628) 来表达 x 小于 628。
  • 反例: 使用 if(!(x >= 628)) 来表达 x 小于 628。

12. 【推荐】 公开接口需要进行入参保护,尤其是批量操作的接口。

  • 反例: 某业务系统,提供一个用户批量查询的接口,API 文档上有说最多查多少个,但接口实现上没做任何保护,导致调用方传了一个 1000 的用户 id 数组过来后,查询信息后,内存爆了。

13. 【参考】 下列情形,需要进行参数校验:

  • 1)调用频次低的方法。
  • 2)执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回
  • 退,或者错误,那得不偿失。
  • 3)需要极高稳定性和可用性的方法。
  • 4)对外提供的开放接口,不管是 RPC / API / HTTP 接口。
  • 5)敏感权限入口。

14. 【参考】 下列情形,不需要进行参数校验:

  • 1)极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查。
  • 2)底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般 DAO层与 Service 层都在同一个应用中,部署在同一台服务器中,所以 DAO 的参数校验,可以省略。
  • 3)被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数。

(九) 注释规约

10. 【参考】 对于注释的要求:第一、能够准确反映设计思想和代码逻辑;第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看的,使其能够快速接替自己的工作。

11. 【参考】 好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的另一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释又是相当大的负担。

反例:

 // put elephant into fridge
put(elephant, fridge); 

方法名 put,加上两个有意义的变量名称 elephant 和 fridge,已经说明了这是在干什么,语义清晰的代码不需要额外的注释。

12. 【参考】 特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。

  • 1)待办事宜( TODO ):(标记人,标记时间,[预计处理时间])表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个Javadoc 标签)。
  • 2)错误,不能工作( FIXME ):(标记人,标记时间,[预计处理时间])在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。

二、异常日志

(一) 错误码

10. 【参考】 错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码

  • 说明: 在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码,分别是:A0001(用户端错误)、B0001(系统执行出错)、C0001(调用第三方服务出错)。
  • 正例: 调用第三方服务出错是一级,中间件错误是二级,消息服务出错是三级。

11. 【参考】 错误码的后三位编号与 HTTP 状态码没有任何关系

12. 【参考】 错误码有利于不同文化背景的开发者进行交流与代码协作

说明: 英文单词形式的错误码不利于非英语母语国家(如阿拉伯语、希伯来语、俄罗斯语等)之间的开发者互相协作。

13. 【参考】 错误码即人性,感性认知+口口相传,使用纯数字来进行错误码编排不利于感性记忆和分类。

  • 说明: 数字是一个整体,每位数字的地位和含义是相同的。
  • 反例: 一个五位数字 12345,第 1 位是错误等级,第 2 位是错误来源,345 是编号,人的大脑不会主动地拆开并分辨每位数字的不同含义。

(二) 异常处理

11. 【推荐】 防止 NPE 是程序员的基本修养 注意 NPE 产生的场景:

  • 1)返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE
  • 反例: public int method() { return Integer 对象; },如果为 null,自动解箱抛 NPE。
  • 2)数据库的查询结果可能为 null。
  • 3)集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
  • 4)远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
  • 5)对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
  • 6)级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。

正例: 使用 JDK8 的 Optional 类来防止 NPE 问题。

12. 【推荐】 定义时区分 unchecked / checked 异常 避免直接抛出 new RuntimeException() 更不允许抛出 Exception 或者 Throwable 应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException 等。

13. 【参考】 对于公司外的 http / api 开放接口必须使用错误码 而应用内部推荐异常抛出;跨应用间RPC 调用优先考虑 使用 Result 方式 ,封装 isSuccess() 方法、错误码、错误简短信息;应用内部推荐异常抛出。

说明: 关于 RPC 方法返回方式使用 Result 方式的理由:

1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。

2)如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多。

如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。

三、单元测试

12. 【推荐】 对于不可测的代码在适当的时机做必要的重构,使代码变得可测避免为了达到测试要求而书写不规范测试代码。

13. 【推荐】 在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好覆盖所有测试用例(UC)。

14. 【推荐】 单元测试作为一种质量保障手段,在项目提测前完成单元测试,不建议项目发布后补充单元测试用例。

15. 【参考】 为了更方便地进行单元测试,业务代码应避免以下情况:

  • 构造方法中做的事情过多。
  • 存在过多的全局变量和静态方法。
  • 存在过多的外部依赖。
  • 存在过多的条件语句。

说明: 多层条件语句建议使用卫语句、策略模式、状态模式等方式重构。

16. 【参考】 不要对单元测试存在如下误解:

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

四、安全规约

8. 【强制】 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损。

说明: 如注册时发送验证码到手机,如果没有限制次数和频率,那么可以利用此功能骚扰到其它用户,并造成短信平台资源浪费。

9. 【强制】 对于文件上传功能,需要对于文件大小、类型进行严格检查和控制。

说明: 攻击者可以利用上传漏洞,上传恶意文件到服务器,并且远程执行,达到控制网站服务器的目的。

10. 【强制】 配置文件中的密码需要加密。

11. 【推荐】 发贴、评论、发送等即时消息,需要用户输入内容的场景。必须实现防刷、内容违禁词过滤等风控策略。

五、MySQL数据库

(一) 建表规约

15. 【推荐】 单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。

说明: 如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。

16. 【参考】 合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。

正例: 无符号值可以避免误存负数,且扩大了表示范围:

(二) 索引规约

9. 【推荐】 建组合索引的时候,区分度最高的在最左边。

  • 正例: 如果 where a = ? and b = ?,a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。
  • 说明: 存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where c > ? and d = ? 那么即使c 的区分度更高,也必须把 d 放在索引的最前列,即建立组合索引 idx_d_c。

10. 【推荐】 防止因字段类型不同造成的隐式转换,导致索引失效。

11. 【参考】 创建索引时避免有如下极端误解:

  • 1)索引宁滥勿缺。认为一个查询就需要建一个索引。
  • 2)吝啬索引的创建。认为索引会消耗空间、严重拖慢记录的更新以及行的新增速度。
  • 3)抵制唯一索引。认为唯一索引一律需要在应用层通过“先查后插”方式解决。

(三) SQL语句

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

12. 【参考】 因国际化需要,所有的字符存储与表示,均采用 utf8 字符集,那么字符计数方法需要注意。

说明:

 SELECT LENGTH("轻松工作");--返回为
SELECT CHARACTER_LENGTH("轻松工作");--返回为 

如果需要存储表情,那么选择 utf8mb4 来进行存储,注意它与 utf8 编码的区别。

13. 【参考】 TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE无事务且不触发 trigger,有可能造成事故,故不建议在开发代码中使用此语句。

说明: TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。

(四) ORM映射

7. 【强制】 更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间。

8. 【推荐】 不要写一个大而全的数据更新接口。传入为 POJO 类,不管是不是自己的目标更新字段,都进行update table set c1 = value1 , c2 = value2 , c3 = value3;这是不对的。执行 SQL 时,不要更新无改动的字段,一是易出错;二是效率低;三是增加 binlog 存储。

9. 【参考】 @Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。

10. 【参考】 <isEqual>中的 compareValue 是与属性值对比的常量,一般是数字,表示相等时带上此条件;<isNotEmpty>表示不为空且不为 null 时执行;<isNotNull>表示不为 null 值时执行。

六、工程结构

(一) 应用分层

1. 【推荐】 根据业务架构实践,结合业界分层规范与流行技术框架分析,推荐分层结构如图所示,默认上层依赖于下层,箭头关系表示可直接依赖,如:开放 API 层可以依赖于 Web 层(Controller 层),也可以直接依赖于 Service 层,依此类推:

(二) 二方库依赖

11. 【推荐】 二方库不要有配置项,最低限度不要再增加配置项。

12. 【推荐】 不要使用不稳定的工具包或者 Utils 类。

  • 说明:不稳定指的是提供方无法做到向下兼容,在编译阶段正常,但在运行时产生异常,因此,尽量使用业界稳定的二方工具包。

13. 【参考】 为避免应用二方库的依赖冲突问题,二方库发布者应当遵循以下原则:

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

(三) 服务器

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

7. 【推荐】 了解每个服务大致的平均耗时,可以通过独立配置线程池,将较慢的服务与主线程池隔离开,免得不同服务的线程同归于尽。

8. 【参考】 服务器内部重定向必须使用 forward;外部部重定向地址必须使用 URL Broker 生成,否则因线上采用 HTTPS 协议而导致浏览器提示“不安全”。此外,还会带来 URL 维护不一致的问题。

七、设计规约

ps:新增11条新规约!

例如,浮点数的后缀统一为大写;枚举的属性字段必须是私有且不可变;配置文件中的密码需要加密等。

新增描述中的正反例 2 条

例如,多个构造方法次序、NoSuchMethodError 处理;

新增扩展说明 5 条

例如,父集合元素的增加或删除异常等。

修改描述 22 处

例如,魔法值的示例代码、ScheduledThreadPool 问题等。

修正嵩山版中部分代码格式错误和描述错误。

无规矩不成方圆 无规范不能协作

众所周知,制订交通法规表面上是要限制行车权,实际上是保障公众的人身安全。试想如果没有限速,没有红绿灯,没有规定靠右行驶,谁还敢上路行驶。