本文正在参加「金石计划」
前言
在日常开发的过程中,通常我们需要先看需求文档,根据需求文档的需求进行开发,其实在我们了解业务的时候,头脑就已经在构建代码如何写了,工作几年后可能一些常见的400、404、500、503等一些报错应该会很少出现,可能会出现一些看似正常,但实际业务接口返回有问题的情况,随着我们业务逻辑的复杂性,这些潜存的BUG可能会隐藏的很深,在某种程度上可能会迷惑开发人员、测试人员,从而让BUG溜到生产。
关于bug排查
- 首先先借住代码规范工具,可以根据idea的Alibaba Java Coding Guidelines阿里的代码规范,看看代码有没有爆黄之处,可以先从这个点看看逻辑是否有问题,之前公司还会用到checkStyle,自己设定了一套规范只需要传入checkStyle.xml进行配置即可,也可以检查是不是因为代码规范带来的bug
- 针对于某一个接口,逐行的去看代码,这里如果你是工作几年后对业务代码有所理解,可以去看到代码,你在看代码的过程中脑子或许就已经在构建没行代码的返回值,每个if判断会出现的情况,对这个结果进行排查。
- 如果你刚工作没几年, 那么第二条可以不用去考虑,直接拿到入参,根据postman去请求,然后debug代码一行一行的看,具体看每个对象在执行的过程中里面属性的值的变化,这种方式也是最稳妥的排查方式,关于代码我只相信执行结果,即便是现在我也不会嫌麻烦,还是去拿到接口入参去debug调试,这种方式我认为是最准确也是比较稳妥的bug排查方式。
- 接口中打日志,接口请求前期进行logger打日志,对入参出参进行日志记录,在业务相对复杂执行结果处进行日志记录,包括一些状态(status)、标识(flag)、list对象和map集合进行stream函数操作、包括过滤、计算、格式转换后的结果,进行日志记录。
关于出现的bug
入参指针问题
对入参数进行stream函数操作,最后还使用原本改变过的参数进行业务处理,导致数据会出现重复或者缺失。
/**
* 创建用户
* @param userDTOS
* @return
*/
@Override
public boolean createUser(List<UserDTO> userDTOS) {
List<SysUser> sysUsers = Lists.newArrayList();
check(userDTOS,sysUsers);
return true;
}
/**
* 校验逻辑
* @param userDTOList
* @param sysUsers
*/
private void check(List<UserDTO> userDTOList, List<SysUser> sysUsers) {
// List<UserDTO> userDTOS = Lists.newArrayList();
// List<UserDTO> updateUserDTOS = Lists.newArrayList();
if (CollectionUtils.isNotEmpty(userDTOList)) {
//过滤出需要新增的数据
userDTOList = userDTOList.stream().filter(r -> r.getActionType() != null).collect(Collectors.toList());
List<SysUser> sysUserList = BeanCopyUtil.copyListProperties(userDTOList, SysUser::new);
//过滤出需要更新的数据
userDTOList = userDTOList.stream().filter(r -> r.getActionType() == null && !StringUtils.isEmpty(r.getTag())).collect(Collectors.toList());
List<SysUser> updateSysUserList = BeanCopyUtil.copyListProperties(userDTOList, SysUser::new);
//数据更新 逻辑省略
//数据新增 逻辑省略
//组装数据
sysUsers.addAll(sysUserList);
sysUsers.addAll(updateSysUserList);
log.info("用户信息数据组装返回结果{}", JSON.toJSONString(sysUsers));
}
}
运行结果
这里可以看到,我们的入参是一个List对象,包括三条数据。
这里可以看到,我们通过第一次过滤,过滤出来了两条数据。
再次过滤的时候,发现更新数据没有过滤出来,是因为我们拿已经过滤出来的数据进行了二次过滤,导致不会存在对应的数据,是因为我们拿过滤数据的参数和接受过滤结果的参数,用的是同一个List对象,这样指向就会出现问题,导致我们漏掉了一条数据。
这种情况在业务代码很多很复杂的情况下,真的很容易出现,让人眼花导致写错,出现bug,需要重新声明List对象进行过滤结果的接收,这样才不会漏数据,这也是写代码习惯的问题,⚠️我在检查代码的时候确实看到过有人这么写过导致出现数据缺失,出现bug。
Mybatis的隐式转换
前端通过字符串类型传值,传了字符串为“0”的参数。
这里可以看到,Mybatis已经将字符串类型转为Integer类型了,
这时候在mapper.xml里对这个入参,如果进行了!=’‘ 判断,由于入参已经被转为Integer类型,在查询的时候这个参数就不会被拼接到sql后面,导致查询数据出现问题。
事务接口未提交前的影响
一个事务的接口,还没执行完之前,通过查询,然后更新某个状态,然后在进行查询。
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean transactionCommit(String userName) {
//查询用户
SysUser sysUser = userMapper.selectUserByUserName(userName,null);
if (null != sysUser) {
//用户信息状态更新 status更新为1
userMapper.updateStatus(userName);
}
//再次查询
SysUser sysUser1 = userMapper.selectUserByUserName(userName,"1");
log.info("状态为1的用户信息"+JSON.toJSONString(sysUser1));
return true;
}
这段代码我通过debug模式断点调试,这里可以看到我已经将user表里的status的状态更新为“1”,并且在事务内进行查询,也是可以查询到状态为“1”的数据,但是我通过在数据库执行sql,查询到的还是status为”0“的数据
这时候我走完debug提交完事务,这里status的状态变为“1”,由此可见mysql的隔离级别是可重复读的,但是这个时候出现时间差的时候会出现幻读,所以在某种程度上我们的接口还没执行完,测试在测试阶段进行数据库查询数据,可能并不是他所想查询到的数据。
总结
这些是最近项目中出现的一些bug贴出来与大家分享,或许也是大家写代码会忽略的一些问题,有时候需求很紧一个接着一个来,我们在写代码的时候,可能会为了赶进度有些代码写出来,是没有经过思考的,导致bug的出现,我们需要在追求新技术的同时,coding好自己手头上工作的代码,即便是一些枯燥无味的CRUD,也是一种锻炼。