目录
10.使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历【Java】
11.避免使用 List的contains方法进行遍历、对比、去重操作
18.当为多个模块提供能力时,一定提升交互信息持久化的优先级
21.在涉及到多参数方法的开发过程中,建议合并参数方法为对象
22.数据库持久层推荐使用MyBatisPlus代替MyBatis
23.业务自定义异常推荐继承RuntimeException
24.对接外部平台涉及操作时间由上游提供
一、研发中的小技巧
1.当业务需要手写sql查询时
Bad Case:
@Select("select * " +
"from worker_bean " +
"where worker_code = #{workerCode}")
Optional<WorkerBean> selectByWorkerCode(String workerCode);
Good Case:
@Select(" select *" +
" from worker_bean" +
" where worker_code = #{workerCode}")
Optional<WorkerBean> selectByWorkerCode(String workerCode);
分析:
Bad Case中,所有的空格都放在最后,容易因为忘记添加而出现sql语法错误,严重时会导致线上事故
Good Case中,所有的空格都统一放在最前面,一目了然,sql中即使多几个空格也不会出现问题
2.身份证或其他用户能够手动输入注意大小写
注意utf8_general_ci(case insensitive)字符集忽略大小写检查
身份证:X x
IDC: LFRZ lfrz
建议:
入参后在最上层先转换
3.多条件分支的else分支
Bad Case:
public void func(int status) { if (status == 0) { // do something } else if (status == 1) { // do something } else { // do something } // do other thing }
Goods Case:
public void func(int status) {
if (status == 0) {
// do something
} else if (status == 1) {
// do something
} else if(status == 2){
// do something
}else {
throw new RuntimeException("unhandled status");
}
// do other thing
}
分析:
对于多条件分支,使用else做剩下条件判断容易导致在业务发生变更,即status增多时忘记同步修改,导致不符合预期,出现bug。
并且若status是用户输入时,容易出现意料之外的输入情况
4.枚举类如果没有顺序要求,建议随机插入,避免代码冲突
public enum LarkUserEnum {
LIUBINGBING("liubingbing.logic@bytedance.com"),
WUSHUANG("wushuang.allen@bytedance.com"),
LITAO("litao.cook@bytedance.com"),
;
}
解释:
目前大家基本使用git作为版本管理工具,当同时修改同一个枚举时,分支合并容易导致冲突
如果枚举对顺序没有要求时,大家在不同位置添加,能很大程度减少冲突出现的可能性
5.BeanCopy还是get set
分析:
beancopy通过名称匹配的方式赋值,主要优势是快,但是随着系统的不断迭代,很容易出现字段名称不一致的情况,导致出现预料之外的情况
可以配合GenerateAllSetter插件使用
6.不使用全量update(record)方法
Bad Case:
record = select(xxx);
record.setStatus(newStatus);
update(record);
Good Case:
// 1
record = select(xxx);
newRecord = new Record();
newRecord.setId(record.getId());
newRecord.setStatus(newStatus);
updateSelective(record);
// 2
updateStatusById(newStatus, id);
分析:
暂时无法在飞书文档外展示此内容
原则:update应该遵循最小修改原则,实际上,业务需要修改哪些字段,实际就只修改哪些字段,使用全量update会出现副作用,导致覆盖了其他的数据
7.基于状态机的“无锁编程”
-
悲观锁:
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
-
乐观锁
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了
暂时无法在飞书文档外展示此内容
适用于非高并发业务,并且无环,如果有环的话可以引入版本字段
@Getter
public enum DemoStatusEnum {
A("a"),
B("b"),
C("c"),
D("d"),
E("d"),
;
final String code;
DemoStatusEnum(String code) {
this.code = code;
}
public static List<String> getPrepositiveStatus(DemoStatusEnum afterStatus) {
switch (afterStatus) {
case A:
throw new ServerException("no prepositive status");
case B:
return Collections.singletonList(A.getCode());
case C:
return Collections.singletonList(B.getCode());
case D:
return Collections.singletonList(B.getCode());
case E:
return Arrays.asList(C.getCode(), D.getCode());
default:
throw new ServerException("unhandled status:" + afterStatus);
}
}
}
private int execute(String sql) {
return 0;
}
@Transactional
public void businessMethodSingle(long id) {
// 参数校验
int update = execute(
" update table_x " +
" set xxx = xxx, " +
" status = " + DemoStatusEnum.D.getCode() +
" where id = " + id +
" and status = " + getPrepositiveStatus(DemoStatusEnum.D));
if (update == 0) {
return;
// or throw exception depend on business
}
// other logical code
}
@Transactional
public void businessMethodBatch(List<Long> ids) {
// 参数校验
int update = execute(
" update table_x " +
" set xxx = xxx " +
" status = " + DemoStatusEnum.D.getCode() +
" where id in " + ids +
" and status = " + getPrepositiveStatus(DemoStatusEnum.D));
if (update != ids.size()) {
throw new BusinessException(CommonStatus.BUSINESS_ERROR, "some alert messages");
}
// other logical code
}
8.事务 & 锁 & 异步任务
-
事务 & 锁
@Transactional public void txMethod(){ lock.lock(); try{ // businessCode }finally { lock.unlock(); } }
Correct case
-
public void lockMethod(){ lock.lock(); try{ // spring需要注意事务是否生效问题 txMethod(); }finally { lock.unlock(); } }
-
事务 & 异步任务
Error case
public void asyncTask(){ } @Transactional public void txMethod() { // businessCode asyncTask(); }
问题分析:
-
第二步中需要访问事务提交内容,因为事务未提交,而出现异常
-
业务需要快速返回,后续长任务异步执行
-
-
第三部中可能因为异常导致事务回滚(极端情况事务未提交,服务宕机),但是异步任务仍然会执行
-
邮件或短信通知等,不注重实时性但需要保证执行
-
-
Error case:
public void asyncTask() {
}
@Transactional
public void txMethod() {
// businessCode
}
public void mainMethod() {
txMethod();
asyncTask();
}
问题分析:
事务提交,异步任务可能执行失败
-
解决方案一(不优雅):
采用第一种结构,异步任务中固定延迟一定时间,通过查询数据库相关数据判断事务是否提交,否则自旋等待
-
解决方案二:
本质是一个分布式事务问题,采用标准分布式事务处理方案
-
本地消息表
-
mq事务消息
-
...
-
// 基本本地消息表实现 public void asyncTask() { } @Transactional public void txMethod() { // businessCode // insert local message table } public void mainMethod(){ txMethod(); asyncTask(); } public void compensateMethod() { // handle unhandled message from local message table }
-
9.跨模块调用时注意引用参数
Bad Case:
// ownerA
public void moduleA() {
List<String> list = new ArrayList<>();
moduleB(list);
// other business
}
// ownerB
public void moduleB(List<String> param){
param.add("hello");
}
-
调用方:注意是否在后续中仍然会使用入参,如果会,建议传入复制对象
-
被调用方:一般禁止对入参内容本身做修改
分析:
即使两个模块归属于同一个用户,仍然可能导致疏忽导致忘记会被修改,导致使用的参数不符合预期
10.使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历【Java】
分析
keySet其实是遍历了 2次,一次是转为 Iterator对象,另一次是从 hashMap中取出
key所对应的value。而entrySet只是遍历了一次就把 key和value都放到了entry中,效
率更高。如果是JDK8,使用Map.foreach方法。
11.避免使用 List的contains方法进行遍历、对比、去重操作
分析
时间复杂度比较大,大家基本都知道,但是还是容易出现这样的代码
12.事务和锁的范围尽可能小
分析
并发量大的时候,如果锁和事务的范围太大,将会导致并发量极大降低,可以考虑参数校验和一些参数准备放到事务和锁外,且在事务和锁中尽可能减少的io调用
13.参数校验时,把拦截率高、耗时少的判断优先放在前面
分析
当业务中需要有较大的参数校验时,拦截率高,耗时少的判断放在前面,可以保证方法尽早返回,节约资源
14.定时任务和周期任务使用try catch包裹
@Async
public void scheduleTask() {
try{
businessMethod();
}catch(RuntimeException e) {
// log
// lark message
}
}
分析
当使用定时任务或异步任务时,建议添加全局的try catch,并在catch中打日志或lark通知,方便第一时间了解情况,做出响应
如果能在业务方法中都识别出来,也可以不用try catch
15.什么时候需要参数校验
-
下列情形,需要进行参数校验
-
调用频次低的方法。
-
执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参
数错误导致中间执行回退,或者错误,那得不偿失。
-
需要极高稳定性和可用性的方法。
-
对外提供的开放接口,不管是 RPC/API/HTTP接口。
-
敏感权限入口。
-
-
下列情形,不需要进行参数校验
-
极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查要求。
-
底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底
层才会暴露问题。一般 DAO层与Service层都在同一个应用中,部署在同一台服务器中,所
以DAO的参数校验,可以省略。
-
被声明成 private只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参
数已经做过检查或者肯定不会有问题,此时可以不校验参数。
16.代码逻辑变更时注释也要同步修改
-
正确的注释 > 没有注释 > 错误的注释
17. 删除永远不会再使用的注释代码
经常能在仓库中看到大段被注释的代码,会给review或接手的同学带来很大的负担,如果可以,请删除了
18.当为多个模块提供能力时,一定提升交互信息持久化的优先级
-
方便聚合出来总结常规问题
-
方便各个模块快速定位问题
19.打日志规范
期望之中的异常,例如用户验证码输入错误,用户输入表单格式不正确,不要打印error,建议用warn就可以
日志中注意npe,不要影响主程序执行
20.开关的使用
核心链路功能改动做到可灰度,可回滚。大部分场景可以使用tcc开关。
开关用完(全量稳定一段时间)后续迭代随手清除,否则会给后来人熟悉代码流程增加阻碍。
开关使用场景举例: 线下环境实在无法验证的功能上线,比如下游没有boe环境,一定加开关!
21.在涉及到多参数方法的开发过程中,建议合并参数方法为对象
多个参数的方法在调用的时候可能会因为传参顺序的问题导致实际运行效果不符合预期
建议多个参数的方法使用合并参数为对象的形式
22.数据库持久层推荐使用MyBatisPlus代替MyBatis
Mybatis需要自己手写单表的CRUD的SQL语句,费时费力,而且当数据库增加字段后,还需要重新梳理修改这些SQL。MybatisPlus单表的CRUD是零代码,也支持注解SQL支持联表查询,无需定义resultMap。QueryWrapper提供可编程的查询能力,灵活性上比手写XML的SQL效率要高不少。
23.业务自定义异常推荐继承RuntimeException
java的运行错误是基于异常机制的,对于业务自定义异常推荐采用RuntimeException机制。优势主要如下:
-
RuntimeException异常编译不强制捕获,java一般都是基于框架开发,比如spring,一般框架顶层都有切面可以定制处理,可以统一拦截这些业务异常,集中处理。
-
调用者可以按需try/catch处理这些业务异常,简化代码中大量异常处理的逻辑。
24.对接外部平台涉及操作时间由上游提供
下游系统不应该把上游回调时间作为操作完成时间记录,因为可能会存在重试情况,导致时间不准