一、背景
在构建一个公共的批处理方法类的时候,在测试输出的时候,打印了" r e f " : " ref":" ref":"[0][0]"的内容,这让我比较疑惑。不由得继续了下去…
二、问题分析
首先,我们需要明确 ,在使用诸如Java的序列化库(如Jackson、Gson或Fastjson等)将数据转换为JSON字符串时,JSON.toJSONString(map<String,String>) 调用中可能出现 “ r e f " : " ref":" ref":"[0][0]” 的原因。在JSON序列化过程中,“ r e f " : " ref":" ref":"[0][0]” 这类引用标记通常表示对象中存在循环引用,即一个对象直接或间接地引用了自己。JSON序列化库在检测到这种循环引用时,会尝试使用引用来避免无限递归,并节省内存。
对于简单的 Map<String, String> 类型,通常不应该出现循环引用,因为键值对本身不包含对其他键值对的引用。因此,这个问题可能是在其他部分的代码或序列化库的实现中产生的。
2.1 问题示例
假设我们有一个简单场景,其中Map中的值是一个自引用的类实例,这会导致序列化时出现$ref。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
class SelfReferencingObject {int id;SelfReferencingObject selfRef;public SelfReferencingObject(int id) {this.id = id;}public void setSelfRef(SelfReferencingObject ref) {this.selfRef = ref;}
}public class Demo {public static void main(String[] args) throws Exception {ObjectMapper mapper = new ObjectMapper();SelfReferencingObject obj1 = new SelfReferencingObject(1);SelfReferencingObject obj2 = new SelfReferencingObject(2);// 创建循环引用obj1.setSelfRef(obj1);obj2.setSelfRef(obj2);Map<String, SelfReferencingObject> map = new HashMap<>();map.put("obj1", obj1);map.put("obj2", obj2);String json = mapper.writeValueAsString(map);System.out.println(json);}
}
上述代码在运行时,将会输出类似于以下内容,其中包含了$ref来表示循环引用:
{
“obj1”: {
“id”: 1,
“@ref”: “KaTeX parse error: Expected 'EOF', got '}' at position 8: [0]" }̲, "obj2": { …[1]”
}
}
三、原因解析
3.1 循环引用
如果Map中的值直接或间接地引用了Map本身或其他位于Map中的对象,形成了一个闭环,序列化时为了防止无限循环和堆栈溢出,序列化库会使用$ref来标记已处理过的对象,避免重复输出。
3.2 重复引用
即使没有循环引用,但如果多个键值对引用了相同的对象实例,一些序列化库也会使用$ref来优化输出,表示这些位置引用的是同一个对象。
四、解决方案
4.1. 禁用循环检测(不推荐,仅作演示)
大多数序列化库提供了配置选项来禁用循环引用检测,但这可能会导致其他问题,如无限循环序列化。
ObjectMapper mapper = new ObjectMapper();
mapper.disable(com.fasterxml.jackson.databind.deser.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.enable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DURABLE_OBJECT_IDS);
String json = mapper.writeValueAsString(map);
...
注意:此方法可能不会直接解决问题,且可能导致无限循环或其它错误,实际应用中应谨慎。
4.2. 自定义序列化策略
你可以通过实现自定义的序列化器或采用库提供的注解等方式,控制特定对象或类的序列化行为,避免$ref的产生。
// 使用Jackson的@JsonIdentityInfo注解解决循环引用
// 上面的SelfReferencingObject类已经添加了@JsonIdentityInfo注解// 序列化代码保持不变
使用@JsonIdentityInfo后,输出的JSON会为重复的对象生成唯一ID,而不是直接使用$ref。
4.3. 手动处理引用
在序列化前,检查并打破潜在的循环引用,比如将引用替换为ID或者浅拷贝对象以切断循环链。
public class Demo {public static void main(String[] args) throws Exception {ObjectMapper mapper = new ObjectMapper();SelfReferencingObject obj1 = new SelfReferencingObject(1);SelfReferencingObject obj2 = new SelfReferencingObject(2);// 防止循环引用,这里不设置selfRefMap<String, SelfReferencingObject> map = new HashMap<>();map.put("obj1", obj1);map.put("obj2", obj2);String json = mapper.writeValueAsString(map);System.out.println(json);}
}
这个例子中,我们直接不设置selfRef,从而避免了循环引用。
4.4. 使用特定库的功能
某些库如Jackson提供了@JsonIdentityInfo注解来处理循环引用问题,自动为重复的对象生成ID引用。
已在示例1中展示了如何使用Jackson的@JsonIdentityInfo注解处理循环引用。
或者使用JSON.toJSONString(Object object, SerializerFeature… features)方法,并传入SerializerFeature.DisableCircularReferenceDetect特性来禁用循环引用检测。
String jsonString = JSON.toJSONString(users, SerializerFeature.DisableCircularReferenceDetect);
五、补充知识点
在Java中,对象之间的循环引用、重复引用、自引用或相互引用,通常在代码层面直观体现为对象的属性互相指向对方。下面通过示例来具体展示这些引用方式:
5.1. 循环引用
当两个或多个对象互相持有对方的引用,形成一个闭环,这就是循环引用。
class Person {String name;Person friend;Person(String name) {this.name = name;}void setFriend(Person friend) {this.friend = friend;// 这里设置朋友的friend为自己,形成循环引用friend.setFriend(this);}
}public class Main {public static void main(String[] args) {Person alice = new Person("Alice");Person bob = new Person("Bob");alice.setFriend(bob); // 设置Alice的朋友是Bob}
}
在这个例子中,alice的朋友是bob,而bob的朋友又被设置为alice,形成了循环引用。
5.2. 重复引用
如果多个变量或数据结构引用同一个对象实例,就是重复引用。
class Book {String title;Book(String title) {this.title = title;}
}public class Main {public static void main(String[] args) {Book popularBook = new Book("Popular Title");List<Book> library = new ArrayList<>();library.add(popularBook);library.add(popularBook); // 同一个Book实例被添加两次,形成重复引用}
}
这里,popularBook这个Book实例被library列表重复引用了两次。
5.3. 自引用
自引用指的是对象的一个属性直接或间接地引用自身。
class Node {String data;Node next; // 可能指向自己,形成自引用Node(String data) {this.data = data;}void setNext(Node next) {this.next = next;}
}public class Main {public static void main(String[] args) {Node node = new Node("Node Data");// 形成自引用node.setNext(node);}
}
在Node类的例子中,next属性可以设置为指向自己,形成自引用。
总结
在解决这个问题时,关键是要找到循环引用的来源。这可能需要你深入检查代码和序列化库的实现。一旦找到循环引用的来源,你就可以采取适当的措施来避免它,例如修改代码逻辑或自定义序列化过程。如果问题是由序列化库引起的,更新到最新版本或寻找替代库可能是一个解决方案。