那些“意料之外,情理之中”的bug们

CodeingBoy 8月 05, 2018

题图来自:https://pixabay.com/en/gummib%C3%A4rchen-gummi-bear-bear-359950/

终于有点时间能够来更新一下博客了。最近都在忙于项目的开发以及补足短板,好久没有发一些新的内容出来了。

之所以要写这么一篇文章,是因为最近遇到了几个这样的 bug。通勤的路上想了一下,其实我已经有好几次遇到这种 bug 了——它们会造成非常让人疑惑的现象,让你非常难找到根源,但是当你找到根源后,却以头抢地,大呼“为什么我没想到”。借用以前小米的一句广告宣传语,“意料之外,情理之中”。

于是,我就打算写这么一篇文章来“展示”这些 bug。希望自己能够吸取从中的教训,以后碰到这种 bug 的情形越来越少。

我的密码被谁改了?

这是我印象挺深刻的一个 bug。半年前我正在依靠着《Spring 实战》津津有味地体验着“POJO 开发的魅力”,Spring 带来的简化确实让我称道。当时,需要递交一份带有登录、注册功能的代码作业,要求使用 Spring。于是我兴致勃勃地写着代码。

Spring MVC 设置完毕,页面设计完毕,数据库设计和对接完毕,逻辑代码编写完毕。最后,需要一个登录和注册功能,理论上并不难实现,但既然我们使用了 Spring,为什么不顺带把 Spring Security 也用起来?

在 pom 中加入依赖,按照书上配置安全设置,编写了自己的密码加密器实现。等下,登录认证怎么来?虽然 Spring Security 有自己的表定义,但既然官方也提供了自定义的手段,我就使用了自己的 User 表,然后修改了查询 SQL:

Done!来测试一下,注册成功,注册表单校验成功,登录……怎么登录不上?

还以为是密码输错了,重新试了几次,却都不行。我输入密码用的是复制粘贴所以和大小写没关系;去数据库查了一下,用户记录已经被成功地插入了,但由于密码使用 MD5+Salt 所以无法判断密码是否是正确的。

就这样,从凌晨开始排查问题,先是确认了一下问题,确实发生在登录过程中。没有发现什么实用的日志信息。排查过程中,我也观察到一个怪事——密码的 MD5 不正确。难道是注册流程出了问题?又花了时间检查注册流程,尤其是密码 Hash 的过程,确认了是正确的,能够和数据库中的记录对的上。

所以,问题还是产生在登录过程中,密码是正确的,但是却提示密码不正确。经过断点查看,可以看到输入的密码与存储的密码确实是不正确的。于是我只好想是哪里的问题,由于原始输入的密码会经过我的密码加密器,因此我在那里断点进行调试,并没有发现任何问题。数次使用调试器修改变量值也没有发现问题所在,也核对了几次数据库,都没有问题。

没有办法了,只有断点跟入大法了——对 Spring Security 的登录方法进行跟踪调试。其实跟踪调试并不是一个太好的办法,在不熟悉代码实现目的的情况下,还是有点难度的。

在尝试了好几次,跟踪了好几个方法后,我终于发现了——我的密码 MD5,也就是我从数据库中查询出来的 MD5,竟然被修改了?等到定位到了修改发生的位置,它的周围写着一句代码(记得不是很清楚了,大概意思是这样):

哭死,原来是没有找到 authorities 表。更重要的是,我的日志等级并不是 debug,因此这个日志并没有打印出来。补上了这个表,并且插入了数据后,一切就正常了。而此时已经是早上快 7 点了。

教训

这是我第一次遇到了这种 bug,所花的时间之长让我对它印象深刻。既然写这篇文章是为了避免发生类似事情,更快地排查 bug。因此我也总结了一下。

bug 起因:

  • 没有仔细研究框架,就以为某个实现是可选的,因此直接舍弃,导致框架工作不正常
  • Logger 等级太低
  • 行为太隐蔽——我个人认为怎么会有框架因为找不到数据库,一言不合就改你的 hash?

教训:

  • 发生了问题,看 log。如果 log 没有有价值的信息,调低 log 等级
  • 采用缩小范围的方法排查,确定 bug 可能的位置
  • 必要时可以使用跟入调试的方法,这个方法我屡试不爽,当然也不是灵丹妙药

数据对不上啊

这是最近的一个 bug,也是促使我写这篇文章的导火索。

正在做的一个项目,产品要求添加一个数据导出成 Excel 的功能。对于我来说,这个需求非常简单,但是为了方便以后的复用,我需要先做一点工具类。

我设计了一个 OneDimenstionExcelExport 类,用来生成能够符合对应表头标签的表格。它的角色如下:

  • 接收一个 List 和一个 Map,其中 List 会被用作表头,后面的 Map 就是表头下面的行了
  • 接收的 Map 中的 key 是第一列的单元格值,而 value 又是一个 Map,该 Map 的 key 是第一行的表头,value 是对应这个表头单元格的值

假设我传入的是 List 是 {“Name”, “Blog, “Remark”},传入的 Map 是 [{“key”: “CodeingBoy”, “value”: [{“key: “Remark”, “value”: “Nothing here :(“}, {“key”: “Blog”, “value”: “blog.codeingboy.me”}]],那么应该导出这样一张表格:

NameBlogRemark
CodeingBoyblog.codeingboy.meNothing here 🙁

你应该能看出,创造这么一个类,是希望能够构成对应于行和列的对应关系。因为部分列对应的结果可能是空的,因此不能简单地在一行里面输出出来。

好,铺垫结束。代码实现好了,具体逻辑代码也实现好了,导出!结果 OK,正准备和产品说已经 OK 的时候,突然想着还是计算确认一下结果比较好。这就发现了数据对应不上——一个列中的每行的总和,和最后的总计列的数据是对不上的(总计列是在数据库里面计算好了返回的,不是在表格生成过程中计算的)。

最初以为是不是 SQL 写错了,检查了 SQL,没有问题,并且在数据库中直接做总和的计算也是正确的。这样一来,就只可能是自己的问题了。此时,发现了一个明显不符合常理的数据,它的数据和总计是一样的,检查了一下,判断可能是 id 相同,数据库缓存的问题。修复之,这一行的数据正常了。

但是还是对不上啊,既然之前的是数据库缓存问题,这个表也是只用于聚合数据的表格,认为可能是这方面的问题。于是就开始把这个表的数据库缓存关掉,但是尝试了好一阵子,关都关不了,遂放弃。

折腾来折腾去,没有太好的头绪。后面去重新检查了一下表格数据,希望能够从中发现某种模式,结果还真发现了——每个总计数字和计算的总和都相差 13,于是到数据库查找到这个 13 的数据,和表格的一对比,并不正确——表格中的是 20。遂断点调试,发现与这个表格生成类有关——由于这两行的表头都一样,而数据结构又是 Map,因此其中一个就被忽略掉了。

修改了数据结构为 List,结果终于正确了。

教训

bug 起因:

  • 选择数据库时,太关注数据结构的形态,并没有考虑其中的特点。致使出现使用唯一键的域存放不唯一键这种情况发生

教训:

  • 仔细思考使用的数据结构,对于不唯一的键,一定不能放到唯一的容器中

本文采用 CC BY-NC-SA 3.0 协议进行许可,在您遵循此协议的情况下,可以自由共享与演绎本文章。
本文链接:https://blog.codeingboy.me/unexpected-bugs/

发表评论

电子邮件地址不会被公开。 必填项已用*标注