编码规约
命名风格
类名(除VO、DTO、PO外),方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格 常量类以Constant结尾,枚举类以Enum结尾、异常类以Exception 结尾,测试类以测试的类的名称开始以 Test 结尾。常量、枚举属性命名全部大写,单词间用下划线隔开,力求语义表达完整清楚 避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名 在常量与变量命名时,表示类型的名词放在词尾,以提升辨识度,例:startTime / workQueue / nameList / TERMINATED_THREAD_COUNT。如果模块、接口、类、方法使用了设计模式,在命名时要体现出具体模式,例public class OrderFactory;public class LoginProxy;public class ResourceObserver 各层命名规约: A)Service / DAO 层方法命名规约: 1)获取单个对象的方法用 get 做前缀。 2)获取多个对象的方法用 list 做前缀,复数结尾,如:listObjects 3)获取统计值的方法用 count 做前缀。 4)插入的方法用 save / insert 做前缀。 5)删除的方法用 remove / delete 做前缀。 6)修改的方法用 update 做前缀。 B)领域模型命名规约: 1)数据对象:xxxDO,xxx 即为数据表名。 2)数据传输对象:xxxDTO,xxx 为业务领域相关的名称。 3)展示对象:xxxVO,xxx 一般为网页名称。
常量定义
不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护 跨应用共享常量放置在二方库中
代码格式
括号、保留字(if / for / while / switch / do)间的空格与换行
if ( flag == 1 ) {
System. out. println ( "world" ) ;
} else {
System. out. println ( "ok" ) ;
String commentString = new String ( "demo" ) ;
double first = 3 . 2D;
int second = ( int) first + 2 ;
}
method ( args1, args2, args3) ;
单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则: 1)第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考示例。 2)运算符与下文一起换行。 3)方法调用的点符号与下文一起换行。 4)方法调用中的多个参数需要换行时,在逗号后进行。 5)在括号前不要换行。 单个方法的总行数不超过 80 行。 说明:除注释之外的方法签名、左右大括号、方法内代码、空行、回车及任何不可见字符的总行数不超过 80 行 不同逻辑、不同语义、不同业务的代码之间插入一个空行以提升可读性,但是没有必要插入多个空行进行隔开。
OOP规约
所有整型包装类对象之间值的比较全部使用 equals BigDecimal 1)值比较应使用 compareTo() 方法 2)Double转BigDecimal
BigDecimal recommend1 = new BigDecimal ( "0.1" ) ;
BigDecimal recommend2 = BigDecimal. valueOf ( 0.1 ) ;
基本数据类型与包装数据类型的使用标准: 1)所有的 POJO 类属性、RPC方法的返回值和参数必须使用包装数据类型。 2)所有的局部变量使用基本数据类型。 定义POJO类时,不要设定任何属性默认值 禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法。框架在调用属性 xxx 的提取方法时,并不能确定哪个方法一定是被优先调用到,神坑之一 使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛 IndexOutOfBoundsException 的风险
String str = "a,b,c,," ;
String[ ] ary = str. split ( "," ) ;
System. out. println ( ary. length) ;
类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 > getter / setter 方法。当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起
日期时间
获取当前毫秒数:System.currentTimeMillis();而不是 new Date().getTime() 使用枚举值来指代月份
集合处理
使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为0 的空数组。直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现ClassCastException 错误
List< String> list = new ArrayList < > ( 2 ) ;
list. add ( "guan" ) ;
list. add ( "bao" ) ;
String[ ] array = list. toArray ( new String [ 0 ] ) ;
使用工具类 Arrays.asList() 把数组转换成集合时,后台的数据仍是数组,不能使用其修改集合相关的方法,它的 add/ remove / clear 方法会抛出 UnsupportedOperationException 异常
String[ ] str = new String [ ] { "yang" , "guan" , "bao" } ;
List list = Arrays. asList ( str) ;
第一种情况:list. add ( "yangguanbao" ) ; 运行时异常。
第二种情况:str[ 0 ] = "change" ; list 中的元素也会随之修改,反之亦然。
不要对正在被遍历的集合进行remove / add 操作 遍历Map使用Map.forEach 方法,否则使用entrySet而不是 keySet 使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。 说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高
并发处理
SimpleDateFormat 是线程不安全的类,JDK8中可以使用DateTimeFormatter代替 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁 在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁 说明一:在 lock 方法与 try 代码块之间的方法调用抛出异常,无法解锁,造成其它线程无法成功获取锁。 说明二:如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中,unlock 对未加锁的对 象解锁,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),抛出 IllegalMonitorStateException 异常。 说明三:在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与说明二相同。
正例:
Lock lock = new XxxLock ( ) ;
lock. lock ( ) ;
try {
doSomething ( ) ;
doOthers ( ) ;
} finally {
lock. unlock ( ) ;
}
反例:
Lock lock = new XxxLock ( ) ;
try {
doSomething ( ) ;
lock. lock ( ) ;
doOthers ( ) ;
} finally {
lock. unlock ( ) ;
}
在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。 锁的释放规则与锁的阻塞等待方式相同 说明:Lock 对象的 unlock 方法在执行时,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),如果当前线程不 持有锁,则抛出 IllegalMonitorStateException 异常。
正例:
Lock lock = new XxxLock ( ) ;
boolean isLocked = lock. tryLock ( ) ;
if ( isLocked) {
try {
doSomething ( ) ;
doOthers ( ) ;
} finally {
lock. unlock ( ) ;
}
}
并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。 说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。 资金相关的金融敏感信息,使用悲观锁策略。 说明:乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策略有较复杂 的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。 正例:悲观锁遵循一锁二判三更新四释放的原则。
控制语句
在一个 switch 块内,每个 case 要么通过 continue / break / return 等来终止,要么注释说明 程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最 后,即使它什么代码也没有。 当 switch 括号内的变量类型为 String 并且此变量为外部参数时,必须先进行 null 判断。
public class SwitchString {
public static void main ( String[ ] args ) { method ( null ) ;
}
public static void method ( String param ) {
switch ( param) {
case "sth" :
System. out. println ( "it's sth" ) ;
break ;
case "null" :
System. out. println ( "it's null" ) ;
break ;
default :
System. out. println ( "default" ) ;
}
}
}
在 if / else / for / while / do 语句中即使只有一行代码,也要采用大括号。 三目运算符 condition ? 表达式 1:表达式 2 中,高度注意表达式 1 和 2 在类型对齐时,可能抛出因自动拆箱导致的 NPE 异常。 说明:以下两种场景会触发类型对齐的拆箱操作: 1)表达式 1 或 表达式 2 的值只要有一个是原始类型。 2)表达式 1 或 表达式 2 的值的类型不一致,会强制拆箱升级成表示范围更大的那个类型。
Integer a = 1 ;
Integer b = 2 ;
Integer c = null ;
Boolean flag = false ;
Integer result = ( flag ? a * b : c) ;
.在高并发场景中,容易产生等值判断被“击穿”的情况避免使用“等于”判断作为中断或退出的条件。 如果非使用 if()…else if()…else…方式表达逻辑,避免后续代码维护困难,请勿超过 3 层。超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现 除常用方法(如 getXxx / isXxx)等外不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。 循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)。 避免采用取反逻辑运算符。 公开接口需要进行入参保护,尤其是批量操作的接口。 反例:某业务系统,提供一个用户批量查询的接口,API 文档上有说最多查多少个,但接口实现上没做任何保护,导致 调用方传了一个 1000 的用户 id 数组过来后,查询信息后,内存爆了。 下列情形,需要进行参数校验: 1)调用频次低的方法。 2)执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回 退,或者错误,那得不偿失。 3)需要极高稳定性和可用性的方法。 4)对外提供的开放接口,不管是 RPC / API / HTTP 接口。 5)敏感权限入口。 下列情形,不需要进行参数校验: 1)极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查。 2)底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般 DAO 层与 Service 层都在同一个应用中,部署在同一台服务器中,所以 DAO 的参数校验,可以省略。 3)被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不 会有问题,此时可以不校验参数。
注释规约
类、类属性、类方法的注释必须使用/** 内容 */ 格式,不得使用 // xxx方式。 日期的设置统一为 yyyy/MM/dd 的格式。 .方法内部单行注释,在被注释语句上方另起一行,使用 // 注释。方法内部多行注释使用 /* */注释,注意与代码对齐。 特殊注释标记 1)待办事宜(TODO):(标记人,标记时间,[预计处理时间]) 2)错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间]) 对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(///)来说明注释掉代码的理由:
public static void hello ( ) {
System. out. println ( "it's finished" ) ;
}
前后端规约
前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合{}。这样可以减少前端很多琐碎的 null 判断。 服务端发生错误时,返回给前端的响应信息必须包含 HTTP 状态码,errorCode、errorMessage、用户提示信息(user_tip)四个部分。其中输出给用户的提示信息要求:简短清晰、提示友好,引导用户进行下一步操作或解释错误原因,提示信息可以包括错误原因、上下文环境、推荐操作等。 对于需要使用超大整数的场景(订单号),服务端一律使用 String 字符串类型返回,禁止使用 Long 类型。Long 类型能表示的最大值是 263-1,在取值范围之内,超过 253(9007199254740992)的数值转化为 Javascript 的 Number 时,有些数值会产生精度损失。 HTTP 请求通过 URL 传递参数时,不能超过 2048 字节。2048 字节是取所有浏览器的最小值。 HTTP 请求通过 body 传递内容时,必须控制长度,超出最大长度后,后端解析会出错。nginx 默认限制是 1MB,tomcat 默认限制为 2MB,当确实有业务需要传较大内容时,可以调大服务器端的限制。 在翻页场景中,用户输入参数的小于 1,则前端返回第一页参数给后端;后端发现用户输入的参数大于总页数,直接返回最后一页。 前后端的时间格式统一为"yyyy-MM-dd HH:mm:ss",统一为 GMT。
异常日志
错误码
全部正常,但不得不填充错误码时返回五个零:00000。 错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。 说明:错误产生来源分为 A/B/C,A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付超时等问题; B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的步长间距预留 100,参考附表 3。 编号不与公司业务架构,更不与组织架构挂钩,以先到先得的原则在统一平台上进行,审批生效,编号即被永久固定。 在获取第三方服务错误码时,向上抛出允许本系统转义,由 C 转为 B,并且在错误信息上带上原有的第三方错误码。
异常处理
异常捕获后不要用来做流程控制,条件控制。异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。 不要在 finally 块中使用 return。
反例:
public int checkReturn ( ) {
try {
return ++ x;
} finally {
return ++ x;
}
}
方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。 定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(),更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException 等。
日志规约
日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。对于当天日志,以 “应用名.log”来保存,保存在/{统一目录}/{应用名}/logs/目录下,过往日志格式为:{logname}.log.{保存日期},日期格式:yyyy-MM-dd 正例:以 mppserver 应用为例,日志保存/home/admin/mppserver/logs/mppserver.log,历史日志名称 为 mppserver.log.2021-11-28 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式: appName_logType_logName.log。logType:日志类型,如 stats / monitor / access 等; logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。 说明:推荐对日志进行分类,将错误日志和业务日志分开放,便于开发人员查看,也便于通过日志对系统进行及时监控。 正例:mppserver 应用中单独监控时区转换异常,如:mppserver_monitor_timeZoneConvert.log 在日志输出时,字符串变量之间的拼接使用占位符的方式。 说明:因为 String 字符串的拼接会使用 StringBuilder 的 append() 方式,有一定的性能损耗。使用占位符仅是替换动 作,可以有效提升性能。 正例:logger.debug(“Processing trade with id : {} and symbol : {}”, id, symbol); 避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity=false 正例: 可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。 说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。
安全规约
在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损。 对于文件上传功能,需要对于文件大小、类型进行严格检查和控制。 说明:攻击者可以利用上传漏洞,上传恶意文件到服务器,并且远程执行,达到控制网站服务器的目的。 发贴、评论、发送等即时消息,需要用户输入内容的场景。必须实现防刷、内容违禁词过滤等风控策略。
mysql数据库
建表规约
表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 tinyint( 1 表示是,0 表示否)。 表名不使用复数名词。 主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。 说明:pk_即 primary key;uk_即 unique key;idx_即 index 的简称。 小数类型为 decimal,禁止使用 float 和 double。 储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。 如果存储的字符串长度几乎相等,使用 char 定长字符串类型。 varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此 值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引率。 在数据库中不能使用物理删除操作,要使用逻辑删除。 说明:逻辑删除在数据删除后可以追溯到行为操作。不过会使得一些情况下的唯一主键变得不唯一,需要根据情况来酌 情解决。 表的命名最好是遵循“业务名称_表的作用”。 库名与应用名称尽量一致。 字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循 1)不是频繁修改的字段。 2)不是唯一索引的字段。 3)不是 varchar 超长字段,更不能是 text 字段。 正例:各业务线经常冗余存储商品名称,避免查询时需要调用 IC 服务获取。 单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表 说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。 不得使用外键与级联,一切外键概念必须在应用层解决。
索引规约
业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。 说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。 超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段需要有索引。 说明:即使双表 join 也要注意表索引、SQL 性能。 在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。 说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使 用 count(distinct left(列名,索引长度)) / count(*) 的区分度来确定。 页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。 如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 filesort 的情况,影响查询性能。 正例:where a = ? and b = ? order by c;索引:a_b_c 反例:索引如果存在范围查询,那么索引有序性无法利用,如:WHERE a > 10 ORDER BY b;索引 a_b 无法排序。 建组合索引的时候,区分度最高的在最左边。 正例:如果 where a = ? and b = ?,a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。 说明:存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where c > ? and d = ? 那么即使 c 的区分度更高,也必须把 d 放在索引的最前列,即建立组合索引 idx_d_c。
sql语句
不要使用 count(列名) 或 count(常量) 来替代 count(),count( ) 是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。 说明:count(*) 会统计值为 NULL 的行,而 count(列名) 不会统计此列为 NULL 值的行。 使用 ISNULL() 来判断是否为 NULL 值。 说明:而 ISNULL(column) 是一个整体,简洁易懂。从性能数据上分析,ISNULL(column) 执行效率更快一些。 SQL 语句中表的别名前加 as,并且以 t1、t2、t3、…的顺序依次命名。 说明: 1)别名可以是表的简称,或者是依照表在 SQL 语句中出现的顺序,以 t1、t2、t3 的方式命名。 2)别名前加 as 使别名更容易识别。 in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在1000 个之内。
ORM 映射
在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。 说明: 1)增加查询分析器解析成本。 2)增减字段容易与 resultMap 配置不一致。 3)无用字段增加网络消耗,尤其是 text 类型的字段。 不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义;反过来,每一个表也必然有一个与之对应。 说明:配置映射关系,使字段与 DO 类解耦,方便维护。 不要写一个大而全的数据更新接口。传入为 POJO 类,不管是不是自己的目标更新字段,都进行update table set c1 = value1 , c2 = value2 , c3 = value3;这是不对的。执行 SQL 时,不要更新无改动的字段,一是易出错;二是效率低;三是增加 binlog 存储。
工程结构
二方库依赖
定义 GAV 遵从以下规则: 1)GroupId 格式:com.{公司/BU}.业务线.[子业务线],最多 4 级。 说明:{公司/BU}例如:alibaba / taobao / tmall / kaikeba 等 BU 一级;子业务线可选。 正例:com.taobao.jstorm 或 com.alibaba.dubbo.register 2)ArtifactId 格式:产品线名-模块名。语义不重复不遗漏,先到中央仓库去查证一下。 正例:dubbo-client / fastjson-api / jstorm-tool 3)Version:详细规定参考下方。 】为避免应用二方库的依赖冲突问题,二方库发布者应当遵循以下原则: 1) 精简可控原则。移除一切不必要的 API 和依赖,只包含 Service API、必要的领域模型对象、Utils 类、常量、枚举 等。如果依赖其它二方库,尽量是 provided 引入,让二方库使用者去依赖具体版本号;无 log 具体实现,只依赖日志 框架。 2) 稳定可追溯原则。每个版本的变化应该被记录,二方库由谁维护,源码在哪里,都需要能方便查到。除非用户主动 升级版本,否则公共二方库的行为不应该发生变化。
服务器
调用远程操作必须有超时设置。 高并发服务器建议调小 TCP 协议的 time_wait 超时时间。 说明:操作系统默认 240 秒后,才会关闭处于 time_wait 状态的连接,在高并发访问下,服务器端会因为处于 time_wait 的连接数太多,可能无法建立新的连接,所以需要在服务器上调小此等待值。 正例:在 linux 服务器上请通过变更/etc/sysctl.conf 文件去修改该缺省值(秒):net.ipv4.tcp_fin_timeout=30