开发规范
方法命名规范
save、remove、update、get、list、count
-
获取单个对象的方法用
get
作前缀
-
例如:查询单个用户
getStudent
,按照 ID 查询单个用户
getStudentById
。
-
获取多个对象的方法用
list
作前缀
-
例如:按照 IDS 查询多个用户,
listStudentByIds
。
-
获取统计值的方法用
count
作前缀
-
插入的方法用
save
作前缀
-
删除的方法用
remove
作前缀
-
修改的方法用
update
作前缀
if/for/while/switch/do等保留字与括号之间都必须加空格
领域模型命名规约
类名使用驼峰法,DO/BO/DTO/VO/AO/PO除外
DO(Data Object) DTO(Data Transfer Object) BO(Business Object) PO(Persistant Object) AO(ApplicationObject) VO(Value Object)
-
DO数据对象(数据库中的表映射到程序中的对象)
-
DTO数据传输对象(数据传输时的对象)
-
xxxDTO
,其中
xxx
为业务领域相关的名称。
-
BO业务对象,BO对内,DTO在BO的基础上只要自己需要的数据,然后对外提供
-
PO持久对象(数据库中的记录),一条记录就是一个PO对象
-
AO应用对象,在Web层与Service层之间抽象的复用对象模型, 极为贴近展示层,复用度不高
-
Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,
禁止使用 Map 类来传输。
-
展示对象(在用户界面上显示信息的对象) 可以优化掉,展示业务不复杂的可以直接用DTO
-
注意事项
-
POJO
是
DO/DTO/BO/VO
的统称,禁止命名成
xxxPOJO
。
抽象类使用Abstract或Base
枚举成员名称需要全大写
常量定义
1、long或者Long初始赋值时,使用大写的L,不能是小写的l,小写容易跟数字1混淆,造成误解。
2、不要使用一个常量类维护所有常量,按常量功能进行归类,比如缓存相关常量放在类CacheConsts下,系统配置相关常量放在类ConfigConsts下
API 路径规约
-
Get 方法尽量把 ID 等变量放到路径上。
-
例如:获取指定用户的信息,使用
GET /user/{id}
。
-
多个不可分割的单词,使用中划线拼接。
-
例如:用户验证码接口,使用
GET /user-verification-code
。
-
参数使用驼峰拼写。
-
例如:获取指定用户购买的指定商品,使用
GET /user/{userId}/purchasedProduct/{productId}
。
-
指向集合的复数名称。
-
例如:获取所有用户列表接口,使用
GET /users
。
-
不用使用动词定义 URL
-
错误示例:
-
正确应该通过 HTTP 方法的语义来定义 URL 的行为。
-
获取用户:
GET /user/{id}
-
添加用户:
POST /user
-
修改用户:
PUT /user
-
对非资源 URL 使用动词
-
如果有一个接口,并不是 CRUD 操作,这种情况可以使用动词。
-
例如:向用户发送邮件接口,使用
POST /user/{id}/send-mail
.
-
在嵌套资源的 URL 中使用关系
-
获取指定订单中所有商品列表,使用
GET /order/{id}/products
.
-
获取指定订单中所有指定商品信息,使用
GET /order/{orderId}/product/{productId}
.
注释规范
注释说明意图;class interface说明意图;方法可以详细一点比如加上对参数的说明;注释要在上一行加;在同行加的更体现的是让人理解的说明
-
注释说明意图即可,无需补充冗余字段
-
Class、Interface、Enum、@interface等文件类型,类上注释仅需说明类的意图即可。不需要补充时间和创建人,因为往往开发代码的不止一个人,容易造成信息干扰。需要的话,查看提交记录即可。
/**
* 适配第三方框架的线程池
*/
public interface ThreadPoolAdapter {
}
-
方法上需要添加注释
-
方法上需添加注释,并说明清楚方法的意图(接口实现类无需注释);必要时描述 @param @return。如果方法为内部引用方法,并且方法名称见名知意,无需方法注释.
/**
* 适配第三方框架的线程池
*/
public interface ThreadPoolAdapter {
/**
* 修改框架线程池的核心参数
*
* @param threadPoolBaseInfo 修改线程池的基础参数
* @return 线程池核心参数修改结果
*/
boolean updateThreadPool(ThreadPoolBaseInfo threadPoolBaseInfo);
}
-
方法块内部注释规范
-
方法内部的注释,应该新起一行,而不是跟在代码后面.、
正例:
// 刷新动态线程池参数
refreshDynamicPool(parameter, executor);
反例:
refreshDynamicPool(parameter, executor); // 刷新动态线程池参数
-
方法命名说明方法本身意图
代码开发规约:
-
类、方法和变量的命名要做到顾名思义,避免使用缩写.
-
静态变量使用大写,多个单词使用下划线连接。示例:
MESSAGE_CENTER_SEND_TYPE
.
-
捕获的异常名称命名为
ex
;捕获异常且不做任何事情,异常名称命名为
ignored
.
// 强制规定:捕获的异常名称命名为 ex;捕获异常且不做任何事情,异常名称命名为 ignored
public class DataProcessor {
public void processData() {
try {
// 一些处理逻辑
} catch (Exception ex) {
// 处理异常情况
}
}
public void processWithIgnoredException() {
try {
// 一些处理逻辑
} catch (Exception ignored) {
// 不做任何事情,只是捕获并忽略异常
}
}
}
-
返回值变量使用
result
命名;循环中使用
each
命名循环变量;map 中使用
entry
代替
each
.
// result 命名示范:
private void parseDate(String data) {
Result result = JSONUtil.parseObject(data, Result.class);
return result;
}
// 或采用 result 为前缀:
private void parseDate(String data) {
Result resultDate = JSONUtil.parseObject(data, Result.class);
return resultDate;
}
// each 命名示范:
appNameLeaseMap.values().forEach(each -> appNameLeaseList.add(each));
// 或是 for 循环:
for (Lease<InstanceInfo> each : appNameLeaseMap.values()) {
appNameLeaseList.add(each);
}
-
业务系统中优先使用 Guava、HuTool、Common3 等工具类中的方法,不存在指定方法时再创建自定义工具类,禁止创建相同语义方法的工具类.
-
空实现接口或类不允许存在空格.
// 正例
public interface AdapterThreadPoolMonitor extends ThreadPoolMonitor {
}
// 反例
public interface AdapterThreadPoolMonitor extends ThreadPoolMonitor {
}
-
优先使用卫语句替换嵌套判断,提高代码简洁度.(卫语句提前返回)
下面这个不是卫语句
public void processOrder(Order order) {
if (order != null) {
if (order.isValid()) {
// 执行订单处理逻辑
} else {
// 处理无效订单
}
} else {
// 处理空订单
}
}
卫语句
public void processOrder(Order order) {
if (order == null) {
// 处理空订单
return;
}
if (!order.isValid()) {
// 处理无效订单
return;
}
// 执行订单处理逻辑
}
-
方法参数自定义实体对象别名尽量使用
requestParam
,非自定义对象可以使用对应语义命名.
@GetMapping("/add")
@ApiOperation(value = "新增购物车")
public Result<CartItemRespDTO> addCartItem(@RequestBody CartItemAddReqDTO requestParam) {
return Results.success(null);
}
OOP规约
1、Object的equals方法容易抛出空指针异常,应使用常量或确定有值的对象来调用equals
正例:“test”.equals(object)
2、关于基本数据类型与包装数据类型的使用标准如下:
所有的POJO类属性必须使用 包装数据类型
RPC方法的返回值和参数必须使用 包装数据类型
所有的局部变量使用 基本数据类型
3、构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在init方法中
4、使用索引访问用String的split方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有角标越界异常
5、类内方法定义的顺序依次是:公有方法或保护方法>私有方法>getter/setter方法
6、循环体内,字符串的连接方式,使用StringBuilder的append方法进行扩展
7、final可以声明类、成员变量、方法、以及本地变量,下列情况使用final关键字
1)不允许被继承的类,如:String类
2)不允许修改引用的域对象,如POJO类的域变量
3)不允许被重写的方法,如:POJO类的setter方法
4)不允许运行过程中重新赋值的局部变量
5)避免上下文重复使用一个变量,使用final描述可以强制重新定义一个变量,方便更好地进行重构
8、慎用Object的clone方法来拷贝对象,对象的clone方法默认是浅拷贝,若想实现深拷贝需要重写clone方法实现属性对象的拷贝
9、类成员与方法访问控制从严
1)如果不允许外部直接通过new来创建对象,那么构造方法必须是private
2)工具类不允许有public或者default构造方法
3)类非static成员变量并且与子类共享,必须是protected
4)类非static成员变量并且仅在本类使用,必须是private
5)类static成员变量如果仅在本类使用,必须是private
6)若是static成员变量,必须考虑是否为final
7)类成员方法只供类内部调用,必须是private
8)类成员方法只对继承类公开,那么限制为protected
集合处理
1、关于hashcode和equals的处理,遵循如下规则:
1)只要重写equals,就必须重写hashCode
2)因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须重写这两个方法
3)如果自定义对象作为Map的键,那么必须重写hashCode和equals,String重写了hashCode和equals方法,所以可以使用String对象作为key
2、不要在foreach循环里进行元素的remove/add操作,remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁
3、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
|
线程不安全
|
并发处理
1、线程资源必须通过线程池提供,不允许在应用中自行显式创建线程,线程不允许使用Executors去创建,而是通过ThraedPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
2、SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类;
如果是JDK8的应用,可以使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormatter代替SimpleDateFormat
3、高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁,避免在锁代码块中调用RPC方法。
4、 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库使用乐观锁,使用version作为更新依据,如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次。
5、多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其他任务便会自动终止运行,使用SchedExecutorService则没有这个问题。
6、使用CountDownLatch进行异步转同步操作,每个线程退出前必须调用countDown方法,线程执行代码注意catch异常,确保 countDown方法被执行到,避免主线程无法执行至await方法,直到超时才返回结果。子线程抛出异常堆栈,不能在主线程try-catch到。
控制语句
1、在一个switch块内,每个case要么通过break/return等来终止,要么注释说明程序将继续执行到哪一个case为止,在switch块内,都必须包含一个default语句并且放在最后,即使空代码
2、在高并发场景中,避免使用“等于”判断作为判断或退出的条件,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。
3、循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的try-catch操作。
MySQL数据库
(一)建表规约
1、表达是与否概念的字段,必须使用is_xxx的方式命名,数据类型是unsigneed tinyint(1表示是,0表示否)
2、表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。
3、小数类型为decimal,禁止使用float和double,如果存储的数据范围超过decimal的范围,建议将数据拆成整数和小数分开存储
4、varchar是可变长字符串,不预先分配存储空间,长度不要超过5000,如果存储长度大于此值,定义字段类型为text,独立出来一张表,用主键来对应,避免影响其他字段索引效率
5、表必备三字段:id主键id、gmt_create创建时间、gmt_modified修改时间
6、单表行数超过500万行,或者单表容量超过2GB,才推荐进行分库分表
(二)索引规约
1、业务上具有唯一特性的字段,必须建成唯一索引
2、超过三个表禁止join,需要join的字段,数据类型必须绝对一致,保证被关联的字段需要有索引
3、在varchar字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可,一般对字符串类型数据,长度为20的索引,区分度会高达90%以上,可以使用count(distinct left(列名,索引长度))/count(“”)的区分度来确定
4、页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。索引文件具有B-Tree的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引
5、如果有order by的场景,请注意利用索引的有序性。order by最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现file_sort的情况,影响查询性能。
正例:where a=? and b=? order by c;索引a_b_c
反例:索引中有范围查找,那么索引有序性无法利用,如where a>10 order by b;索引a_b无法排序
6、SQL性能优化的目标:至少要达到range级别,要求是ref级别,如果可以是consts最好;consts单表在中最多只有一个匹配行,ref指的是使用普通的索引,range对索引进行范围检索,type=index,索引物理文件全扫描,速度非常慢
(三)SQL语句
1、数据订正时,特别是删除、修改记录操作,要先select,避免出现误删除,确认无误才能执行更新语句
2、in操作能避免就避免,避免不了,控制在1000个之内
3、sql.xml配置参数使用#{},#param#,不要使用${},此种方式容易出现SQL注入。
代码格式化Spotless
Spotless
是一个用于代码格式化和风格检查的工具,可用于许多编程语言和项目类型。Spotless 的目标是确保项目中的代码始终符合定义的代码格式和风格规范。
代码检查Checkstyle
在日常生活中有两种常用的使用方式,分别是通过代码编辑器 IDEA 和 Maven 配合使用。
消息队列正常使用
命名规范
RocketMQ 相关命名强制使用英文小写。
1)【强制】Topic 命名:业务线_项目名_topic。业务线或项目包含多个单词,使用 - 分割,例如:
common_message-center_topic
。
2)【强制】Tag 命名:业务线_项目_业务_tag,例如:
common_message-center_send-message_tag
。
3)【强制】生产者组命名:业务线_项目*_*业务_pg,例如:
common_message-center_send-message_pg
。
4)【强制】消费者组命名:业务线_项目*_*业务_cg,例如:
common_message-center_send-message_cg
。
申请规范
创建 Topic 需要走线上申请,申请时补充下述信息。
申请人:马称
Topic:
common_message-center_topic
Produce:
|
生产应用
|
生产者组
|
Tag
|
备注
|
1
|
message-center
|
common_message-center_send-message_pg
|
common_message-center_send-message_tag
|
流量削峰
|
2
|
message-center
|
common_message-center_send-message_pg
|
insurance_trading-order_send-message-fail_tag
|
微信模板消息发送失败通知保险项目
|
Consume:
|
消费应用
|
消费者组
|
Tag
|
消费模型
|
备注
|
1
|
message-center
|
common_message-center_send-message_cg
|
common_message-center_send-message_tag
|
集群模式
|
发送微信模板、短信、小程序等消息
|
2
|
trading-order
|
insurance_trading-order_send-message-fail_cg
|
insurance_trading-order_send-message-fail_tag
|
集群模式
|
微信模板消息发送失败通知保险项目
|
使用规范
1. 消息发送
1)【强制】消息生产者创建时,必须指定生产者组。
2)【强制】一个系统对应一个 Topic,系统下的不同业务根据 Tag 区分,参考申请规范-消费应用 Tag。
3)【强制】发送消息时,需设置 KEYS。KEYS 建议定义为业务唯一标识,比如订单 ID。
4)【强制】发送消息不管发送成功或失败,需打印 KEYS、Payload、执行时间以及 SendResult
5)【强制】发送消息时,需设置超时时间,避免应用被拖垮;建议超时时间设置为 2000ms 内。
6)【建议】针对可靠性较高的消息,发送失败后可以存储到 DB,开启定时任务扫描,并重新投递。
2. 消息消费
1)【强制】消费端创建时,必须指定消费者组。
2)【强制】消费端需要保证数据幂等。
3)【强制】消费消息不管成功或失败,需打印 KEYS、MsgId、执行时间以及 Message。
4)【强制】不同的应用集群应使用不同的消费者组,如果不同的应用集群需要订阅同一消费者组,需保证 Topic Tag 订阅关系一致。
5)【强制】引入 easymall-rocketmq-spring-boot-starter 打印消息消费日志。
log.info("Execute result: {}, Keys: {}, Dispatch time: {} ms, Execute time: {} ms, Message: {}", ...);
6)【建议】消费时尽量不设置重试,大部分情况下,执行失败的消息重试后会再次失败,反而会影响消费进度。开发者应该针对特定场景在代码中设置重试逻辑。
7)【建议】消费者并发消费数量默认为 1,即串行化,应该基于不同系统场景来设置并发数,同时要考虑消费过程中其它组件的压力。
-
系统 CPU 任务少:
CPU 核数 / (1 - 阻塞系数 0.8)
。
-
系统 CPU 任务较多,建议
CPU 核数 + 1
即可。