老项目 json-lib 2.4 JSONTokener 精度丢失问题排查

一次 0.001 的生产数据异常:WMS 出入库数量精度丢失排查

出入库数量从 29056.122 变成 29056.121,差值只有 0.001,且大部分数据看起来正常。但对库存、订单、供应链系统来说,这是数据正确性问题。本文记录这次真实排查:如何通过落库数据、日志、本地复现,最终定位到 JSONTokener.nextValue() 把字符串解析成了 Float。
排查完成日期:2026-09-04(本文数据已全部脱敏)

一、问题现象

WMS 通过 WebService 向集成服务推送出入库数据,数量字段 MENGE 原始报文为 29056.122,进入后续系统后变成 29056.121

两个特点让它隐蔽性很强:

  1. 差值仅 0.001,业务侧容易忽略;

  2. 并非所有数据都出错 ------比如 29056.125 就一切正常,所以最初看起来像偶发异常。

典型问题数据(已脱敏):执行单 ORD-202608-XXXXXX,物料 MAT-XXXXXX,单位 KG,行号 10。

二、先说结论

WMS 原始数据正确,MQ 没有改数据,下游也没有计算错误。问题出在集成服务内部的 JSON 解析:字符串被 JSONTokener.nextValue() 解析成了 Float,浮点舍入后无法还原。

javascript 复制代码
"29056.122"(String)

↓ JSONTokener.nextValue()

29056.121(Float,IEEE 754 单精度,舍入发生在这里)

↓ 转回字符串

"29056.121" ------ 原始值已丢失,无法恢复

三、定位过程:三轮证据

生产排查不能一上来就看代码,正确姿势是先逐段比对链路,确认数据在哪一跳发生变化。

证据一:落库的原始报文 vs 解析结果

集成服务消息存储表中,同一记录的原始 XML 和解析后 JSON 并排保存:

| 字段 | 内容 | 值 |

| --- | --- | --- |

| message_content | WMS 原始 XML | <MENGE>29056.122</MENGE> |

| result_content | 服务解析后的 JSON | "menge":"29056.121" |

数据在集成服务内部就已经变了。这一条证据直接排除了 WMS 生成错误、MQ 传输丢精度、下游首次修改、DB 显示误差。

证据二:全链路日志交叉验证

按执行单 + 物料 + 时间(2026-08-11 10:36)追踪同一条数据:

| 链路位置 | 数量 |

| --- | --- |

| WMS → Integration Service | 29056.122 |

| Integration Service → MQ | 29056.121 |

| MQ → Order Service | 29056.121 |

| Order Service → Supply Chain Service | 29056.121 |

变化只发生在 Integration Service 一跳,MQ 和下游只是传播了错误数据。

证据三:本地最小复现

从生产 fat jar 中提取实际运行依赖(org.json、fastjson、json-lib 及传递依赖),按生产调用顺序复刻:

| 步骤 | 操作 | 结果 |

| --- | --- | --- |

| ① | XML.toJSONObject(xml) | 29056.122 |

| ② | fastjson 重解析 | 29056.122 |

| ③ | JSONTokener.nextValue() | Float = 29056.121 |

| ④ | fastjson getString("MENGE") | "29056.121" |

最小复现只有两行:

java 复制代码
Object value = new JSONTokener("29056.122").nextValue();

System.out.println(value.getClass()); // class java.lang.Float

System.out.println(value); // 29056.121

new JSONTokener("29056.125").nextValue() 输出的还是 29056.125。整个过程中没有减法、没有四舍五入、没有截断,仅仅是"字符串 → 浮点数"就改变了数据。是否产生肉眼可见的误差取决于具体数值和它的二进制表示,这正是问题显得"偶发"的原因。

四、根因

业务代码中的这段调用:

java 复制代码
new JSONTokener(stockReq.getString("StockItem")).nextValue();

nextValue() 遇到数字时走了 Float 路径,精度在这里丢失,之后转回字符串也无法还原。业务数量本应是精确值,却进入了近似浮点域------这就是典型的 String → Float → String 精度丢失。

五、为什么这类问题很难发现?

如果所有数据都异常,反而好查。麻烦在于 IEEE 754 的舍入是"挑值"的:29056.125 正常、29056.122 出错,于是生产上只有少量特定尾数的数据暴露问题,大部分数据看起来毫无异常------问题可以长期潜伏。

另外,代码检索发现 JSONTokener.nextValue() 在该服务中共有 17 处 调用,横跨多个 WebService 方法,潜在受影响字段不止 MENGE,还有 QTYCHANGE_QTYORDER_QTYGIVE_WAY_QTY 等。凡是"业务字段 → JSONTokener → 低精度浮点"的路径都存在同类风险。

六、一个重要排查经验:源码 ≠ 运行时

公开的 json-lib 2.4 JSONTokener 源码中,普通数字解析路径与本次生产运行结果存在差异。所以严谨的说法不是"json-lib 2.4 一定把小数解析成 Float",而是"本次生产实际运行环境中,nextValue() 对该输入返回了 java.lang.Float"。

排查线上依赖问题时,真正应该相信的是 ClassLoader 实际加载的 class,而不是 pom.xml。建议加上运行时检查:

java 复制代码
System.out.println(

JSONTokener.class.getProtectionDomain().getCodeSource().getLocation()

);

确认类到底来自哪个 jar,同时排查重复依赖、shade/relocation、多版本共存和 ClassLoader 差异。

七、解决方案

短期止血:

  1. new JSONTokener(str).nextValue() 替换为统一 JSON 框架的解析(如 JSON.parse(str)),并同步处理 net.sf.json.JSONArray 分支,保持单对象/数组逻辑一致。改动小、风险可控,可快速上线;

  2. 数量字段改用 BigDecimalobject.getBigDecimal("MENGE"))或纯透传场景直接 getString,原则是数量全程不进 Float/Double;

  3. 对历史数据巡检:比对消息表中原始报文数量与解析结果数量,逐条找出不一致记录,评估修正、重推或补偿。代码修复只救未来,历史上的 29056.121 不会自己变回去。

长期治理:

  1. 逐步移除 json-lib。当前系统同时使用 org.json、fastjson、json-lib 三套框架,一次转换要经历 XML → JSON → JSONObject → JSONTokener → JSONObject → String 的多次变换,这个转换链本身就是风险;

  2. 数量/金额类字段统一 BigDecimal 或 String,禁止进入浮点类型;

  3. 增加全链路精度回归测试:用 29056.12220.00065535.999 等不同数量级和尾数的用例,从 XML 进、到 MQ 出,断言最终值 == 原始值------测的是整条链路,不是单个方法;

  4. 对核心数量字段增加线上入口校验:原始值 != 解析值时记录告警(含单号、字段、两个值),把问题拦在入口,而不是等下游对账或业务投诉。

八、写在最后

29056.122 变成 29056.121,表面上是一次 JSON 解析的坑,本质上是一个设计问题:需要精确表示的业务数量,在中间环节被放进了低精度浮点域。

要记住的不是"某个 JSON 库有问题",而是:库存、订单、出入库、供应链这类系统里,业务数量应该全程走 String / BigDecimal。对这些系统来说,0.001 不是一个显示问题,而是一个数据正确性问题。

相关推荐
IT_Octopus4 小时前
JSON 日志里的 `{“$ref“:“$.xxx“}`:从 fastjson 兼容包到原生 fastjson2 的迁移实录
开发语言·python·json
数据狐(Datafox)9 小时前
mercadolibre.item_get 工程实战:美客多商品详情API技术解析与落地应用
java·人工智能·mysql·json
Fcy64810 小时前
Linux下 应⽤层⾃定义协议与序列化(JSON方法)
linux·json
半亩码田1 天前
C#转Python第4.4篇:JSON 处理:json 模块 vs System.Text.Json
python·c#·json
清水白石0082 天前
Python 类型设计深度解析:TypedDict 能否替代 dataclass?从 JSON 数据边界到 API 设计的最佳实践
java·python·json
DevOpenClub2 天前
网页采集如何稳定输出结构化数据:JSON、链接与快照三阶段流水线
数据库·json·api
霸道流氓气质2 天前
Spring AI 结构化输出:JSON Mode 与 BeanOutputConverter
人工智能·spring·json
SelectDB3 天前
StarRocks 适合做日志分析吗?
数据分析·json·自动化运维
SelectDB3 天前
为什么 JSON 正在成为分析数据库新的竞争点?
数据库·json·agent