JSON金额解析BUG的解决过程
【原创申明:文章为原创,欢迎非盈利性转载,但转载必须注明来源】
这是在我们开发的一个支付系统中暴露的一个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的新版本,各项目引用。
我有时候爱说一句很装的话:一个问题,如果你找到了一个解决方案,那么说明你还没有理解这个问题。
JSON金额解析BUG的解决过程的更多相关文章
- android一个下拉放大库bug的解决过程及思考
android一个下拉放大库bug的解决过程及思考 起因 项目中要做一个下拉缩放图片的效果,搜索了下github上面,找到了两个方案. https://github.com/Frank-Zhu/Pul ...
- 一个疑难bug的解决过程
一个crontab脚本,下载一个文件并把内容入mysql数据库.具体流程如下: 1, wget一个文件. 2,处理文件生成一个中间文件. 3,将中间文件load入库. 05 10 * * * /hom ...
- 记录一个前端bug的解决过程
人在江湖飘,哪能不挨刀. 我挨了重重一bug.严格来讲这可能是我职业生涯以来的首个悲惨经历,因为凭我的知识储备和经验,基本上任何可重现的bug都是可解的.然而这个bug却困扰了我三个月之久,它具有以下 ...
- php url链接地址传数组方法 json_decode解析数组失败 经过url链接的json数组解析出错的解决方法 (原)
先说出现的问题: 请求一个接口(例如 http://www.a.com/getmes.php)需要传一个数组参数 param ,值为 数组 array(0=>'刘师傅',1=>'1760 ...
- springboot中json转换LocalDateTime失败的bug解决过程
环境:jdk1.8.maven.springboot 问题:前端通过json传了一个日期:date:2019-03-01(我限制不了前端开发给到后端的日期为固定格式,有些人就是这么不配合), ...
- 解析xml数据存入bean映射到数据库的 需求解决过程
解析xml数据存入bean映射到数据库的 需求解决过程2017年12月19日 15:18:57 守望dfdfdf 阅读数:419 标签: xmlbean 更多个人分类: 工作 问题编辑版权声明:本文为 ...
- 解决ajax请求返回Json无法解析"\"字符的问题
原因:获取身份证信息,涉及图片路径,存在“\”字符,导致Json解析错误 解决思路:将返回类型从"json"改成"text",此时返回的数据类型变成字符串,将字 ...
- 使用Gson轻松解决复杂结构的Json数据解析
转载请注明来源: http://blog.csdn.net/kjunchen/article/details/50961803 JSON简介 JSON(JavaScript Object Notati ...
- Jquery.ajax报parseerror Invalid JSON错误的原因和解决方法:不能解析
(默认: 自动判断 (xml 或 html)) 请求失败时调用时间.参数有以下三个:XMLHttpRequest 对象.错误信息.(可选)捕获的错误对象.如果发生了错误,错误信息(第二个参数)除了得到 ...
随机推荐
- CSharpGL - Object Oriented OpenGL in C#
Object Oriented OpenGL in C#
- ABP源码分析十:Unit Of Work
ABP以AOP的方式实现UnitOfWork功能.通过UnitOfWorkRegistrar将UnitOfWorkInterceptor在某个类被注册到IOCContainner的时候,一并添加到该类 ...
- jQuery源码分析系列
声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://git ...
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(61)-如何使用框架来开发
系列目录 前言: 有些园友经常问如何正确快速开发,但是我告诉你没有什么帮助文档比自己动手做更加实在,不用代码生成器 这一节专门抽了些时间来非常非常详细演示这个框架的数据流,废话不多说,现在开始!下面看 ...
- Java设计模式之策略模式(Strategy)
前言: 最近一直在学习基于okHttp网络请求,学习的过程中就想起了之前项目中有这么一个需求不同的接口要采用不同的加密方式,比如登录之前要采用RSA加密,登录之后要采用AES加密,当时是采用靠传递一个 ...
- NLP&数据挖掘基础知识
Basis(基础): SSE(Sum of Squared Error, 平方误差和) SAE(Sum of Absolute Error, 绝对误差和) SRE(Sum of Relative Er ...
- Oracle --> Vertica 数据类型转换规则
需求:在Vertica数据库上建表,表结构来源于原Oracle数据库,故需要转换成Vertica数据库库表结构. 实际转换操作需要评估源库用到的所有数据类型和数据本身特性. 下面是总结的某场景下的 ...
- sql重置自增长
SQL的自增列挺好用,只是开发过程中一旦删除数据,标识列就不连续了 写起来 也很郁闷,所以查阅了一下标识列重置的方法 发现可以分为三种: --- 删除原表数据,并重置自增列 truncate tabl ...
- 中文分词工具探析(一):ICTCLAS (NLPIR)
1. 前言 ICTCLAS是张华平在2000年推出的中文分词系统,于2009年更名为NLPIR.ICTCLAS是中文分词界元老级工具了,作者开放出了free版本的源代码(1.0整理版本在此). 作者在 ...
- ASP.NET MVC5中的Model验证
Model验证是ASP.NET MVC中的重要部分,它主要用于判断输入的数据类型及值是否符合我们设定的规则,这篇文章就介绍下ASP.NET MVC中Model验证的几种方式. 后台验证 DataAnn ...