一线真实事故复盘。标题不是夸张,确实因为一个同名类 JSONObject的错误引用,我的周末双休没了:服务发布后一切看似正常,日志不报错、接口不报错,但关键字段悄无声息地空了。最终原因是 org.json.JSONObject 与 fastjson.JSONObject 混用,且在两个方法中错误引用,导致值无法显示。本文提供完整的故事情景、代码原理解析、定位错误原因、修改错误的成套操作,以及可复用的最佳实践清单。
一、故事与现场:不报错,却看不见的数据
周五晚上例行发版,测试环境全绿,灰度也没报警,大家准备愉快双休。结果周六早上开始,用户反馈页面某块数据空白。看日志:没有异常;看接口 HTTP 码:200;看 JSON:结构齐全,就是关键对象字段是空对象 {}
或者干脆不显示。
更离谱的是,本地 dev 调试时,一切正常;打成 jar 包跑(尤其是 fat/uber jar 或容器环境)就出问题。
排查两小时后,指向一个无害的工具方法:
java
// 注意:这里使用了 org.json
import org.json.JSONObject;
public class JsonHelper {
public static JSONObject toOrgJson(Object data) {
// 以为很聪明:传什么都能包进去
return new JSONObject(data); // 不会抛异常
}
}
调用侧传入的是 fastjson 的 JSONObject:
java
// 注意:这里使用了 fastjson
import com.alibaba.fastjson.JSONObject;
public class AssembleService {
public JSONObject buildFastjsonData() {
JSONObject f = new JSONObject();
f.put("title", "周末加班实录");
f.put("meta", new JSONObject().fluentPut("owner", "ops").fluentPut("tag", "lesson"));
return f;
}
}
在 Controller 里进行转换后继续处理:
java
// Controller:想统一处理为 org.json 的 JSONObject
org.json.JSONObject payload = JsonHelper.toOrgJson(assembleService.buildFastjsonData());
// ... 继续渲染、拼装
现象是:本地跑没问题;打包后,payload
里的 meta
要么是空的,要么层级丢失。没有异常、没有 warn、没有 stacktrace,数据就这样蒸发了。
二、复现最小示例
为了稳定复现,我们把问题浓缩成最小代码。
错误示例一:两个方法各自导入了不同的 JSONObject,且用 Object 作为桥,编译通过但行为不稳定。
java
// JsonHelper.java
import org.json.JSONObject;
public class JsonHelper {
public static JSONObject toOrgJson(Object any) {
return new JSONObject(any); // 看似万能,但取决于 org.json 的版本/构造器路径
}
}
java
// Demo.java
import com.alibaba.fastjson.JSONObject;
public class Demo {
public static void main(String[] args) {
JSONObject f = new JSONObject();
f.put("a", 1);
f.put("b", new JSONObject().fluentPut("x", 2));
org.json.JSONObject o = JsonHelper.toOrgJson(f);
System.out.println("Result = " + o.toString());
}
}
错误示例二:在更多层级中传递,IDE 自动导包混淆,导致一处用 fastjson,一处用 org.json,调用点用 JSONObject
简名,肉眼不易分辨。某些路径下编译器选择的构造器不同,运行时又被打包后的依赖版本左右。
三、原理解析:为什么打包后才出事
要点一:org.json.JSONObject
的构造器重载
JSONObject(String source)
:从 JSON 字符串解析,最稳。JSONObject(Map<?, ?> m)
:从 Map 构造,会遍历 map 并 wrap。JSONObject(Object bean)
:走 bean introspection/反射,将字段转为键值。- wrap 逻辑对未知类型/非标准 Map 实现的处理在不同版本中略有差异。
要点二:fastjson.JSONObject
- 实现了
Map<String, Object>
,但它是 fastjson 自己的类(含嵌套 fastjson JSON 类型)。 - 当作为
Object
传入new org.json.JSONObject(Object bean)
时,版本不同可能选择 bean 构造器路径(而不是 Map 构造器),反射提取字段,导致内部值被 wrap 成 org.json 未知的结构,或对嵌套 fastjson 类型 wrap 不一致。 - 当作为
Map
传入,通常能工作,但嵌套的 fastjson 子对象仍需被正确 wrap;某些版本会出现对嵌套值处理异常安静失败(不抛错但丢数据)。
要点三:打包后暴露
- 本地与打包后 classpath 顺序、org.json 版本不同(多份重复依赖、shade 重定位、冲突合并)。
- 不同版本的 org.json 对
wrap
、JSONObject(Map)
、JSONObject(Object)
的实现差异触发无声失败。
四、定位步骤(一步步排查)
- 打印运行时使用的 org.json 来自哪个 jar
java
System.out.println(org.json.JSONObject.class.getProtectionDomain().getCodeSource().getLocation());
输出的 jar 路径若和你在本地/IDE 里看到的版本不一致,八成是打包时被换了版本或被 shade 了。
- 查看依赖树(确认重复/冲突)
- Maven:
bash
mvn -q dependency:tree
- Gradle:
bash
./gradlew -q dependencies
关注 org.json:json 是否出现多版本,或被某个传递性依赖引入。
- 写个对照单测,验证三种构造方式行为
java
import com.alibaba.fastjson.JSONObject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class InteropTest {
@Test
void compareWays() {
JSONObject f = new JSONObject();
f.put("a", 1);
f.put("b", new JSONObject().fluentPut("x", 2));
// 方式1:字符串桥(最稳)
org.json.JSONObject o1 = new org.json.JSONObject(f.toJSONString());
assertEquals("{\"a\":1,\"b\":{\"x\":2}}", o1.toString());
// 方式2:Map 拷贝(较稳)
org.json.JSONObject o2 = new org.json.JSONObject(new java.util.LinkedHashMap<>(f));
assertEquals(o1.toString(), o2.toString());
// 方式3:Object 构造(易踩雷)
org.json.JSONObject o3 = new org.json.JSONObject((Object) f);
// 在某些版本/环境下,这里会与 o1 不一致(可能空/缺字段/层级错)
System.out.println("o3 = " + o3.toString());
}
}
五、错误原因
- 混用了两套 JSON 库,还用
Object
做桥接,构造器解析路径随版本/类加载器变化,行为不稳定。 - 代码中多处使用
JSONObject
简名,IDE 自动导包导致一处用 fastjson,另一处用 org.json,肉眼难以发现。 - 打包后 org.json 版本变化(重复依赖/shade),触发不同的 wrap/构造分支,导致无声数据丢失。
六、修复方案
优先级从高到低:
- 统一库,全链路只用一种 JSON 库
- 如果可能,选一个(fastjson2/jackson/org.json 等),避免跨库互转。
- 需要互转时,使用字符串桥(最稳健)
java
public final class JsonInterop {
private JsonInterop() {}
public static org.json.JSONObject toOrg(com.alibaba.fastjson.JSONObject f) {
if (f == null) return null;
return new org.json.JSONObject(f.toJSONString());
}
public static com.alibaba.fastjson.JSONObject toFast(org.json.JSONObject o) {
if (o == null) return null;
return com.alibaba.fastjson.JSONObject.parseObject(o.toString());
}
}
- 或使用Map 拷贝桥(避免库特有类型)
java
public static org.json.JSONObject toOrgMapBridge(com.alibaba.fastjson.JSONObject f) {
if (f == null) return null;
return new org.json.JSONObject(new java.util.LinkedHashMap<>(f));
}
- 如必须传
Object
,至少强制走 Map 构造(不推荐但比 bean 构造稳)
java
public static org.json.JSONObject toOrgForceMap(Object maybeMap) {
if (maybeMap == null) return null;
if (maybeMap instanceof java.util.Map) {
return new org.json.JSONObject((java.util.Map<?, ?>) maybeMap);
}
// 兜底:字符串桥(或抛出异常)
return new org.json.JSONObject(String.valueOf(maybeMap));
}
- 显式限定类型,避免误导包
- 禁止在公共 API 中使用
Object
/JSONObject
简名做参数;明确写全限定名,或将互操作限定在工具类中。 - 示例(错误 -> 正确):
错误:
java
public org.json.JSONObject convert(Object any) { return new org.json.JSONObject(any); }
正确:
java
public org.json.JSONObject convert(com.alibaba.fastjson.JSONObject f) {
return new org.json.JSONObject(f.toJSONString());
}
七、构建与依赖治理
- 锁定 org.json 版本,确保构建、打包、运行一致(Maven 示例)
xml
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20210307</version>
</dependency>
- 排除重复/冲突依赖
xml
<dependency>
<groupId>some.group</groupId>
<artifactId>some-artifact</artifactId>
<exclusions>
<exclusion>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</exclusion>
</exclusions>
</dependency>
- 使用 shade/uber-jar 时
- 确保只打进一份 org.json;
- 不要随意 relocate org.json 或 fastjson 包名;
- 打包后用如下代码在启动时打印实际加载的 jar 路径:
java
System.out.println("[org.json from] " + org.json.JSONObject.class.getProtectionDomain().getCodeSource().getLocation());
八、代码规范与 CI 防护
- 命名规范
- 禁止在同一代码库同时使用两个
JSONObject
简名;推荐统一以全限定名出现,或仅在工具类中出现跨库互转。
- 禁止在同一代码库同时使用两个
- IDE/检查工具
- 开启同名类自动导包警告,对
JSONObject
等设为强提示。 - Checkstyle/PMD 禁止通配符导入,禁止在公共接口暴露
Object
作为 JSON 容器参数。
- 开启同名类自动导包警告,对
- CI 任务
- 加入
mvn -q dependency:tree
或 Gradle 依赖报告检查,阻断重复/冲突版本合入。 - 单测覆盖跨库互转的关键路径,校验 toString 等价。
- 加入
九、完整演进示例:从错误到修复
错误版本(隐患:Object + 构造器不确定性)
java
// JsonHelper.java
import org.json.JSONObject;
public class JsonHelper {
public static JSONObject toOrgJson(Object data) {
return new JSONObject(data);
}
}
java
// Controller.java
import com.alibaba.fastjson.JSONObject;
public class Controller {
public String handle() {
JSONObject f = new JSONObject();
f.put("a", 1);
f.put("b", new JSONObject().fluentPut("x", 2));
org.json.JSONObject o = JsonHelper.toOrgJson(f);
// 打包后:o 可能是 {"a":1,"b":{}} 或缺字段
return o.toString();
}
}
修复版本(字符串桥)
java
// JsonInterop.java
public final class JsonInterop {
private JsonInterop() {}
public static org.json.JSONObject toOrg(com.alibaba.fastjson.JSONObject f) {
if (f == null) return null;
return new org.json.JSONObject(f.toJSONString());
}
}
// Controller.java
import com.alibaba.fastjson.JSONObject;
public class Controller {
public String handle() {
JSONObject f = new JSONObject();
f.put("a", 1);
f.put("b", new JSONObject().fluentPut("x", 2));
org.json.JSONObject o = JsonInterop.toOrg(f);
return o.toString(); // 稳定输出:{"a":1,"b":{"x":2}}
}
}
回归单测
java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class InteropFixTest {
@Test
void stableBridge() {
com.alibaba.fastjson.JSONObject f = new com.alibaba.fastjson.JSONObject();
f.put("a", 1);
f.put("b", new com.alibaba.fastjson.JSONObject().fluentPut("x", 2));
org.json.JSONObject o = JsonInterop.toOrg(f);
assertEquals("{\"a\":1,\"b\":{\"x\":2}}", o.toString());
}
}
十、排坑补充
- Bean 构造器 vs Map 构造器
new JSONObject(Object bean)
走反射,适合普通 POJO;传第三方 Map 实现时可能走到非预期路径。new JSONObject(Map map)
更贴近 JSON 容器语义;但嵌套值若是第三方类型(如 fastjson 的 JSONArray/JSONObject),仍需正确 wrap。
- 空值与 JSONObject.NULL
- org.json 使用
JSONObject.NULL
表示 null,和 Java null 区分;跨库时注意空值转换一致性(字符串桥最省心)。
- org.json 使用
- 性能
- 字符串桥会多一次序列化/反序列化,但换来确定性与可预期性;在关键链路可评估 Map 桥作为折中。
十一、最终 Checklist(上线前自查)
- 只使用一种 JSON 库,或将互转集中在工具类中并采用字符串桥/Map 桥。
- 代码中不出现
Object
作为 JSON 容器参数;不在公共 API 暴露两套JSONObject
简名。 - 打包产物中只包含一个版本的 org.json;无重复依赖;shade 不重定位 org.json/fastjson。
- 启动日志打印
org.json.JSONObject
来源 jar,确保与构建一致。 - 单测覆盖:fastjson ↔ org.json 互转,在 CI 中执行。
- IDE 自动导包配置已加强,对
JSONObject
等同名类给出强提醒。
十二、总结
这次事故的本质不是JSON 很复杂,而是同名类和多版本依赖带来的不确定性。在 Java 生态里,同名类在不同库里随处可见,任何一处不加思考的自动导包、看似通用的 Object 适配,都可能在 classpath 改变时引爆隐患。教训是:
- 设计上避免混用库;需要互转时固定桥梁(字符串或 Map)。
- 工程上锁版本、控依赖、查冲突;打包后核验类来源。
- 规范上避免简名歧义;加强 IDE 与 CI 的静态检查。
修复并不复杂,难的是建立起对构造器分派路径和类加载差异的敬畏心。愿这篇复盘能帮你避开同样的坑,守住你的周末双休。