我遇到一个 Bug,金额大于一千万就报错

Author Avatar
Pang Jian 1月 13, 2016
总字数:1.1k 预计阅读:3 min
  • 在其它设备中阅读本文章

这是一个在测试环境发现的一个 BUG,感觉很有趣,便写出来分享一下。本文的灵感以及标题都来自于《我遇到一个 BUG,每逢周三就崩溃》。
我在银行做系统开发,“金额”基本上是最常见的字段,也是最不能出错的字段了。每一个错误都代表着实打实的资金损失,无论是客户的还是银行的。所以,作为开发人员,对这个字段也相对敏感一些。金额这个字段的规则也相对其他字段更复杂一些。举个例子来说,金额是一个数字,可以是这样 12;当然也有小数点的情况,比如这样 12.34;人们还有这样的习惯,每隔 3 位有一个逗号分隔符,比如这样 1,000.23。可是一千万是个什么特殊情况,为什么会有问题呢?

目前系统为了校验前端上送的数据,有一套统一的校验机制。大概过程就是,先使用 jackson 框架将前端上送的 JSON 数据进行解析,然后根据这个字段配置的正则表达式进行校验,校验不通过,就会报错。
这是一个工作日,在我们内部通讯工具上弹出来一个同事的窗口。“我们有一个交易上送金额,80099690.89 元,校验就报错,但是 8009969.89 元校验就不报错。”我见到这个问题后,第一反应是:“怎么可能,这个校验规则在生产环境运行了那么久都没有出错,怎么会突然有问题呢?会不会是哪里搞错了?”然后,这位同事又发来“前端上送的 JSON 报文中,金额使用的数字类型 amount:80099690.89, 但我们平时都上送的字符串类型amount:'80,099,690.89'”。于是,我开始怀疑那个正则表达式是否正确,还在埋怨是谁写的正则,居然要求必须有逗号分隔符的时候,我看到了这正则:^(\d*|\d{1,3}(,\d{3})*)(\.\d{1,2})?。居然是对的!完美的考虑了带逗号和不带逗号的场景。这时候,这位同事又发来消息“把金额按 string 上送就不报错了amount:'80099690.89'
WTF?!下面这个表情就是我当时的表情。
WTF
我们知道,在 JavaScript 中,stringnumber 是可以随时互转的,难道 amount:80099690.89amount:'80099690.89'上送到后端会有不同的逻辑?几千万大小的数字也没有超过精度呀。
于是我开始看这篇文章开头提到的那个字段校验公共机制,先使用 Jackson 解析,然后再利用正则表达式校验。难道是 Jackson 有问题?看过官方文档,Jackson 会将 number 默认解析成 Java 的 Double 型,string则转换为 Java 的 String 型,而 Double 在进行正则校验之前需要先转换为String

Double amount1 = new Double("80099690.89");
Double amount2 = new Double("8009969.89");
System.out.print("amount1 = " + amount1.toString());
System.out.print("amount2 = " + amount2.toString());

打印结果为:

amount1 = 8.009969089E7
amount2 = 8009969.89

相信大家已经看出来了,原来是这样!Java 里 DoubletoString方法当大于 7 位数的时候会取科学计数法。所以科学计数法的结果是无法匹配这个正则的 ^(\d*|\d{1,3}(,\d{3})*)(\.\d{1,2})?
从那以后,我终于明白编码规范中为什么要求前端都是用 string 类型了。我还专门看了接口文档,将类型都调整为了 string
其实无论一个 bug 看上去多么随机和不可思议,如果你挖的足够深的话,总能找到一个符合逻辑的解释,极少有真的“不相关”的错误,几乎都是你自己的错。这就是为什么我微博的简介里会写这么一句话“电脑确实比人靠谱,你和他们打交道时一旦有问题,肯定是你的问题。不像人,即使他们有问题也不轻易承认。”

写在最后:文中提到的那位同事,现在已经去新的岗位提升自己去了,写这篇文章也算纪念一下我们一起共事的时光。^_^~
EOF

Documentation licensed under CC BY-SA 4.0.
本文链接:https://www.pangjian.me/2016/01/13/a-strange-bug/