Java 中 null 值在 JSON 输出时丢失的坑:一次 Object 参数 + Fastjson 多态的血泪教训

大家好,我是G探险者!

在实际开发中,我们经常会遇到 Java 对象需要序列化成 JSON 字符串的场景。然而,如果使用不当,很可能会出现 null 字段"神秘消失" 的情况。本文将通过一个真实案例,完整分析问题成因及解决方案。

一、问题背景

在项目中,我们有一个统一响应类 InvokeResult,构造方法如下:

java 复制代码
public InvokeResult(StatusCode statusCode, Object result) {
    this.code = statusCode.getCode();
    this.message = statusCode.getReasonPhrase();
    this.result = result;
}

按理说,result 是个 Object,谁传什么就按原样存。 但在一次调试中,我发现 result 内部很多字段值为 null,Debug 里都能看到这些字段,可一旦调用:

java 复制代码
result.getResult().toString();

这些 null 字段就凭空消失了。


二、现象描述

Debug 截图(简化版):

json 复制代码
{
    "monitor_level": "01",
    "input_user_id": null,
    "check_user_id": null,
    "mod_datetime": null,
    "post_code": null
}
  • 在调试器中 :这些 null 字段清清楚楚地存在。
  • 在 toString() 输出后:只剩下非 null 的字段,所有 null 都消失。

三、定位的"卡点"

这个问题根因并不复杂,但我定位过程却耗费了不少时间,原因在于:

  • InvokeResult 的构造方法参数类型是 Object,所以第一反应不会想到它是个 JSON 对象。
  • 实际调用过程中,result 传入的是 com.alibaba.fastjson.JSONObject
  • 多态的"隐身攻击"Object.toString() 在运行时,其实会执行 JSONObject 重写的 toString() 方法,而不是 java.lang.Object 原始实现。

这就导致了我一开始一直以为只是简单的字符串化操作,没想到背后走的是 Fastjson 的序列化逻辑。


四、真相揭秘

  1. Fastjson 的 JSONObject 重写了 toString()

    java 复制代码
    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }
  2. Fastjson 默认过滤 null

    java 复制代码
    JSON.toJSONString(obj); // 默认不输出 null
  3. 所以当你调用:

    java 复制代码
    result.getResult().toString();

    实际等价于:

    java 复制代码
    JSON.toJSONString(result.getResult());

    而不是普通的 Object.toString(),null 自然就被"优化"掉了。


五、解决方案

方案 1:显式保留 null

java 复制代码
return JSON.toJSONString(result.getResult(), SerializerFeature.WriteMapNullValue);

方案 2:避免直接用 JSONObject 做业务数据

java 复制代码
Map<String, Object> map = new HashMap<>();
map.put("input_user_id", null);
...
new InvokeResult(StatusCode.SUCCESS, map);

方案 3:全局配置 Fastjson 保留 null

java 复制代码
JSON.DEFAULT_GENERATE_FEATURE |= SerializerFeature.WriteMapNullValue.getMask();

六、经验教训

  1. 多态坑点 :方法签名是 Object,但实际类型可能是任何实现类,要随时留意运行时类型。

  2. toString 不一定安全 :特别是第三方库的对象,toString() 很可能被重写成带业务逻辑的方法。

  3. 调试时多用 getClass()

    java 复制代码
    System.out.println(result.getResult().getClass());

    一眼就能看出是不是自己想象的类型。


七、总结

这个问题的根因,其实一句话就能说清: JSONObject.toString() 默认会丢掉 null 字段,因为它走的是 Fastjson 的 JSON 序列化逻辑。

但它的难点在于:

  • 表面上你在用 Object.toString(),实则调用了子类的重写方法。
  • 参数是 Object,传进来的到底是什么类型,如果不特意去查,很容易忽略。
  • 调试时看到的对象和最终输出的字符串差异,容易让人把注意力放在 JSON 配置上,而不是类型本身。
相关推荐
brzhang21 小时前
A2UI:但 Google 把它写成协议后,模型和交互的最后一公里被彻底补全
前端·后端·架构
开心猴爷21 小时前
iOS App 性能测试中常被忽略的运行期问题
后端
SHERlocked9321 小时前
摄像头 RTSP 流视频多路实时监控解决方案实践
c++·后端·音视频开发
AutoMQ1 天前
How does AutoMQ implement a sub-10ms latency Diskless Kafka?
后端·架构
Rover.x1 天前
Netty基于SpringBoot实现WebSocket
spring boot·后端·websocket
疯狂的程序猴1 天前
用 HBuilder 上架 iOS 应用时如何管理Bundle ID、证书与描述文件
后端
ShaneD7711 天前
Redis 实战:从零手写分布式锁(误删问题与 Lua 脚本优化)
后端
我命由我123451 天前
Python Flask 开发问题:ImportError: cannot import name ‘Markup‘ from ‘flask‘
开发语言·后端·python·学习·flask·学习方法·python3.11
無量1 天前
Java并发编程基础:从线程到锁
后端
小信啊啊1 天前
Go语言数组与切片的区别
开发语言·后端·golang