FastJson序列化导致的 “$ref“

文章讲述了在后端开发中遇到的FastJson序列化时处理循环引用的问题,通过示例展示了当对象在JSON中重复出现时,FastJson会使用$ref表示引用。解决方法是避免直接赋值,而是通过复制对象属性来处理。即使不修改,反序列化也能得到正确对象,但作者仍建议了解这一特性以防止未来问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:

刚转后端没多久,在通过RPC调用其他组的服务的时候,其他组对接的同学说,你的入参是有问题的,然后他把入参发我,类似于下面的 json

{
	"addressMO": {
		"roomNumber": "1002",
		"street": "上海市徐汇区田林街道"
	},
	"age": 10,
	"brotherMO": {
		"addressMO": {
			"$ref": "$.addressMO"
		},
		"className": "上海中学",
		"name": "我是哥哥"
	},
	"name": "我是弟弟"
}

这个对象包含一个 addressMObrotherMO以及 nameagebrotherMO 中也包含了addressMO,就会导致 出现上面的 "$ref": "$.addressMO",这个其实表示的是引用根目录中的 addressMO 对象。

这个例子其实是我简化后的 demo,为了简单说明。其他的 对象如下:

public static class PersonMO {
        private String name;
        private int age;
        private AddressMO addressMO;
        private BrotherMO brotherMO;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public AddressMO getAddressMO() {
            return addressMO;
        }

        public void setAddressMO(AddressMO addressMO) {
            this.addressMO = addressMO;
        }

        public BrotherMO getBrotherMO() {
            return brotherMO;
        }

        public void setBrotherMO(BrotherMO brotherMO) {
            this.brotherMO = brotherMO;
        }
    }

    public static class AddressMO {
        private String street;
        private String roomNumber;

        public String getStreet() {
            return street;
        }

        public void setStreet(String street) {
            this.street = street;
        }

        public String getRoomNumber() {
            return roomNumber;
        }

        public void setRoomNumber(String roomNumber) {
            this.roomNumber = roomNumber;
        }
    }

    public static class BrotherMO {
        private String name;
        private String className;
        private AddressMO addressMO;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getClassName() {
            return className;
        }

        public void setClassName(String className) {
            this.className = className;
        }

        public AddressMO getAddressMO() {
            return addressMO;
        }

        public void setAddressMO(AddressMO addressMO) {
            this.addressMO = addressMO;
        }
    }

测试代码如下:

 @Test
    public void testJSON() {
        AddressMO addressMO = new AddressMO();
        addressMO.setStreet("上海市徐汇区田林街道");
        addressMO.setRoomNumber("1002");


        BrotherMO brotherMO = new BrotherMO();
        brotherMO.setName("我是哥哥");
        brotherMO.setAddressMO(addressMO);
        brotherMO.setClassName("上海中学");

        PersonMO personMO = new PersonMO();
        personMO.setName("我是弟弟");
        personMO.setAge(10);
        personMO.setAddressMO(addressMO);
        personMO.setBrotherMO(brotherMO);

        String ss = JSON.toJSONString(personMO);
        System.out.println("ss ===== " +ss);
    }

可以看到,对于一个对象,当其首次出现时,FastJson 的序列化是正常工作的,但是当其重复出现时,就会变为对象的引用。这是FastJson内部存在一些特殊逻辑,问题产生的原因在于FastJson存在循环/重复引用检测特性,并且该特性是缺省开启的。
实体转化为json字符串后出现了$ref字样的东西,这是因为在传输的数据中出现相同的对象时,fastjson 默认开启引用检测将相同的对象写成引用的形式,引用是通过"$ref"来表示的。

在这里插入图片描述
修改也很简单,就是在 BrotherMO 中设置 AddressMO 对象是,通过 Copy AddressMO 对象的属性就行了,如下:
在这里插入图片描述
就能正常输出了,其中的 DozerBeanMapper 是开源库,用来复制对象属性的。

{
	"addressMO": {
		"roomNumber": "1002",
		"street": "上海市徐汇区田林街道"
	},
	"age": 10,
	"brotherMO": {
		"addressMO": {
			"roomNumber": "1002",
			"street": "上海市徐汇区田林街道"
		},
		"className": "上海中学",
		"name": "我是哥哥"
	},
	"name": "我是弟弟"
}

其实不改也是没问题的,通过反序列化出来的 PersonMO 对象都是正常的。
在这里插入图片描述
刚开始,其他同事跟我说的时候我是很懵逼的,完全不知道,因为我调用他的服务之前是加了日志的,都是正常的,但是同事说的是这个服务上线2年多了,肯定不是他们的问题,哈哈,然后 google 了下,记录下,怕自己会忘记。

<think>首先,用户的问题是关于FastJSON中出现"$ref"问题的原因及解决方案。根据系统指令,我需要使用中文回答,确保回答结构清晰,并引用提供的参考内容。 关键点从引用中提取: - 引用[^1]:提到"$ref"是由于FastJSON在重复引用或循环引用时产生的,目的是为了性能优化。 - 引用[^2]:详细解释了"$ref"是FastJSON默认开启引用检测的结果,会导致相同的对象被写成引用形式。给出了循环引用和重复引用的区别,并提供了示例。 - 引用[^3]:提供了解决方案,包括通过配置FastJsonConfig来禁用某些特性,如设置SerializerFeature。 - 引用[^4]:展示了如何通过禁用循环引用检测来解决这个问题。 现在,构建回答结构: 1. **解释问题原因**:基于引用,说明"$ref"是由于FastJSON的引用检测特性导致的,当有重复引用或循环引用时,会生成"$ref"标记。 2. **提供解决方案**: - 禁用循环引用检测:使用`SerializerFeature.DisableCircularReferenceDetect`。 - 全局配置:在Spring项目中配置FastJsonHttpMessageConverter。 - 其他建议:避免在数据结构中出现重复对象。 3. **添加相关问题**:根据指令,生成相关问题。 确保回答格式: - 行内数学表达式用$...$(但这里可能不涉及数学)。 - 独立公式用$$...$$(同样,可能不适用)。 - 代码块用```包裹。 - 引用末尾添加引用标识,如[^1]。 最终回答大纲: - 开头简述问题。 - 原因部分。 - 解决方案部分,包括代码示例。 - 相关问题。</think>### FastJSON 出现 "$ref" 问题的原因及解决方案 在使用 FastJSON 序列化 Java 对象为 JSON 字符串时,如果遇到输出中包含 `$ref` 标记(例如 `"$ref":"$[0].orderList[0]"`),这通常是由 FastJSON 的引用检测机制引起的。以下是详细的原因分析和解决方案,基于实际开发中的常见场景。 #### 一、问题原因 FastJSON 默认启用了引用检测特性,旨在优化性能(减少重复对象的序列化开销)[^1]。当 JSON 数据结构中出现重复引用或循环引用时,FastJSON 会自动生成 `$ref` 标记来替代重复对象,以避免无限递归或数据冗余[^2]。 - **重复引用**:同一个对象实例在多个地方被引用(例如,一个 `HashMap` 对象被多个列表引用)。FastJSON 会为后续引用生成 `$ref` 路径,而不是重新序列化整个对象[^2]。 - **循环引用**:对象 A 引用对象 B,而对象 B 又引用对象 A(可能导致堆栈溢出错误)。但 `$ref` 问题更常见于重复引用场景[^2]。 例如,在以下代码中,`list2` 包含两个相同的 `map1` 对象,序列化后会输出 `$ref`: ```java import com.alibaba.fastjson.JSON; import java.util.ArrayList; import java.util.HashMap; import java.util.List; public class Main { public static void main(String[] args) { HashMap<String, String> map1 = new HashMap<>(); map1.put("key", "value"); List<HashMap<String, String>> list2 = new ArrayList<>(); list2.add(map1); list2.add(map1); // 重复引用同一个对象 System.out.println(JSON.toJSONString(list2)); // 输出: [{"key":"value"},{"$ref":"$[0]"}] } } ``` 这会导致前端解析 JSON 时出错,因为 `$ref` 不是标准 JSON 格式[^1][^2]。 #### 二、解决方案 解决 `$ref` 问题的核心是禁用 FastJSON 的引用检测特性。以下是两种常用方法: ##### 1. **局部禁用(推荐)** 在序列化时显式添加 `SerializerFeature.DisableCircularReferenceDetect` 参数。这仅影响当前序列化操作,避免全局配置的潜在风险[^4]。 ```java import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; public class Solution { public static void main(String[] args) { // 示例对象(同上) HashMap<String, String> map1 = new HashMap<>(); map1.put("key", "value"); List<HashMap<String, String>> list2 = new ArrayList<>(); list2.add(map1); list2.add(map1); // 禁用引用检测 String json = JSON.toJSONString(list2, SerializerFeature.DisableCircularReferenceDetect); System.out.println(json); // 输出: [{"key":"value"},{"key":"value"}],无$ref } } ``` - **优点**:简单直接,适用于特定场景。 - **缺点**:需在每个序列化调用处添加参数[^4]。 ##### 2. **全局禁用(谨慎使用)** 在 Spring Boot 项目中,通过配置 `FastJsonConfig` 全局禁用引用检测。但需注意,这可能引发循环引用问题(如堆栈溢出)[^3]。 ```java import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import java.util.List; @Configuration public class FastJsonConfig { @Bean public HttpMessageConverter<?> fastJsonConverter() { FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); FastJsonConfig config = new FastJsonConfig(); // 禁用引用检测 config.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect); converter.setFastJsonConfig(config); return converter; } } ``` - **优点**:一劳永逸,适用于整个项目。 - **缺点**:可能掩盖循环引用错误,导致程序崩溃;建议仅在确认无循环引用时使用[^3]。 ##### 3. **其他优化建议** - **避免重复对象**:重构代码,确保集合中不包含相同实例(例如,使用 `new` 创建新对象)。 - **使用 `@JSONField(serialize = false)`**:对特定字段禁用序列化,减少引用机会。 - **升级 FastJSON**:新版 FastJSON 可能优化了引用检测逻辑。 #### 三、注意事项 - **性能权衡**:禁用引用检测会增加序列化开销(尤其是大对象),但能保证 JSON 兼容性[^1][^4]。 - **测试覆盖**:修改后需测试循环引用场景,避免 `StackOverflowError`[^2]。 - **前端兼容**:确保前端库(如 JavaScript 的 `JSON.parse()`)能处理无 `$ref` 的标准 JSON[^1]。 通过以上方法,您可以有效解决 FastJSON 中的 `$ref` 问题。如果涉及循环引用,建议重构数据结构以避免相互依赖[^2]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值