由于排版问题,您可以点击原文链接以获得更佳阅读体验:
http://www.prchen.com/2019/06/12/阿里巴巴Java开发手册要点整理/#more
别人都说我们是搬砖的码农,可我们知道自己是追求个性的艺术家。也许我们不会过多在意自己的外表和穿着,但在我们不羁的外表下,骨子里追求着代码的美、系统的美,代码规范其实就是一个对程序美的定义。
声明:本文是对阿里巴巴Java开发手册 v1.4.0 的一个整理。其中去掉了IDE可以帮助我们实现的部分,只整理出个人认为较为重要但在实际开发中易被忽略的点。
编程规约
- POJO 类中布尔类型的变量,都不要加 is 前缀 ,否则部分框架解析会引起序列化错误。
反例:定义为基本数据类型Boolean isDeleted
的属性,它的方法也是isDeleted()
, RPC框架在反向解析的时候,“误以为”对应的属性名称是deleted
,导致属性获取不到,抛出异常。 - Service/DAO 层方法命名前缀:
- 获取单个对象用 get,多个对象用 list。如:
listObjects
- 获取统计值用 count
- 插入用 save/insert,删除用 remove/delete,修改用update
- 获取单个对象用 get,多个对象用 list。如:
- 领域模型命名规约:
- 数据对象: xxxDO , xxx 即为数据表名。
- 数据传输对象: xxxDTO , xxx 为业务领域相关的名称。
- 展示对象: xxxVO , xxx 一般为网页名称。
- POJO 是 DO / DTO / BO / VO 的统称,禁止命名成 xxxPOJO 。
- 不允许任何魔法值直接出现在代码中。
魔法值(魔法数字),在编程领域指的是莫名其妙出现的数字。数字的意义必须通过详细阅读才能推断出来。一般魔法数字都需要使用枚举变量来替换。
反例:
String key = "Id#prchen_" + tradeId;
cache.put(key, value);
正例:
String PRE_KEY="Id#prchen_"
String key = PRE_KEY + tradeId;
cache.put(key, value);
-
IDE 的 text file encoding 设置为 UTF -8 ; IDE 中文件的换行符使用 Unix 格式,不要使用 Windows 格式。
-
不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性。任何情形,没有必要插入多个空行进行隔开。
-
避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
-
所有的覆写方法,必须加@ Override 注解。可以准确判断是否覆盖成功。
-
尽量不用可变参数编程。可变参数必须放置在参数列表的最后。
-
Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用
equals 。如"prchen".equals(object);
-
所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。
说明:对于Integer var = ? 在-128至127范围内的赋值,Integer对象是在IntegerCache.cache
产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals方法进行判断。Integer a = 66; Integer b = 66; Integer c = 666; Integer d = 666; System.out.println(a == b);//输出true System.out.println(c == d);//输出false
-
POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。
- 所有的 POJO 类属性必须使用包装数据类型。
- RPC 方法的返回值和参数必须使用包装数据类型。
- 所有的局部变量使用基本数据类型。
-
禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法。框架在调用属性 xxx 的提取方法时,并不能确定哪个方法一定是被优先调用到。
-
慎用 Object 的 clone 方法来拷贝对象。对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现域对象的深度遍历式拷贝。
-
类成员与方法访问控制从严:
- 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private 。
- 工具类不允许有 public 或 default 构造方法。
- 类非 static 成员变量并且与子类共享,必须是 protected 。
- 类非 static 成员变量并且仅在本类使用,必须是 private 。
- 类 static 成员变量如果仅在本类使用,必须是 private 。
- 若是 static 成员变量,考虑是否为 final 。
- 类成员方法只供类内部调用,必须是 private 。
- 类成员方法只对继承类公开,那么限制为 protected 。
任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。这里作者用了一个很形象的比喻:
思考:如果是一个 private 的方法,想删除就删除,可是一个 public 的 service 成员方法或成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你会担心的。
-
关于 hashCode 和 equals 的处理:
- 只要重写 equals ,就必须重写 hashCode 。
- 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法。
- 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals 。
-
使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其 add/remove/clear 方法,否则会抛异常。 asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法,是适配器模式的体现。
-
JDK 7 版本及以上, Comparator 实现类要满足如下三个条件,不然 Arrays.sort, Collections.sort 会报 IllegalArgumentException 异常。
- x, y 的比较结果和 y , x 的比较结果相反。
- x > y , y > z ,则 x > z 。
- x = y ,则 x , z 比较结果和 y , z 比较结果相同。
-
使用 entrySet 遍历 Map 类集合 KV ,而不是 keySet 方式进行遍历。
keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出key 所对应的 value 。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK 8,使用 Map.foreach 方法,foreach 底层就是封装了 entrySet 。 -
对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
-
并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
-
ThreadLocal 无法解决共享对象的更新问题, ThreadLocal 对象建议使用 static 修饰。
-
关于Javadoc注释:
- 类、类属性、类方法的注释必须使用 Javadoc 规范,使用
/**内容*/
格式,不得使用// xxx
方式。 - 所有的抽象方法 ( 包括接口中的方法 ) 必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
- 所有的类都必须添加创建者和创建日期。
- 与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可。
- 好的命名、代码结构是自解释的,注释力求精简准确、表达到位。以下注释是万万不可取的:
// put elephant into fridge put(elephant, fridge);
- 类、类属性、类方法的注释必须使用 Javadoc 规范,使用
-
后台输送给页面的变量必须加 $!{var} ——中间的感叹号。如果 var 等于 null 或者不存在,那么 ${var} 会直接显示在页面上。
-
Math.random() 这个方法返回是 double 类型,注意取值的范围 0 ≤ x < 1,如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,可以直接使用 Random 对象的 nextInt 或者 nextLong 方法
异常日志
- 异常不要用来做流程控制,条件控制。异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
- 不要在 finally 块中使用 return 。finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。
- 防止 NPE ,是程序员的基本修养,注意 NPE 产生的场景:
- 返回类型为基本数据类型, return 包装数据类型的对象时,自动拆箱有可能产生 NPE 。
- 数据库的查询结果可能为 null 。
- 集合里的元素即使 isNotEmpty ,取出的数据元素也可能为 null 。
- 远程调用返回对象时,要进行空指针判断,防止 NPE 。
- Session 中获取的数据可能为 null 。
- 级联调用 obj.getA().getB().getC(); 易产生NPE。
- 可以使用 JDK8 的 Optional 类来防止 NPE 问题。
- 避免出现重复的代码 (Don’t Repeat Yourself),即 DRY 原则。抽取共性方法。
- 应用中的扩展日志 ( 如打点、临时监控、访问日志等 ) 命名方式:appName_logType_logName.log 。
logType :日志类型,如 stats/monitor/access 等 ;logName :日志描述。好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,利于归类查找。 - 务必在 log4j.xml 中**设置 additivity = false **。防止重复打印日志,浪费磁盘空间。
单元测试
-
好的单元测试必须遵守 AIR 原则(Automatic, Independent, Repeatable)。单元测试在线上运行时,感觉像空气 (AIR) 一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
-
单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。
-
对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。
安全规约
- 用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入,禁止字符串拼接 SQL 访问数据库。
- 禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据。
- 表单、 AJAX 提交必须执行 CSRF 安全验证。
CSRF (Cross-site request forgery)跨站请求伪造是一类常见编程漏洞。对于存在 CSRF 漏洞的应用/网站,攻击者可以事先构造好 URL ,只要受害者用户一访问,后台便在用户不知情的情况下对数据库中用户参数进行相应修改。
MySQL数据库
-
表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint(1 表示是,0 表示否)。
-
小数类型为 decimal ,禁止使用 float 和 double 。 float 和 double 在存储的时候,存在精度损失的问题。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。
-
单表行数超过 500 万行或者单表容量超过 2 GB ,才推荐进行分库分表。三年内数据量达不到这个级别,就不要分库分表。
-
页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
例:对列col1、列col2和列col3建一个联合索引
KEY test_col1_col2_col3 on test(col1,col2,col3);
联合索引test_col1_col2_col3
实际建立了(col1)
、(col1,col2)
、(col,col2,col3)
三个索引。
SELECT * FROM test WHERE col1 = 1 AND col2 < 2 AND col3 = 3;
上面这个查询语句执行时会依照最左前缀匹配原则,一直向右匹配直到遇到范围查询(>,<,BETWEEN,LIKE)
就停止匹配,即会命中索引(col1, col2)
。
- 利用覆盖索引来进行查询操作,避免回表。
- 不要使用
count(列名)
或count(常量)
来替代count(*)
。count(*)
会统计值为 NULL 的行,而count(列名)
不会。 - 当某一列的值全是 NULL 时,
count(col)
的返回结果为 0,但sum(col)
的返回结果为NULL ,因此使用sum()
时需注意 NPE 问题。 - POJO 类的布尔属性不能加 is ,而数据库字段必须加 is_,要求在 resultMap 中进行字段与属性之间的映射。
- sql.xml 配置参数使用:
#{}
,#param#
不要使用${}
此种方式容易出现 SQL 注入。
工程结构
- 应用分层:开放接口层、终端显示层、Web层、Service层、Manager层、DAO层、外部接口或第三方平台(如下图)
特别说明, Manager 层是通用业务处理层,作用如下:
- 对第三方平台封装,预处理返回结果及转化异常信息
- 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理
- 与 DAO 层交互,对多个 DAO 的组合复用
设计规约
- 谨慎使用继承的方式来进行扩展,优先使用聚合/组合的方式来实现。
- 系统设计时,根据依赖倒置原则,尽量依赖抽象类与接口,有利于扩展与维护。
- 避免如下误解:敏捷开发 = 讲故事 + 编码 + 发布。敏捷开发是快速交付迭代可用的系统,省略多余的设计方案,摒弃传统的审批流程,但核心关键点上的必要设计和文档沉淀是需要的。
- 系统架构设计的目的:
- 确定系统边界。确定系统在技术层面上的做与不做。
- 确定系统内模块之间的关系。确定模块之间的依赖关系及模块的宏观输入与输出。
- 确定指导后续设计与演化的原则。使后续的子系统或模块设计在规定的框架内继续演化。
- 确定非功能性需求。非功能性需求是指安全性、可用性、可扩展性等。
最后,引用作者孤尽在手册中的一句话:
很多编程方式客观上没有对错之分,一致性很重要,可读性很重要,团队沟通效率很重要。有一个理论叫帕金森琐碎定律:一个组织中的成员往往会把过多的精力花费在一些琐碎的争论上。程序员天生需要团队协作,而协作的正能量要放在问题的有效沟通上。个性化应尽量表现在系统架构和算法效率的提升上,而不是在合作规范上进行纠缠不休的讨论、争论,最后没有结论。
个人订阅号,欢迎关注,一起讨论交流↑