文章目录
编程规约
命名风格
- 类命名用大驼峰,缩写除外;
- 方法名、参数名、成员变量、局部变量命名用小驼峰;
- 常量和枚举成员名称全部大写,用下划线
_
隔开; - 抽象类用Abstract或Base开头;
- 异常类用Exception结尾;
- 测试类用Test结尾;
- POJO类中布尔变量不适用
is
前缀; - 包名小写,点分隔符中仅有一个(单数)单词,类名可以用复数;
- 命名时将表示类型的名词放在词尾:startTime/nameList/workQueue
- 命名时在词尾体现具体设计模式(如果使用的话)
- 接口类方法和属性不加任何修饰符(包括public);
- Service和DAO类,实现类使用接口名加Impl后缀命名;
常量定义
- 不允许未经定义的的常量直接出线;
- 对
long
类型赋值时使用大写L
而非小写l
; - 按常量功能进行归类,分开维护;
代码格式
- 左大括号前空格但不换行,后换行;右大括号在后续有
else
等代码时不换行,否则前后都换行; - 保留字和运算符需要空格其余内容;
- 注释双斜线和注释内容间有且只有一个空格;
- 单行字符小于
120
个,超出则换行,换行后须于第一行缩进4
个空格(或设置1
个TAB为4
个空格),.
和,
需要作为换行排头(如果存在的话); - 参数列表
,
分隔符后需加空格;
OOP规约
- 只允许通过类名而非类对象来访问此类的静态成员;
- 覆写方法必须用
@Override
注解; - 外部调用或二方库依赖的接口不允许修改方法签名;
- 接口过时必须用
@Deprecated
注解,并表明新接口或新服务; - 禁止使用过时类或方法;
Object
的equal
方法易抛空指针异常,应使用常量或确定有值的对象来调用该方法;- 所有整型包装类对象间值的比较,全部使用
equals
方法; - 浮点数间值的比较,基本数据类型不能用
==
,包装数据类型不能用equals
; - 浮点数比较可指定一个误差范围,当两浮点数差值在误差范围内时认为是相等的;
- 浮点数比较可用
BigDeimal
获取浮点数值,运算后使用CompareTo
方法忽略精度计算; - 使用
String
的构造方法或BigDecimal
的valueOf
方法转化浮点对象double
,避免直接使用BigDecimal
的构造方法造成精度损失导致业务逻辑异常; - 所有POJO类属性、RPC方法的返回值和参数必须使用包装数据类型;
- DO/DTO/VO等POJO类不设定初值,提示使用者必须自行显式赋值;
- 序列化类新增属性时,避免修改
serialVersionUID
字段,避免反序列失败,若完全不兼容升级,修改serialVersionUID
值,避免反序列化混乱; - 构造方法禁止任何业务逻辑,若有初始化逻辑需求,则放入
init
方法中; - POJO类必须写
toString
方法; - 禁止在POJO类中同时存在属性
xxx
的isXxx()
方法和getXxx()
方法,避免两个方法的调用优先级混乱; - 类内方法顺序:公有方法或保护方法>私有方法>
getter/setter
方法,同名方法应优先一同放置; - 循环体中字符串的连接,使用
StringBuilder
的append
方法进行扩展,避免程序自行生成一个对象造成内存资源浪费; - 使用
final
关键字的情况:
不允许被继承的类;
不允许修改引用的域对象;
不允许被覆写的方法;
不允许运行过程中重新赋值的局部变量;
避免上下文重复使用一个变量;
clone
方法默认浅拷贝,深拷贝需要覆写该方法实现域对象的深度遍历拷贝;- 类成员与方法控制从严:
若不允许外部直接
new
创建对象,那么构造方法规定为private
;
工具类不允许public
或default
构造方法;
类非static
成员变量并且与子类共享,必须是protected
;
类非static
成员变量并且仅在本类使用,必须是private
;
类static
成员变量如果仅在本类使用,必须是private
;
若是static
成员变量,考虑是否为final
;
类成员方法只供类内部调用,必须是private
;
类成员方法只对继承类公开,限制为protected
;
日期时间
- 日期传入
pattern
中统一使用小写的yyyy
表示当天年份,YYYY
表示当周年份,周跨年时返回下一年; - 日期中月份----
M
,分钟----m
,24小时制----H
,12小时制----h
; - 不允许使用
java.sql.Date/Time/Timestamp
方法; - 闰年的存在,需要禁止使用
365
天/年的固定写法,避免2月29日问题;
集合处理
- 只要覆写
equals
,就必须覆写hashCode
; Set
存储的是不重复的对象、自定义对象作为Map
的键时,必须覆写这些对象的equals
和hashCode
方法;- 只能使用
isEmpty()
方法判断集合内部是否为空,而非size() == 0
; java.util.stream.Collectors
类中,只能使用含有参数类型BinaryOperator
,参数名mergeFunction
的toMap()
方法,避免出现相同key
值抛出IllegalStateException
异常的情况,该方法还需要注意value
为null
时抛出的NPE
异常;ArrayList
的subList
结果不可强转为ArrayList
,否则会抛出ClassCastException
异常;- 使用
Map
方法keySet()/values()/entrySet()
返回集合对象时,不可对其进行添加元素操作,否则会抛出UnsupportedOperationException
异常; - 对
Collections
类返回的对象不能进行添加或删除元素的操作; subList
场景中对父集合元素的增减,都会导致子列表的便利、增加、删除产生ConcurrentModificationException
异常;- 只能使用集合的
toArray(T[] array)
方法,传入类型完全一致、长度为0的数组,来将集合转为数组; - 使用
Collection
接口实现任何类的addAll()
方法时,都要对输入的集合参数进行NPE
判断; - 不能使用
Arrays.asList()
中修改集合相关的方法,它的add/remove/clear
方法会抛出UnsupportedOperationException
异常; - 泛型通配符
<? extends T>
的泛型集合不能使用add
方法,<? super T>
不能使用get
方法; - 无泛型限制定义的集合赋值给泛型限制的集合,在使用这集合元素时,需要进行
instanceof
判断,避免抛出ClassCastException
异常; remove/add
操作不能在foreach
循环中操作,使用Iterator
进行元素remove
,并发操作时需要对Iterator
对象加锁;Comparator
类的实现需要满足:
x,y 的比较结果与 y,x的比较结果相反;
x > y , y > z, 则 x > z;
x = y , 则 x 与 z 的比较结果同 y 与 z 的比较结果相同;
- 集合初始化时,指定集合初始化值大小,若不能确定则指定默认值
16
;
并发处理
- 单例对象获取需要保证线程安全,其方法也要保证线程安全;
- 创建线程及线程池需要制定有意义的线程名词,易于回溯;
- 线程资源只能通过线程池提供,不允许自行显式创建线程;
- 线程池不允许使用
Executors
创建,通过ThreadPoolExecutor
的方式避免资源耗尽的风险; SimpleDateFormat
是线程不安全的类,一般不要定义为static
变量,若一定要定义,必须加锁或使用DateUtils
工具类;- 牢记使用
try-finally
块回收自定义的ThreadLocal
变量,避免影响后续业务逻辑和造成内存泄漏; - 高并发时,尽可能使加锁的代码块工作量尽可能的小(甚至不加锁),避免在锁代码块中调用RPC方法;
- 对多个目标同时加锁时,需要保持一致的加锁顺序,避免死锁;
- 使用阻塞等待获取锁的方式只能在
try
代码块外,且加锁方法与try
代码块间没有任何可能抛出异常的方法调用,避免加锁后在finally
中无法解脱; - 使用尝试机制获取锁的方式在进入业务代码块之前,必须先判断当前线程是否已经持有锁;
- 并发修改同一记录时,避免更新丢失,需要加锁,要么在应用层加锁,要么在缓存加锁,要么在数据库存层使用乐观锁(冲突概率小于20%时,否则使用悲观锁),使用
version
作为更新依据; - 多线程并行处理定时任务时,使用
ScheduledExcutorService
代替Timer
运行多个TimeTask
,可以避免其中一个没有捕获抛出的异常导致其它任务自动终止运行的情况;
控制语句
switch
括号内的变量类型为String
并且此变量为外部参数时,必须先进行null
判断;- 注意三目运算符表达式1和表达式2类型对齐时,可能抛出的因自动拆箱导致的
NPE
异常; - 高并发场景中,避免使用
==
作为中断或推出的条件; - 条件判断中避免使用复杂语句,将复杂语句的判断结果赋值给一个有意义的布尔变量,提高可读性;
- 避免使用取反逻辑
!
,采用对应的正向逻辑写法; - 公开接口需要采取入参保护,尤其是批量操作的接口;
注释规约
- 类、类属性、类方法的注释必须使用
Javadoc
规范; - 所有抽象方法(包括接口中的方法)必须使用
Javadoc
注释返回值、参数、异常说明和指出该方法做什么事情,实现什么功能; - 所有类都必须添加创建者和创建日期;
- 方法内部单行注释在被注释语句上方另起一行,使用
//
注释,多行注释使用/**/
注释,且需与代码对齐; - 所有枚举类型字段必须要有注释,说明每个数据项的用途;
前后端规约
- 前后端交互的
API
需要明确协议、域名、路径、请求方法、状态码、响应体; - 前后端数据列表相关的接口返回,若为空则返回空数组或空集合,减少前端琐碎的
null
判断; - 服务端发生错误时,返回给前端的响应信息包含
HTTP
状态码、errorCode
、errorMessage
、用户提示信息四个部分,分别针对浏览器、前端开发、错误排查人员、用户; JSON
格式数据中,所有key
必须为小驼峰风格;- 需要使用超大整数的场景,服务端一律使用
String
字符串类型返回,而非Long
类型; HTTP
请求通过URL
传递参数时,不能超过2048
字节。通过body
传递内容时,不能超过设定长度,否则后端解析出错;- 服务器内部重定向必须使用
forward
;外部重定向必须使用URL
统一代理模块生成,否则会导致浏览器“不安全”提示,及URL
维护不一致; - 前后端时间格式统一为
yyyy-MM-dd HH:mm:ss
,统一为GMT
;
其它
- 避免使用
Apache Beanutils
进行属性copy
; - 后台输送给页面的变量必须加感叹号成
$!{var}
形式,否则var
等于null
或不存在时,${var}
会直接显示在页面上; Math.random()
返回值为double
类型,取值范围在0到1之间(左闭右开),取整数类型直接使用Random
对象的nextInt
或nextLong
方法;- 任何数据结构的构造或初始化,都应该指定大小,避免数据结构无限增长吃光内存;
- 及时清理不再使用的代码段及配置信息;
异常日志
错误码
- 不体现版本号和错误等级;
- 正常情况返回
00000
; - 错误码为5位字符串类型,错误产生来源+四位数字编号;
- 不能直接输出给用户作为提示信息;
- 错误码之外的业务独特信息由
error_message
承载;
异常处理
- 类库中定义的可以通过预检查方式规避的
RuntimeException
异常不应该通过catch
方式处理; - 异常捕获后不能作为流程控制、条件控制;
- 分清稳定代码(无论如何不会出错的代码)和非稳定代码,对后者的
catch
尽可能区分异常类型后,再做处理; - 事务场景异常被
catch
后,若需要回滚则自行手动回滚; finally
块必须对资源对象、流对象关闭,异常也需要进行try-catch
,禁止使用return
;- 捕获与抛出异常必须完全匹配,或者捕获异常是抛出异常的父类;
- 调用
RPC
、二方包、动态生成类的相关方法时,捕获异常必须使用Throwable
类进行拦截; - 时刻注意
NPE
的防止;
日志规约
- 应用中不可直接使用日志系统的
API
,应使用日志框架中的API
; - 按公司要求进行扩展日志命名;
- 日志输出时,使用占位符进行字符串变量之间的拼接;
- 对
trace/debug/info
级别的日志输出,必须进行日志级别的开关判断; - 在日志配置文件中设置
additivity=false
,避免重复打印日志,浪费空间; - 生产环境禁止直接使用
System.out
或System.err
输出日志或e.printStackTrace()
打印异常堆栈; - 异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字
throws
往上抛出
logger.error("inputParams:{} and errorMessage:{}", 各类参数或对象toString(), e.getMessage(), e);
- 日志打印时禁止直接用
JSON
工具将对象转换成String
;
单元测试
- 单元测试需要遵循
AIR原则
:
automatic
independent
repeatable
- 单元测试应为全自动、非交互式执行;
- 保证单元测试的独立性,测试用例间禁止相互调用、依赖;
- 必须可以重复执行;
- 测试中的测试粒度需足够小,至多为类级别,常为方法级别,便于精确定位问题;
- 源码编译时会跳过如下目录
src/test/java
,单元测试框架默认扫描,因此单元测试代码需写在该目录中; - 单元测试的语句覆盖率至少70%,核心模块的语句覆盖率和分支覆盖率要求100%;
- 单元测试代码遵守
BCDE原则
:
Border:边界值测试
Correct:正确输入,得到预期结果
Design:结合设计文档
Error:强制错误信息输入,得到预期结果
- 业务代码应避免:
在构造方法中做过多的内容;
存在过多的全局变量和静态方法;
存在过多的外部依赖;
存在过多的条件语句;
安全规约
- 率属于用户个人的页面或功能必须进行权限控制校验;
- 用户敏感数据禁止直接展示;
- 对用户输入的
SQL
参数严格使用参数绑定或METADATA
字段值限定,放置SQL
注入,禁止字符串拼接SQL
访问数据库; - 用户输入的任何参数必须做有效性严重;
- 禁止向
HTML
页面输出未经安全过滤或未正确转义的用户数据; - 表单、
AJAX
提交必须执行CSRF
安全验证; URL
外部重定向传入的目标地址必须执行白名单过滤;- 使用平台资源必须实现正确的防重放机制,避免滥刷导致资损;
MySQL数据库
建表规约
- 是否概念的字段必须用
is_xxx
的方式命名,且使用unsigned tinyint
数据类型; - 表明字段名必须使用小写字母或数字,禁止数字开头、下划线间只出现数字;
- 表名固定为单数名词,尽量遵循“业务名称_表作用”,库名和应用名尽量保持一致;
- 主键索引名为
pk_字段名
,唯一索引名为uk_字段名
,普通索引名为idx_字段名
; - 小数类型仅使用
decimal
,禁止使用float
和double
; - 若存储几乎相同长度字符串时,优先使用
char
定长字符串类型; varchar
是可变长字符串,避免其长度超过5000
,超过则定义类型为text
,独立一张表并用主键对应;- 表必备字段:
id
create_time
update_time
- 不是频繁修改的字段、不是唯一索引的字段、不是
varchar
和text
字段,则允许为了提高查询性能适当冗余,但数据必须一致; - 单表超过
500
万行或单表容量超过2GB
,才考虑进行分库分表;
索引规约
- 业务上具有唯一特性的字段,即便是组合字段也必须建成唯一索引;
- 超过三张表则禁止
join
; - 在
varchar
字段建立索引时,必须指定索引长度,根据实际文本区分度决定索引长度,不必对全字段建立索引; - 页面搜索禁止左模糊或全模糊;
SQL
优化的目标要求至少ref
,最少达到range
,最好做到consts
;- 组合索引中区分度最高的内容在最左边;
SQL语句
- 禁止使用
count(列名)
或count(常量)
来替代count(*)
; - 某列全为
NULL
时,count(col)
的返回结果为0
,但sum(col)
的返回结果为NULL
,因此使用sum()
是注意NPE
问题; - 只使用
ISNULL()
来判断是否为NULL
值; - 分页查询逻辑编写时,
count
为0
直接返回,避免后续执行; - 禁止使用外键与级联,一切外键概念必须在应用层解决;
- 禁止使用存储过程;
- 数据订正(特别是删除或修改)时,要先
select
,避免误删除; - 设计多张表的查询和变更,都需要在列名前加表名或表的别名进行限定;
- 尽量避免
in
操作,实在要用则需将数量控制在1000
个以内; - 不建议在开发代码中使用
TRUNCATE TABLE
;
ORM映射
- 表查询中禁止使用
*
作为查询的字段列表,必须明确查询字段; - POJO类布尔属性不能加
is
,数据库字段必须加is
,要求二者在resultMap
中进行属性和字段之间的映射; - 禁止使用
resultClass
作为返回参数,必须定义<resultMap>
,因此每张表也必然有一个<resultMap>
对应; sql.xml
配置参数使用:#{}
,#param#
,禁止使用${}
,后者易于出现SQL
注入;- 不推荐使用
iBATIS
自带的queryForList(String statementName, int start, int size)
; - 禁止直接使用
HashMap
和Hashtable
作为查询结果集的输出; - 执行
SQL
时不要更新无改动的字段,易出错、效率低、增加binlog
存储; - 禁止滥用
@Transactional
事务,避免影响数据的QPS
;
工程结构
应用分层
- 默认上层依赖于下层,箭头关系表示直接向下依赖;
开放API层
终端显示层:模板渲染及显示;
Web层:转发、校验及不复用的业务简单处理层;
Service层:具体业务逻辑层;
Manager层:通用业务层;
DAO层:数据访问层;
第三方及外u接口:其他部门PRC服务接口、基础平台、其他公司接口;
二方库依赖
- 定义
GAV
遵循规则:
groupID:com.{公司/BU}.业务线 [.子业务线]
artifactID:com.taobao.jstorm
version
- 指定版本号命名方式为:
主版本号:产品方向改变,或大规模API不兼容,或架构不兼容升级
次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的API不兼容修改
修订号:保持完全兼容性,修复BUG,新增次要功能特性
- 线上应用禁止依赖于
SNAPSHOT
版本(安全包除外),正式发布的类库必须先向中央仓库进行查证,使RELEASE
版本号有延续性,且版本号不允许覆盖升级; - 二方库的新增或升级,保持除功能点之外的其它
jar
包仲裁结不变。如果有改变,必须明确评估和验证; - 允许二方库定义枚举,参数使用枚举类型,但接口返回值不允许使用枚举或包含枚举的POJO对象;
- 禁止在子项目的
pom
依赖中出现相同的GroupId
、ArtifactId
,但不是相同的Version
; - 所有
pom
文件中的依赖声明放在<dependencies>
语句块中,所有版本仲裁放在<dependencyManagement>
语句块中; - 二方库发布者应遵循精简可控原则和稳定可追溯原则;
服务器
- 高并发服务器建议调小
TCP
协议的time_wait
超时时间,调大服务器所支持的最大文件句柄数; - 线上生产环境
JVM
的Xms
和Xmx
设置一样大小的内容容量,避免在GC
后调整堆大小带来的压力;
设计规约
- 需求分析阶段:
系统交互的
User
超过一类,且相关User Case
超过五个,使用用例图来表达更清晰的结构化需求;
业务对象的状态超过三种,使用状态图表达并明确状态变化的各个触发条件;
系统中某功能的调用链路上对象超过三个,使用时序图表达并明确各调用环节的输入与输出;
系统中模型类超过五个并存在复杂的依赖关系,使用类图表达并明确类之间的关系;
系统中超过两个对象间存在协作关系,并需要表示复杂的处理流程,使用活动图来表示;
- 系统架构设计阶段:
确定系统边界;
确定系统内模块间的关系;
确定知道后续设计与演化的原则;
确定非功能性需求;
- 需求分析与系统设计时,需充分考虑异常流程与业务边界;
- 类的设计与实现过程中要符合单一原则;
- 谨慎使用继承,优先使用聚合/组合;
- 系统设计时尽量依赖抽象类与接口,注意对扩展开放,对修改闭合,同时抽取出公共模块、公共配置、公共类、公共方法等;
- 可扩展的本质是找到系统的变化点,并隔离它;
- 设计的本质是识别和表达系统难点。