阅读阿里Java开发手册记录

时间:2023-03-10 02:39:18
阅读阿里Java开发手册记录

概述

在阅读完阿里Java开发手册(嵩山版)后,发现自己在开发过程中有一些没有按照规范开发的情况,这里将容易忘记的规范记录下来,并且添加自己的理解,一方面方便自己巩固记忆,另一方面希望对其他同学能够提供一下帮助。这里将收集的阿里Java开发手册文档放在了 github 仓库,更多详细内容可以下载文档查看。

编程规约

  1. 【强制】 抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类 命名以它要测试的类的名称开始,以 Test 结尾。

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

    说明:在本文 MySQL 规约中的建表约定第一条,表达是与否的变量采用 is_xxx 的命名方式,所以需要在 <resultMap> 设置从 is_xxxxxx 的映射关系。

    反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),框架在反向解析的时 候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。

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

    说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。例如使用了代理模式可以命名为:public class LoginProxy;

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

    说明:枚举其实就是特殊的常量类,且构造方法被默认强制是私有。

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

  6. 【强制】 采用 4 个空格缩进,禁止使用 Tab 字符。

    说明:如果使用 Tab 缩进,必须设置 1 个 Tab 为 4 个空格。IDEA 设置 Tab 为 4 个空格时,请勿勾选 Use tab character;而在 Eclipse 中,必须勾选 insert spaces for tabs。

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

  8. 【强制】 Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。

    说明:推荐使用 JDK7 引入的工具类 java.util.Objects.equals(Object a, Object b),此方法的内部实现为:return (a == b) || (a != null && a.equals(b));

  9. 关于基本数据类型与包装数据类型的使用标准如下:

    1)【强制】 所有的 POJO 类属性必须使用包装数据类型。

    2)【强制】 RPC 方法的返回值和参数必须使用包装数据类型。

    3)【推荐】所有的局部变量使用基本数据类型。

    说明:POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或 者入库检查,都由使用者来保证。数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。

  10. 【强制】 定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值。

  11. .【强制】 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。

  12. 【强制】 POJO 类必须写 toString 方法。使用 IDE 中的工具:source> generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString()

    说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排查问题。

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

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

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

    说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime() 的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。

  15. 【推荐】前后端的时间格式统一为"yyyy-MM-dd HH:mm:ss",统一为 GMT。

  16. 【强制】 在前后端交互的 JSON 格式数据中,所有的 key 必须为小写字母开始的 lowerCamelCase 风格,符合英文表达习惯,且表意完整。

    正例:errorCode / errorMessage / assetStatus / menuList / orderList / configFlag

集合处理

  1. 【强制】 只要覆写 equals,就必须覆写 hashCode

    说明:String 因为覆写了 hashCodeequals 方法,所以可以愉快地将 String 对象作为 Map 的 key 来使用。

  2. 【强制】 判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size()==0 的方式。

    说明:在某些集合中,前者的时间复杂度为 O(1),而且可读性更好。

  3. 【强制】 使用集合转数组的方法,必须使用集合的 toArray(T[] array)方法,传入的是类型完全一致、长度为 0 的空数组。

    说明:直接使用 toArray() 无参方法存在问题,此方法返回值只能是 Object[] 类,若强转其它类型数组将出现 ClassCastException 错误。正例:String[] array = list.toArray(new String[0]);

  4. 【强制】 使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法, 它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。

    说明:asList() 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。

  5. 【强制】 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。

  6. .【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:

    集合类 key(键) value(值) super(父类) 说明
    Hashtable 不允许为 null 不允许为 null Dictionary 线程安全
    ConcurrentHashMap 不允许为 null 不允许为 null AbstractMap 线程安全,锁分段(jdk8:CAS)
    TreeMap 不允许为 null 允许为 null AbstractMap 线程不安全
    HashMap 允许为 null 允许为 null AbstractMap 线程不安全

    反例:由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上,存储 null 值时会抛出 NPE 异常。

并发处理

  1. 【强制】 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

  2. 【强制】 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

    说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

  3. 【强制】 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

    说明:Executors 返回的线程池对象的弊端如下:

    1) FixedThreadPoolSingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM

    2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

  4. 【强制】 SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static, 必须加锁,或者使用 DateUtils 工具类。

    说明:如果是 JDK8 的应用,可以使用 Instant 代替 DateLocalDateTime 代替 CalendarDateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe

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

    说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。

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

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

    正例:悲观锁遵循一锁、二判、三更新、四释放的原则。\

  7. 【强制】 在高并发场景中,避免使用”等于”判断作为中断或退出的条件。

    说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件 来代替。

    反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数, 这样的话,活动无法终止。

异常处理

  1. **【强制】 **生产环境禁止直接使用 System.outSystem.err 输出日志或使用 e.printStackTrace()打印异常堆栈。

    说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易 造成文件大小超过操作系统大小限制。

  2. 【强制】 错误码使用者避免随意定义新的错误码。

    说明:尽可能在原有错误码附表中找到语义相同或者相近的错误码在代码中使用即可。

  3. 【强制】 错误码不能直接输出给用户作为提示信息使用。

    说明:堆栈、错误信息、错误码、提示信息 是一个有效关联并互相转义的和谐整体,但是请勿互相越俎代庖。

  4. 【强制】 捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请 将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的 内容。

  5. 【强制】 事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务。

  6. 【强制】 finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。

    说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。

  7. 【强制】 异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过 关键字 throws 往上抛出。

  8. 【强制】 日志打印时禁止直接用 JSON 工具将对象转换成 String。

    说明:如果对象里某些 get 方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流 程的执行。

    正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法

  9. 【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑 爆,并记得及时删除这些观察日志。

    说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些 日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?

MySQL 数据库

建表规约

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

    说明:任何字段如果为非负数,必须是 unsigned。

    注意:POJO 类中的任何布尔类型的变量,都不要加 is 前缀,所以,需要在设置从 is_xxx 到 Xxx 的映射关系。数据库表示是与否的值,使用 tinyint 类型,坚持 is_xxx 的命名方式是为了明确其取值含 义与取值范围。

    正例:表达逻辑删除的字段名 is_deleted,1 表示删除,0 表示未删除。

  2. 【强制】 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只 出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。

  3. 【强制】 表名不使用复数名词。

  4. 【强制】 禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字。

  5. 【强制】 主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。

    说明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称。

  6. 【强制】 小数类型为 decimal,禁止使用 floatdouble

    说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的 结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。

  7. 【强制】 如果存储的字符串长度几乎相等,使用 char 定长字符串类型。

  8. 【强制】 varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度 大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。

  9. 【强制】 表必备三字段:id, create_time, update_time

    说明:其中 id 必为主键,类型为 bigint unsigned、单表时自增、步长为 1。create_time, update_time 的类型均为 datetime 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新。

索引规约

  1. 【强制】 业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。

    说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外, 即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。

  2. 【强制】 超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时, 保证被关联的字段需要有索引。

    说明:即使双表 join 也要注意表索引、SQL 性能。

  3. 【强制】 在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据 实际文本区分度决定索引长度。

    说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90% 以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。

  4. 【强制】 页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。

    说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。

  5. 【推荐】利用覆盖索引来进行查询操作,避免回表。

    正例:能够建立索引的种类分为主键索引、唯一索引、普通索引三种,而覆盖索引只是一种查询的一种效 果,用 explain 的结果,extra 列会出现:using index。

  6. 【推荐】利用延迟关联或者子查询优化超多分页场景。

    说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。

    正例:先快速定位需要获取的 id 段,然后再关联: SELECT t1.* FROM 表 1 as t1, (select id from 表 1 where 条件 LIMIT 100000,20 ) as t2 where t1.id=t2.id

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

    正例:如果 where a=? and b=?,a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。

    说明:存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where c>? and d=? 那么即使 c 的区分度更高,也必须把 d 放在索引的最前列,即建立组合索引 idx_d_c。

SQL 语句

  1. 【强制】 不要使用 count(列名)count(常量)来替代 count(*)count(*)SQL92 定义的标 准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。

    说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。

  2. 【强制】 count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。

  3. 【强制】 当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为 NULL,因此使用 sum()时需注意 NPE 问题。

    正例:可以使用如下方式来避免 sum 的 NPE 问题:SELECT IFNULL(SUM(column), 0) FROM table;

  4. . 【强制】 使用 ISNULL()来判断是否为 NULL 值。

    反例:在 SQL 语句中,如果在 null 前换行,影响可读性。select * from table where column1 is null and column3 is not null; 而ISNULL(column)是一个整体,简洁易懂。从性能数据上分析,ISNULL(column) 执行效率更快一些。

  5. 【强制】 不得使用外键与级联,一切外键概念必须在应用层解决。

    说明:(概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学 生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机 低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库 的插入速度。

  6. 【强制】 禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。

  7. 【强制】 数据订正(特别是删除或修改记录操作)时,要先 select,避免出现误删除,确认无 误才能执行更新语句。

ORM 映射

  1. 【强制】 在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。

    说明:

    1)增加查询分析器解析成本。

    2)增减字段容易与 resultMap 配置不一致。

    3)无用字段增加网络 消耗,尤其是 text 类型的字段。

  2. 【强制】 不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要 定义;反过来,每一个表也必然有一个与之对应。

    说明:配置映射关系,使字段与 DO 类解耦,方便维护。

  3. 【强制】 sql.xml 配置参数使用:#{},#param# 不要使用 ${} 此种方式容易出现 SQL 注入。

  4. 【强制】 不允许直接拿 HashMap 与 Hashtable 作为查询结果集的输出。

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