前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JSON金额解析BUG的解决过程

JSON金额解析BUG的解决过程

作者头像
程序猿讲故事
发布2019-09-27 15:30:02
1.1K0
发布2019-09-27 15:30:02
举报
文章被收录于专栏:程序猿讲故事程序猿讲故事

【原创申明:文章为原创,欢迎非盈利性转载,但转载必须注明来源】

这是在我们开发的一个支付系统中暴露的一个BUG,问题本身比较简单,有意思的是解决问题的过程。将过程分享出来,希望能够对大家有所帮助。

一、错误现象

在我们的支付系统中,有一个账户模块负责记录交易的流水,以供后续的查询以及对账清账等功能使用。就在春节放假前最后一天,当客户完成交易后,运营同事发现一个天大的问题,流水表中的部分金额,跟提交支付的金额有出入,差了几分钱。

这位客官说了,几分钱的问题,还是问题?哈哈,我也这么想,奈何运营、产品、测试同事们都不答应。好吧,其实我们程序猿是有洁癖的,怎么容忍有这样的问题出现?把火车票、机票都先放在看不见的地方,解决问题先。

先从不同的数据库中找出付款前后的金额进行比较,发现还真不是个案。这是当时比较的结果,黑体部分有差异。

这些数据中,业务系统的金额跟客户提交金额相等,账户记录的金额有异。

二、分析并定位问题

1.数据流转过程

下图是一个简略的支付、记录流水的过程。

通过检查各个环节的报文及数据库中保存的数据,发现问题出在第4步,金额在支付系统中无误,发送到账户系统并保存到数据库后就出现了误差。这儿发生了什么?

2.账户记账的处理过程

这是一个简略的处理过程,支付系统生成json并传输到账户系统,解析后保存到数据库。

经过查看各个环节的日志,发现问题出在解析环节。

3.错误重现

经过定位、调试,发现问题出在解析json数据的代码上。账户系统接收到传输来的json数据后,首先保存在一个字符串content中,然后利用代码将字符串转换为json对象。

JSONObject json = JSONObject.fromObject(content);

在Eclipse中设置断点跟踪,发现这行代码执行前后的变量值差异:

在转换前后,金额从 527726.03 变成了527726,这个差异符合前面观察到的错误现象。仔细查看json字符串,发现金额没有使用双引号括起来,说明生成json的时候,直接赋值的是金额,而不是转成字符串后再赋值。

那么如果将金额用双引号括起来,会有这个问题吗?再测试一下

神奇的是,转换为字符串后,转成json就没有问题了。

我们解析json,使用的是sf的json-lib库,其他json库是不是也有问题呢?使用另两个json库做了一些测试后发现,只有json-lib有这个问题。

有问题

<dependency>

<groupId>net.sf.json-lib</groupId>

<artifactId>json-lib</artifactId>

<version>2.4</version>

<classifier>jdk15</classifier>

</dependency>

没问题

<dependency>

<groupId>org.json</groupId>

<artifactId>json</artifactId>

<version>20160212</version>

</dependency>

<dependency>

<groupId>com.google.code.gson</groupId>

<artifactId>gson</artifactId>

<version>2.6.2</version>

</dependency>

三、初步解决方案

根据前面的分析,立刻就有了两个很自然的解决方案:修改json中金额的格式、换JSON库。

1.修改json格式

用这个方案,只需要在支付系统中生成json对象的时候,将金额转成字符串之后在赋值到json即可。

但这种方案有缺点,需要将所有生成json的地方都检查一遍,确保所有金额都用字符串传递。因为这个地方代码有问题,其他地方代码也会有问题,只是还没暴露出来而已。

2.替换json库

这种方案,可以将json-lib替换为org.json。暂时不考虑gson,是因为这个gson库需要为json编写对应的Java类,修改工作量比较大。

那么,json-lib和org.json在代码生有什么差异呢?网上找了找,粗略的比较如下:

json-lib

org.json

构造 json 对象

JSONObject.fromObject(content)

new JSONObject(content)

是否存在key

containsKey()

has()

array方法

size() add()

length() put()

读取json的限制

限制数据格式

spring封装

MappingJackson2HttpMessageConverter 支持

貌似缺省不支持

这种方案的代码量也是很大,所有涉及到json转换的地方都需要修改代码。如果采用替换json库的方法,有没有更简便一点的做法呢?

把《设计模式》里面的各种名称想了想,“适配器模式”,能不能用上?

3.替换json库+适配器

针对这个方案,做了一些技术预演,大概思路如下图

理想的目标是所有源码只需要使用一次查找-替换操作即可。

这个方案应该是可行的,只是这两个适配器类的写法需要比较严谨一点,写完代码后需要经过充分的测试无误,才能真正执行。

四、问题解决了吗?

前面提到了三种解决方案,从修改工作量上来看,第一种方案应该是最合适的,只需要修改支付系统的代码即可,代码也容易定位,修改也不容易出错。采用适配器的这个方案,看起来很高大上的样子,但风险较大,暂时先放弃。

还有没有更简单的方法?

1.json-lib为什么会出错?

负责开发账户的同事,下载了json-lib的源码,进行了进一步的跟踪调试,更准确的定位到了出错的位置:是在调用commons-lang.jar中的NumberUtils类中代码时出错。下图是一个简单的调用过程。

最终出错的地方是在解析 Float !!重新写一个最简单的测试用例,

float floatValue = Float.valueOf("542772.03");

结果,floatValue = 542772.0。这是JDK的Float 数据类型固有的问题,我们同时在JDK1.7和JDK1.8下进行测试,都有这个问题。

同时,顺手写了一个测试用例,找出最小的十个会出错的金额,如下:

error1131072.01131072.02

error2131072.04131072.05

error3131072.07131072.06

error4131072.09131072.1

error5131072.13131072.12

error6131072.15131072.16

error7131072.18131072.19

error8131072.21131072.2

error9131072.24131072.23

error10131072.26131072.27

基本上每过几分钱就会出错。

2.有什么新的解决方案?

能想到两个新的方案

1、修改 java.lang.Float

2、修改 org.apache.commons.lang.math.NumberUtils

这两种方案,技术上可行吗?要从这个思路上去解决问题,需要解决两个问题:

1、能不能修改源码,解决BUG?

2、怎么让修改后的类,生效?

考虑到后续需要讨论的解决方案,先介绍一个大家可能司空见惯但没注意过的概念::ClassLoader

3.JVM ClassLoader

参考书目:《深入理解Java虚拟机》,有兴趣的自行阅读。(其实是我也讲不清楚)

① Tomcat中的class 加载顺序

对于普通java类,按照如下优先级进行加载。

l tomcat/webapps/<war>/WEB-INF/classes

l tomcat/webapps/<war>/WEB-INF/lib/*.jar

l tomcat/lib/*.jar

l jre/lib/*.jar

是不是所有的java类都是这个加载顺序?如果可以,我们是不是可以随便重载jdk自己提供的类?

② JRE ClassLoader

Java在设计的时候已经考虑到这个风险,不能允许随便替换JRE自己的类。所以,针对JRE自身的代码,使用的是另一套ClassLoader。对所有java.*和javax.*,使用的加载顺序

详细解析,自行查资料吧,我也不懂。

关键是结论:除非我们重写 JRE的jar,才能通过修改 java.lang.Float来解决问题。何况Float的问题,应该不好修改,否则Java早解决了。

3.怎么修改NumberUtils

在NumberUtils,方法 createNumber(String)首先调用createFloat(String)解析,如果抛Exception,再调用createDouble(String)。

有两个自然地修改方案:

1、修改 createNumber(),不再调用 createFloat(),直接调用createDouble()。

2、修改 createFloat(),如果数据解析出错,抛异常。

下面列了一个粗略的修改createFloat(String)的实现,基本思路是解析后再同原字符串做一个比较,如果值不同则抛异常。

public static Float createFloat(String str) {

if (str == null) {

return null;

}

str = removeZeroTail(str);

Float floatValue = Float.valueOf(str);

if (!removeZeroTail(String.valueOf(floatValue)).equals(str)) {

throw new NumberFormatException(str + " parse float error.");

}

return floatValue;

}

4.修改后的NumberUtils放哪儿?

根据前面对class loader的分析,修改后的NumberUtils类,有两个保存位置。

① 在账户系统中重写NumberUtils类

将NumberUtils类重写在src/main/java中,部署后在war/WEB-INF/classes下。

如果采用这个方案,需要在所有的项目中重写这个类。

③ 重做一个commons-langs.jar

我们使用的版本是2.6,如果能够重做一个新的版本,并让各个项目能方便的引用,这个方案应是最简单的。恰好,我们有内部的Maven库,分享jar不是问题。

五、最终方案:重做commons-lang.jar

1.代码修改

这个就不多说了,Eclipse建一个项目,进行必要的修改,然后打包放到内部maven库中。顺便推荐一个搭建maven内部库的利器:nexus,价格便宜(免费)量又足。当然前提是你需要有一个能够供大家访问的服务器。

2.项目修改方案

各项目修改方案,仅需要修改 pom.xml

① 所有引用了commons-lang的depencency

<dependency>

<groupId>net.sf.json-lib</groupId>

<artifactId>json-lib</artifactId>

<version>2.4</version>

<classifier>jdk15</classifier>

<exclusions>

<exclusion>

<artifactId>commons-lang</artifactId>

<groupId>commons-lang</groupId>

</exclusion>

</exclusions>

</dependency>

注意exclusion所有的commons-lang老版本引用。

② 引用commons-lang的新版本

<dependency>

<groupId>commons-lang</groupId>

<artifactId>commons-lang</artifactId>

<version>2.7.0-SNAPSHOT</version>

</dependency>

六、解决方案的变迁过程

简单列一下方案变迁过程,

1、支付系统修改json格式的封装代码,金额都使用字符串。

2、账户系统替换 json 解析包。

3、写一个 json proxy,从org.json继承,实现json-lib的接口。

4、在项目中重写 NumberUtils工具类。

5、重做一个commons-lang的新版本,各项目引用。

我有时候爱说一句很装的话:一个问题,如果你找到了一个解决方案,那么说明你还没有理解这个问题。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序猿讲故事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、错误现象
  • 二、分析并定位问题
    • 1.数据流转过程
      • 2.账户记账的处理过程
        • 3.错误重现
        • 三、初步解决方案
          • 1.修改json格式
            • 2.替换json库
              • 3.替换json库+适配器
              • 四、问题解决了吗?
                • 1.json-lib为什么会出错?
                  • 2.有什么新的解决方案?
                    • 3.JVM ClassLoader
                      • ① Tomcat中的class 加载顺序
                      • ② JRE ClassLoader
                    • 3.怎么修改NumberUtils
                      • 4.修改后的NumberUtils放哪儿?
                        • ① 在账户系统中重写NumberUtils类
                        • ③ 重做一个commons-langs.jar
                    • 五、最终方案:重做commons-lang.jar
                      • 1.代码修改
                        • 2.项目修改方案
                          • ① 所有引用了commons-lang的depencency
                          • ② 引用commons-lang的新版本
                      • 六、解决方案的变迁过程
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档