《阿里巴巴Java开发手册》要点整理

别人都说我们是搬砖的码农,可我们知道自己是追求个性的艺术家。也许我们不会过多在意自己的外表和穿着,但在我们不羁的外表下,骨子里追求着代码的美、系统的美,代码规范其实就是一个对程序美的定义。

声明:本文是对阿里巴巴Java开发手册 v1.4.0 的一个整理。其中去掉了IDE可以帮助我们实现的部分,只整理出个人认为较为重要但在实际开发中易被忽略的点。

编程规约

  1. POJO 类中布尔类型的变量,都不要加 is 前缀 ,否则部分框架解析会引起序列化错误。
    反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是isDeleted() , RPC框架在反向解析的时候,“误以为”对应的属性名称是 deleted ,导致属性获取不到,抛出异常。
  2. Service/DAO 层方法命名前缀:
    1) 获取单个对象用 get,多个对象用 list。如:listObjects
    2) 获取统计值用 count
    3) 插入用 save/insert,删除用 remove/delete,修改用update
  3. 领域模型命名规约:
    1) 数据对象: xxxDO , xxx 即为数据表名。
    2) 数据传输对象: xxxDTO , xxx 为业务领域相关的名称。
    3) 展示对象: xxxVO , xxx 一般为网页名称。
    4) POJO 是 DO / DTO / BO / VO 的统称,禁止命名成 xxxPOJO 。
  4. 不允许任何魔法值直接出现在代码中。

    魔法值(魔法数字),在编程领域指的是莫名其妙出现的数字。数字的意义必须通过详细阅读才能推断出来。一般魔法数字都需要使用枚举变量来替换。

反例:

1
2
String key = "Id#prchen_" + tradeId;
cache.put(key, value);

正例:

1
2
3
String PRE_KEY="Id#prchen_"
String key = PRE_KEY + tradeId;
cache.put(key, value);
  1. IDE 的 text file encoding 设置为 UTF -8 ; IDE 中文件的换行符使用 Unix 格式,不要使用 Windows 格式

  2. 不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性。任何情形,没有必要插入多个空行进行隔开。

  3. 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。

  4. 所有的覆写方法,必须加@ Override 注解。可以准确判断是否覆盖成功。

  5. 尽量不用可变参数编程。可变参数必须放置在参数列表的最后。

  6. Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用
    equals
    。如"prchen".equals(object);

  7. 所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。
    说明:对于Integer var = ? 在-128至127范围内的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals方法进行判断。

    1
    2
    3
    4
    5
    6
    7
    Integer a = 66;
    Integer b = 66;
    Integer c = 666;
    Integer d = 666;

    System.out.println(a == b);//输出true
    System.out.println(c == d);//输出false
  8. POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。
    1) 所有的 POJO 类属性必须使用包装数据类型。
    2) RPC 方法的返回值和参数必须使用包装数据类型。
    3) 所有的局部变量使用基本数据类型。

  9. 禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法。框架在调用属性 xxx 的提取方法时,并不能确定哪个方法一定是被优先调用到。

  10. 慎用 Object 的 clone 方法来拷贝对象。对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现域对象的深度遍历式拷贝

  11. 类成员与方法访问控制从严
    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 成员方法或成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你会担心的。

  12. 关于 hashCode 和 equals 的处理:
    1) 只要重写 equals ,就必须重写 hashCode 。
    2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法。
    3) 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals 。

  13. 使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其 add/remove/clear 方法,否则会抛异常。 asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法,是适配器模式的体现。

  14. JDK 7 版本及以上, 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 比较结果相同。

  15. 使用 entrySet 遍历 Map 类集合 KV ,而不是 keySet 方式进行遍历。
    keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出key 所对应的 value 。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK 8,使用 Map.foreach 方法,foreach 底层就是封装了 entrySet 。

  16. 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁

  17. 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。

  18. ThreadLocal 无法解决共享对象的更新问题, ThreadLocal 对象建议使用 static 修饰。

  19. 关于Javadoc注释:
    1) 类、类属性、类方法的注释必须使用 Javadoc 规范,使用/**内容*/格式,不得使用 // xxx方式。
    2) 所有的抽象方法 ( 包括接口中的方法 ) 必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
    3) 所有的类都必须添加创建者创建日期
    4) 与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可。
    5) 好的命名、代码结构是自解释的,注释力求精简准确、表达到位。以下注释是万万不可取的:

    1
    2
    // put elephant into fridge
    put(elephant, fridge);
  20. 后台输送给页面的变量必须加 $!{var} ——中间的感叹号。如果 var 等于 null 或者不存在,那么 ${var} 会直接显示在页面上。

  21. Math.random() 这个方法返回是 double 类型,注意取值的范围 0 ≤ x < 1,如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,可以直接使用 Random 对象的 nextInt 或者 nextLong 方法

异常日志

  1. 异常不要用来做流程控制,条件控制。异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
  2. 不要在 finally 块中使用 return 。finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。
  3. 防止 NPE ,是程序员的基本修养,注意 NPE 产生的场景:
    1) 返回类型为基本数据类型, return 包装数据类型的对象时,自动拆箱有可能产生 NPE 。
    2) 数据库的查询结果可能为 null 。
    3) 集合里的元素即使 isNotEmpty ,取出的数据元素也可能为 null 。
    4) 远程调用返回对象时,要进行空指针判断,防止 NPE 。
    5) Session 中获取的数据可能为 null 。
    6) 级联调用 obj.getA().getB().getC(); 易产生NPE。
    7) 可以使用 JDK8 的 Optional 类来防止 NPE 问题。
  4. 避免出现重复的代码 (Don’t Repeat Yourself),即 DRY 原则。抽取共性方法。
  5. 应用中的扩展日志 ( 如打点、临时监控、访问日志等 ) 命名方式:appName_logType_logName.log
    logType :日志类型,如 stats/monitor/access 等 ;logName :日志描述。好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,利于归类查找。
  6. 务必在 log4j.xml 中设置 additivity = false 。防止重复打印日志,浪费磁盘空间。

单元测试

  1. 好的单元测试必须遵守 AIR 原则(Automatic, Independent, Repeatable)。单元测试在线上运行时,感觉像空气 (AIR) 一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。

  2. 单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证

  3. 对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。

安全规约

  1. 用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入,禁止字符串拼接 SQL 访问数据库。
  2. 禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据。
  3. 表单、 AJAX 提交必须执行 CSRF 安全验证。

    CSRF (Cross-site request forgery)跨站请求伪造是一类常见编程漏洞。对于存在 CSRF 漏洞的应用/网站,攻击者可以事先构造好 URL ,只要受害者用户一访问,后台便在用户不知情的情况下对数据库中用户参数进行相应修改。

MySQL数据库

  1. 表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint(1 表示是,0 表示否)。

  2. 小数类型为 decimal ,禁止使用 float 和 double 。 float 和 double 在存储的时候,存在精度损失的问题。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。

  3. 单表行数超过 500 万行或者单表容量超过 2 GB ,才推荐进行分库分表。三年内数据量达不到这个级别,就不要分库分表。

  4. 页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
    例:对列col1、列col2和列col3建一个联合索引

    1
    KEY test_col1_col2_col3 on test(col1,col2,col3);

    联合索引test_col1_col2_col3实际建立了(col1)(col1,col2)(col,col2,col3)三个索引

    1
    SELECT * FROM test WHERE col1 = 1 AND col2 < 2 AND col3 = 3;

上面这个查询语句执行时会依照最左前缀匹配原则,一直向右匹配直到遇到范围查询(>,<,BETWEEN,LIKE)就停止匹配,即会命中索引(col1, col2)

  1. 利用覆盖索引来进行查询操作,避免回表。
  2. 不要使用 count(列名)count(常量) 来替代 count(*)count(*) 会统计值为 NULL 的行,而 count(列名) 不会。
  3. 当某一列的值全是 NULL 时, count(col) 的返回结果为 0,但 sum(col) 的返回结果为NULL ,因此使用 sum() 时需注意 NPE 问题
  4. POJO 类的布尔属性不能加 is ,而数据库字段必须加 is_,要求在 resultMap 中进行字段与属性之间的映射。
  5. sql.xml 配置参数使用:#{}#param# 不要使用${} 此种方式容易出现 SQL 注入

工程结构

  1. 应用分层:开放接口层、终端显示层、Web层、Service层、Manager层、DAO层、外部接口或第三方平台(如下图)

    特别说明, Manager 层是通用业务处理层,作用如下:
    1) 对第三方平台封装,预处理返回结果及转化异常信息
    2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理
    3) 与 DAO 层交互,对多个 DAO 的组合复用

设计规约

  1. 谨慎使用继承的方式来进行扩展,优先使用聚合/组合的方式来实现。
  2. 系统设计时,根据依赖倒置原则,尽量依赖抽象类与接口,有利于扩展与维护。
  3. 避免如下误解:敏捷开发 = 讲故事 + 编码 + 发布。敏捷开发是快速交付迭代可用的系统,省略多余的设计方案,摒弃传统的审批流程,但核心关键点上的必要设计和文档沉淀是需要的。
  4. 系统架构设计的目的:
    1) 确定系统边界。确定系统在技术层面上的做与不做。
    2) 确定系统内模块之间的关系。确定模块之间的依赖关系及模块的宏观输入与输出。
    3) 确定指导后续设计与演化的原则。使后续的子系统或模块设计在规定的框架内继续演化。
    4) 确定非功能性需求。非功能性需求是指安全性、可用性、可扩展性等。

最后,引用作者孤尽在手册中的一句话:

很多编程方式客观上没有对错之分,一致性很重要,可读性很重要,团队沟通效率很重要。有一个理论叫帕金森琐碎定律:一个组织中的成员往往会把过多的精力花费在一些琐碎的争论上。程序员天生需要团队协作,而协作的正能量要放在问题的有效沟通上。个性化应尽量表现在系统架构和算法效率的提升上,而不是在合作规范上进行纠缠不休的讨论、争论,最后没有结论。

扫一扫,关注我的微信公众号↓

0%