血泪教训,JSONObject的引用导致我周末双休没有了

一线真实事故复盘。标题不是夸张,确实因为一个同名类 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 对 wrapJSONObject(Map)JSONObject(Object) 的实现差异触发无声失败。

四、定位步骤(一步步排查)

  1. 打印运行时使用的 org.json 来自哪个 jar
java 复制代码
System.out.println(org.json.JSONObject.class.getProtectionDomain().getCodeSource().getLocation());

输出的 jar 路径若和你在本地/IDE 里看到的版本不一致,八成是打包时被换了版本或被 shade 了。

  1. 查看依赖树(确认重复/冲突)
  • Maven:
bash 复制代码
mvn -q dependency:tree
  • Gradle:
bash 复制代码
./gradlew -q dependencies

关注 org.json:json 是否出现多版本,或被某个传递性依赖引入。

  1. 写个对照单测,验证三种构造方式行为
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/构造分支,导致无声数据丢失。

六、修复方案

优先级从高到低:

  1. 统一库,全链路只用一种 JSON 库
  • 如果可能,选一个(fastjson2/jackson/org.json 等),避免跨库互转。
  1. 需要互转时,使用字符串桥(最稳健)
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());
    }
}
  1. 或使用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));
}
  1. 如必须传 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));
}
  1. 显式限定类型,避免误导包
  • 禁止在公共 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 区分;跨库时注意空值转换一致性(字符串桥最省心)。
  • 性能
    • 字符串桥会多一次序列化/反序列化,但换来确定性与可预期性;在关键链路可评估 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 的静态检查。

修复并不复杂,难的是建立起对构造器分派路径和类加载差异的敬畏心。愿这篇复盘能帮你避开同样的坑,守住你的周末双休。

相关推荐
孟永峰_Java几秒前
MySQL 组合IN查询:你的索引为什么罢工了?
后端
ruokkk2 分钟前
一个困扰我多年的Session超时Bug,被我的新AI搭档半天搞定了
javascript·后端·架构
楽码3 分钟前
端到端应用Hmac加密
服务器·后端·算法
孟永峰_Java3 分钟前
Java程序员的周五:代码没写完,但我的心已经放假了!
后端
uhakadotcom4 分钟前
Flink有python的SDK入门教程
后端·面试·github
uhakadotcom11 分钟前
开源一个AI导航站工具-jobleap4u
前端·面试·github
今禾15 分钟前
JavaScript 响应式系统深度解析:从 `Object.defineProperty` 到 `Proxy` 的演进与优化
前端·javascript·面试
kong@react25 分钟前
spring boot配置es
spring boot·后端·elasticsearch
胡gh33 分钟前
面试官问你如何实现居中?别慌,这里可有的是东西bb
前端·css·面试
言兴35 分钟前
面试题之React组件通信:从基础到高级实践
前端·javascript·面试