第一章 编程规约
命名风格
- 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
- 代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,即使纯拼音命名方式也要避免采用。
- 类名使用Uppercamelcase风格,但以下情形例外:Do/B0/DTo/vo/A0/PO/UID等。
方法名、参数名、成员变量、局部变量都统一使用1owerCamelCase风格,必须遵从驼峰形式。
常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
抽象类命名使用Abstract或Base开头;异常类命名使用Exception 结尾;测试类命名以它要测试的类的名称开始,以Test结尾。
类型与中括号紧挨相连来表示数组。
P0J0类中布尔类型的变量,都不要加is前缀,否则部分框架解析会引起序列化错误。
包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
杜绝完全不规范的缩写,避免望文不知义。
为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意。
如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。
接口类中的方法和属性不要加任何修饰符号(public也不要加),保持代码的简洁性,并加上有效的Javadoc注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量。
接口和实现类的命名有两套规则:
- 对于Service和DA0类,基于SOA的理念,暴露出来的服务一定是接口,内部的实现类用Impl的后缀与接口区别。
- 如果是形容能力的接口名称,取对应的形容词为接口名(通常是-able的形式)。
枚举类名建议带上Enum后缀,枚举成员名称需要全大写,单词间用下划线隔开。
各层命名规约:
A)Service/DA0层方法命名规约
1)获取单个对象的方法用get做前缀。
2)获取多个对象的方法用1ist做前缀,复数形式结尾如:listObjects。
3)获取统计值的方法用count做前缀。
4)插入的方法用save/insert做前缀。
5)删除的方法用remove/delete做前缀。
6)修改的方法用update做前缀。
B)领域模型命名规约
1)数据对象:xxxDO,xxx即为数据表名。
2)数据传输对象:xxXDTO,xxx为业务领域相关的名称。
3)展示对象:xxxVO,xxx一般为网页名称。
4)POJ0是DO/DTO/B0/Vo的统称,禁止命名成xxxP0J0。
常量定义
不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。
在1ong或者Long赋值时,数值后使用大写的L,不能是小写的1,小写容易跟数字1混淆,造成误解。
不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。
常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量。
1)跨应用共享常量:放置在二方库中,通常是client.jar中的constant目录下。
2)应用内共享常量:放置在一方库中,通常是子模块中的constant目录下。3)子工程内部共享常量:即在当前子工程的constant目录下。
4)包内共享常量:即在当前包下单独的constant目录下。
5)类内共享常量:直接在类内部 private static final定义。如果变量值仅在一个固定范围内变化用enum类型来定义。
代码格式
if/for/while/switch/do
等保留字与括号之间都必须加空格。- 采用 4 个空格缩进,禁止使用 Tab 控制符。
- 注释的双斜线与注释内容之间有且仅有一个空格。e.g.
// 这是示例注释
- 单行字符数不超过 120 个,超出则需要换行,换行遵循:
- 第二行相对第一行缩进 4 个空格,从第三行开始,不再缩进。
- 运算符与下文一起换行。
- 方法调用的点符号与下文一起换行。
- 方法调用的点符号与下文一起换行时,在逗号后进行。
- 在括号前不要换行。
1 | // 正例 |
- IDE 的 text file encoding 设置为 UTF-8;IDE 文件的换行符使用 UNIX 格式,不要使用 Windows 格式。
- 没有必要增加若干空格来使某一行的字符与上一行对应位置的字符对齐。
oop规约
慎用0bject的clone方法来拷贝对象。
类成员与方法访问控制从严:
1)如果不允许外部直接通过new来创建对象,那么构造方法必须是private。
2)工具类不允许有public或default构造方法。
3)类非static成员变量并且与子类共享,必须是protected。
4)类非static成员变量并且仅在本类使用,必须是private。
5)类static成员变量如果仅在本类使用,必须是private。
6)若是static成员变量,考虑是否为final。
7)类成员方法只供类内部调用,必须是private。
8)类成员方法只对继承类公开,那么限制为protected。
集合处理
- 所有相同类型的包装类对象之间值的比较,全部使用 equals 方法。
- 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。
- 慎用 Object 的 clone 方法来拷贝对象。
说明:对象的 clone 方法默认是浅拷贝,若想实现深拷贝,需要重写 clone 方法。 - 关于 hashCode 和 equals 的处理,遵循如下规则:
- 只要重写 equals,就必须重写 hashCode;
- 因为 Set 存储的是不重复对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法。
- 如果自定义对象作为 Map 的键,那么必须重写这两个方法。
- 说明:String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地将 String 对象作为 key 来使用。
- ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出
ClassCastException
异常。
说明:subList 是 ArrayList 的一个视图,对于 subList 子列表的所有操作最终会反映到原列表上。 - 在 subList 场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均产生
ConcurrentModificationException
。
1 | List<Integer> list = new ArrayList<>(); |
- 在使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法,否则会抛出
UnsupportedOperationException
异常。
说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。体现的是适配器模式,只是转换接口,后台的数据仍是数组。
1 | String[] str = new String[] {"you", "wu"}; |
- 在集合初始化时,指定集合初始值大小。若 HashMap 需要放置 1024 个元素,由于没有设置初始大小(默认 16),随着元素不断增加,容量被迫扩大 7 次,resize 需要重建 hash 表,这严重影响性能。
- 使用 entrySet 遍历 Map 类集合 K/V,而不是 keySet 方式遍历。如果时 JDK8,使用 Map.foreach() 方法。
- 高度注意 Map 类集合 K/V 能不能存储 null 值的情况。由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值的,而事实上,存储 null 值时会抛出 NPE 异常。
集合类 | Key | Value | Supper | 说明 |
---|---|---|---|---|
Hashtable | 不允许为 null | 不允许为 null | Dictionary | 线程安全 |
ConcurrentHashMap | 不允许为 null | 不允许为 null | AbstractMap | 锁分段技术 |
TreeMap | 不允许为 null | 允许为 null | AbstractMap | 线程不安全 |
HashMap | 允许为 null | 允许为 null | AbstractMap | 线程不安全 |
- 利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的 contains 方法进行遍历、对比、去重操作。
并发处理
- 在创建线程或线程池时,请指定有意义的线程名称,方便出错时回溯。
1 | public class TimeTaskThread extends Thread { |
- 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间及系统资源,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大流量同类线程而导致消耗完内存或者“过度切换”的问题。 - 在对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
说明:如果线程一需要对表 A/B/C 依次加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A/B/C,否则可能出现死锁。 - volatile 解决多线程内存不可见问题。对于一写多读,可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
控制语句
- 在一个 switch 块内,每个 case 要么通过 break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码都没有。
- 在高并发场景中,避免使用“等于”判断作为中断或退出的条件。
说明:如果并发控制没有处理好,容易产生等值判断被击穿的情况,应使用大于或小于的区间判断条件来代替。 - 不要在条件判断中执行其它复杂的语句,可将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
注释规约
- 特殊注释标记。TODO 实际上是一个 Javadoc 的标签,虽然目前的 Javadoc 还没有实现,但已经被广泛使用,且只能应用于类、接口和方法上。在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正。
其他
- 注意 Math.random() 这个方法返回的是 double 类型,取值范围 x ∈ [0, 1),如果想获得整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法。
第二章 异常日志
异常处理
- catch 时请分清稳定代码和不稳定代码。稳定代码指的是无论如何都不会出错的代码。对于非稳定代码的 catch,尽可能在进行异常类型的区分后,再做对应的异常处理。
- 不要在 finally 块中使用 return。
说明:当 finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。 - 定义时区分 unchecked/checked 异常,避免直接抛出 new RuntimeException(),更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如 DAOException/ServiceException 等。
日志规约
- 应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 SLF4J 中的 API。使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
1 | import org.slf4j.Logger; |
- 谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用 warn 记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并及时删除这些观察日志。
第三章 单元测试
- 单元测试是可重复执行的,不能受到外界环境的影响。
- 和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。
- 单元测试作为一种质量保障手段,不建议项目发布后补充单元测试用例,建议在项目提测前完成单元测试。
第四章 安全规范
- 隶属于用户个人的页面或者功能必须进行权限控制校验。
- 用户敏感数据禁止直接展示,必须对展示数据进行脱敏。
- 用户输入的sQL参数严格使用参数绑定或者METADATA字段值限定,防止5QL注入,禁止字符串拼接SQL访问数据库。
- 用户请求传入的任何参数必须做有效性验证。
- 禁止向HTML页面输出未经安全过滤或未正确转义的用户数据。
- 表单、AJAX提交必须执行CSRF安全验证。
- 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损。
- 发贴、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过滤等风控策略。
第五章 mysql数据库
建表规约
- 表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型为
unsigned tinyint
。
说明:任何字段如果为非负数,则必须是 unsigned。 - 字段允许适当冗余,以提高查询性能,但必须考虑数据一致。e.g. 商品类目名称使用频率高,字段长度短,名称基本一成不变,可在相关联的表中冗余存储类目名称,避免关联查询。
- 冗余字段遵循:
- 不是频繁修改的字段;
- 不是 varchar 超长字段,更不能是 text 字段。
索引规约
- 在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。
- 页面搜索严禁左模糊或者全模糊,如果需要请通过搜索引擎来解决。
说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。 - 如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。
- 正例:where a=? and b=? order by c; 索引: a_b_c。
- 反例:索引中有范围查找,那么索引有序性无法利用,如 WHERE a>10 ORDER BY b; 索引 a_b 无法排序。
- 利用延迟关联或者子查询优化超多分页场景。
说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 的行,返回 N 行。当 offset 特别大的时候,效率会非常的低下,要么控制返回的总页数,要么对超过阈值的页数进行 SQL 改写。 - 建组合索引的时候,区分度最高的在最左边。
- SQL 性能优化的目标,至少要达到 range 级别,要求是 ref 级别,最好是 consts。
SQL 语句
- 不要使用 count(列名) 或 count(常量) 来替代 count(),count() 是 SQL92 定义的标准统计行数的语句,跟数据库无关,跟 NULL 和非 NULL 无关。
说明:count(*) 会统计值为 NULL 的行,而 count(列名) 不会统计此列为 NULL 值的行。 count(distinct column)
计算该列除 NULL 外的不重复行数。注意,count(distinct column1,column2)
如果其中一列全为 NULL,那么即使另一列用不同的值,也返回为 0。- 当某一列的值全为 NULL 时,
count(column)
的返回结果为 0,但sum(column)
的返回结果为 NULL,因此使用 sum() 时需注意 NPE 问题。
可以使用如下方式来避免 sum 的 NPE 问题。
1 | SELECT IF(ISNULL(SUM(g), 0, SUM(g))) FROM table; |
- 使用
ISNULL()
来判断是否为 NULL 值。
说明:NULL 与任何值的直接比较都为 NULL。 - 不得使用外键与级联,一切外键概念必须在应用层解决。
说明:以学生和成绩的关系为例,学生表的 student_id 是主键,成绩表的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。 - 禁止使用存储过程。存储过程难以调试和扩展,更没有移植性。
in
操作能避免则避免。若实在避免不了,需要仔细评估 in 后面的集合元素数量,控制在 1000 个之内。
ORM 映射
- POJO 类的布尔属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行字段与属性的映射。
sql.xml
配置参数使用:#{}, #param#
,不要使用 ${},此种方式容易出现 SQL 注入。@Transactional
事务不要滥用。事务会影响数据库的 QPS。另外,使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
第六章 工程结构
应用分层
- 在 DAO 层,产生的异常类型有很多,无法用细粒度的异常进行 catch,因此使用
catch(Exception e)
方式,并 throw newDAOException(e)
,不需要打印日志,因为日志在 Manager/Service 层,一定需要捕获并写到日志文件中去。如果同台服务器再写日志,会浪费性能和存储。
二方库依赖
- 定义 GAV 遵从以下规则:
- GroupID 格式:com.{公司/BU}.业务线.[子业务线],最多 4 级。e.g.
com.taobao.jstorm
- ArtifactID 格式:产品线名-模块名。语义不重复不遗漏。e.g.
dubbo-client、fastjson-api、jstorm-tool
- Version 格式:主版本号.次版本号.修订号。
- GroupID 格式:com.{公司/BU}.业务线.[子业务线],最多 4 级。e.g.
- 线上应用不要依赖 SNAPSHOT 版本。
说明:不依赖 SNAPSHOT 版本是保证应用发布的幂等性。另外,也可以加快编译时的打包构建。
服务器
- 高并发服务器建议调小TCP协议的time_wait超时时间。
- 调大服务器所支持的最大文件句柄数(File Descriptor,简写为fd)。
- 给JVM环境参数设置-XX:+HeapDumpOnOutofMemoryError 参数,让JVM碰到00M场景时输出dump信息。
- 在线上生产环境,JVM的Xms和Xmx设置一样大小的内存容量,避免在GC后调整堆大小带来的压力。
- 服务器内部重定向使用forward;外部重定向地址使用URL拼装工具类来生成,否则会带来URL维护不一致的问题和潜在的安全风险。
第七章 设计规约
- 存储方案和底层数据结构的设计获得评审一致通过,并沉淀成为文档。
- 在需求分析阶段,如果与系统交互的User超过一类并且相关的User Case超过5个,使用用例图来表达更加清晰的结构化需求。
- 如果某个业务对象的状态超过3个,使用状态图来表达并且明确状态变化的各个触发条件。
- 如果系统中某个功能的调用链路上的涉及对象超过3个,使用时序图来表达并且明确各调用环节的输入与输出。
- 如果系统中模型类超过5个,并且存在复杂的依赖关系,使用类图来表达并且明确类之间的关系。
- 如果系统中超过2个对象之间存在协作关系,并且需要表示复杂的处理流程,使用活动图来表示。
- 需求分析与系统设计在考虑主干功能的同时,需要充分评估异常流程与业务边界。
- 类在设计与实现时要符合单一原则。
说明:单一原则最易理解却是最难实现的一条规则,随着系统演进,很多时候,忘记了类设计的初衷。 - 谨慎使用继承的方式来进行扩展,优先使用聚合/组合的方式来实现。
- 系统设计时,根据依赖倒置原则,尽量依赖抽象类与接口,有利于扩展与维护。
- 系统设计时,注意对扩展开放,对修改闭合。
- 系统设计阶段,共性业务或公共行为抽取出来公共模块、公共配置、公共类、公共方法等,避免出现重复代码或重复配置的情况。
- 避免如下误解:敏捷开发=讲故事+编码+发布。
其他
插件的使用
可以在idea中加入Alibaba的规范插件,从而在平常的代码检视中趁早消灭问题。