zoukankan      html  css  js  c++  java
  • 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的新版本,各项目引用。

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

  • 相关阅读:
    原生js设置cookie
    vuex数据持久化存储
    export和export default的区别
    userAgent判断是微信还是企业微信
    SVN的使用方法
    Js字符串反转
    通用JS10——一元加和减
    通用JS十——递增/递减操作符
    通用JS9
    @SuppressWarning注解用法
  • 原文地址:https://www.cnblogs.com/codestory/p/5512087.html
Copyright © 2011-2022 走看看