本文概述
是否曾经尝试创建一个JSON数据结构, 其中包含具有双向关系(即循环引用)的实体?如果有的话, 你可能会在”未捕获的TypeError:将圆形结构转换为JSON”一栏中看到JavaScript错误。或者, 如果你是使用Jackson库的Java开发人员, 则可能会遇到”无法编写JSON:根本原因java.lang.StackOverflowError的无限递归(StackOverflowError)”。
本文提供了一种健壮的工作方法来创建包含双向关系的JSON结构, 而不会导致这些错误。
通常, 针对此问题提供的解决方案需要一些变通方法, 这些变通方法基本上是可以回避的, 但并未真正解决该问题。示例包括使用诸如@JsonManagedReference和@JsonBackReference之类的Jackson批注类型(它们只是从序列化中省略了反向引用), 或者使用@JsonIgnore来简单地忽略关系的某一方面。或者, 可以开发自定义的序列化代码, 而忽略数据中的任何此类双向关系或循环依赖性。
但是我们不想忽略或忽略双向关系的任何一方。我们希望在两个方向上都保留它, 而不会产生任何错误。真正的解决方案应该允许JSON中存在循环依赖关系, 并允许开发人员停止思考它们而无需采取其他措施来解决它们。本文提供了一种实用而直接的方法, 可以为当今的前端开发人员提供任何有用的标准技巧和实践。
一个简单的双向关系示例
出现这种双向关系(也称为循环依赖)的常见情况是, 当有一个具有子对象(它引用了)的父对象, 而那些子对象又想维护对其父对象的引用。这是一个简单的示例:
var obj = {
"name": "I'm parent"
}
obj.children = [
{
"name": "I'm first child", "parent": obj
}, {
"name": "I'm second child", "parent": obj
}
]
如果尝试将上述父对象转换为JSON(例如, 通过使用stringify方法, 如var parentJson = JSON.stringify(parent);), 则会引发异常Uncaught TypeError:将圆形结构转换为JSON。
尽管我们可以使用上面讨论的一种技术(例如使用@JsonIgnore之类的注释), 或者我们可以简单地从子级中删除上述对父级的引用, 但是这些都是避免而不是解决问题的方法。我们真正想要的是一个生成的JSON结构, 该结构可以维护每个双向关系, 并且我们可以转换为JSON而不会引发任何异常。
寻求解决方案
解决方案的一个可能显而易见的步骤是向每个对象添加某种形式的对象ID, 然后用对父对象ID的引用替换子对象对父对象的引用。例如:
var obj = {
"id": 100, "name": "I'm parent"
}
obj.children = [
{
"id": 101, "name": "I'm first child", "parent": 100
}, {
"id": 102, "name": "I'm second child", "parent": 100
}
]
这种方法肯定会避免由于双向关系或循环引用而导致的任何异常。但是仍然存在一个问题, 当我们考虑如何对这些引用进行序列化和反序列化时, 这个问题就变得显而易见。
问题是, 使用上述示例, 我们需要知道, 每次对值” 100″的引用都指向父对象(因为这是其ID)。在上面的示例中, 这是很好的工作, 其中唯一具有值” 100″的属性是父属性。但是, 如果我们添加另一个值为” 100″的属性怎么办?例如:
obj.children = [
{
"id": 101, "name": "I'm first child", "priority": 100, // This is NOT referencing object ID "100"
"parent": 100 // This IS referencing object ID "100"
}, {
"id": 102, "name": "I'm second child", "priority": 200, "parent": 100
}
]
如果我们假设对值” 100″的任何引用都在引用对象, 那么我们的序列化/反序列化代码将无法知道当父级引用值” 100″时, 即引用父级对象的ID, 但是当优先级引用值” 100″, 即不引用父对象的ID(并且由于它将认为优先级也在引用父对象的ID, 因此它将错误地将其值替换为对父对象的引用)。
你可能会在此时问:”等等, 你缺少一个明显的解决方案。除了使用属性值来确定它引用的是对象ID之外, 为什么不使用属性名呢?”确实, 这是一个选择, 但非常有限。这意味着我们将需要预先指定一个”保留”属性名称列表, 这些属性名称始终被假定为引用其他对象(诸如”父”, “子”, “下一个”等名称)。然后, 这将意味着仅那些属性名称可用于对其他对象的引用, 也将意味着那些属性名称将始终被视为对其他对象的引用。因此, 在大多数情况下, 这不是可行的选择。
因此, 看起来我们需要坚持将属性值识别为对象引用。但这意味着我们将需要确保这些值与所有其他属性值唯一。我们可以使用全局唯一标识符(GUID)来满足对唯一值的需求。例如:
var obj = {
"id": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc", "name": "I'm parent"
}
obj.children = [
{
"id": "6616c598-0a0a-8263-7a56-fb0c0e16225a", "name": "I'm first child", "priority": 100, "parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc" // matches unique parent id
}, {
"id": "940e60e4-9497-7c0d-3467-297ff8bb9ef2", "name": "I'm second child", "priority": 200, "parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc" // matches unique parent id
}
]
这样就行了, 对吗?
是。
但…
全自动解决方案
记住我们最初的挑战。我们希望能够序列化和反序列化与JSON有双向关系的对象, 而又不会产生任何异常。尽管上述解决方案可以做到这一点, 但它是通过要求我们(a)向每个对象添加某种形式的唯一ID字段, 以及(b)用相应的唯一ID替换每个对象引用来实现的。这会起作用, 但是我们更希望使用一种解决方案, 该解决方案可以自动使用现有的对象引用, 而无需我们以这种方式”手动”修改对象。
理想情况下, 我们希望能够通过序列化器和反序列化器传递一组对象(包含任何任意一组属性和对象引用)(不基于双向关系生成任何异常), 并使反序列化器生成的对象精确匹配送入序列化程序的对象。
我们的方法是让序列化程序自动创建唯一的ID(并使用GUID)并将其添加到每个对象。然后, 它将任何对象引用替换为该对象的GUID。 (请注意, 序列化程序还需要为这些ID使用一些唯一的属性名称;在我们的示例中, 我们使用@id, 因为假定在属性名称前加上” @”足以确保其唯一性。)然后, 它将用对该对象的引用替换与对象ID对应的任何GUID(请注意, 反序列化器还将从反序列化的对象中删除由序列化器生成的GUID, 从而将它们精确地返回其初始状态)。
因此, 回到我们的示例, 我们希望将以下对象集按原样提供给序列化器:
var obj = {
"name": "I'm parent"
}
obj.children = [
{
"name": "I'm first child", "parent": obj
}, {
"name": "I'm second child", "parent": obj
}
]
然后, 我们期望序列化程序生成类似于以下内容的JSON结构:
{
"@id": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc", "name": "I'm parent", "children": [
{
"@id": "6616c598-0a0a-8263-7a56-fb0c0e16225a", "name": "I'm first child", "parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc"
}, {
"@id": "940e60e4-9497-7c0d-3467-297ff8bb9ef2", "name": "I'm second child", "parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc"
}, ]
}
(你可以使用JSON格式化程序工具来美化任何JSON对象。)
然后将上述JSON提供给解串器将生成原始对象集(即, 父对象及其两个子对象, 可以正确地相互引用)。
因此, 既然我们知道我们想做什么以及我们想如何做, 那就实施吧。
在JavaScript中实现序列化器
以下是序列化器的示例JavaScript实施示例, 它将正确处理双向关系而不会引发任何异常。
var convertToJson = function(obj) {
// Generate a random value structured as a GUID
var guid = function() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
};
// Check if a value is an object
var isObject = function(value) {
return (typeof value === 'object');
}
// Check if an object is an array
var isArray = function(obj) {
return (Object.prototype.toString.call(obj) === '[object Array]');
}
var convertToJsonHelper = function(obj, key, objects) {
// Initialize objects array and
// put root object into if it exist
if(!objects) {
objects = [];
if (isObject(obj) && (! isArray(obj))) {
obj[key] = guid();
objects.push(obj);
}
}
for (var i in obj) {
// Skip methods
if (!obj.hasOwnProperty(i)) {
continue;
}
if (isObject(obj[i])) {
var objIndex = objects.indexOf(obj[i]);
if(objIndex === -1) {
// Object has not been processed; generate key and continue
// (but don't generate key for arrays!)
if(! isArray(obj)) {
obj[i][key] = guid();
objects.push(obj[i]);
}
// Process child properties
// (note well: recursive call)
convertToJsonHelper(obj[i], key, objects);
} else {
// Current object has already been processed;
// replace it with existing reference
obj[i] = objects[objIndex][key];
}
}
}
return obj;
}
// As discussed above, the serializer needs to use some unique property name for
// the IDs it generates. Here we use "@id" since presumably prepending the "@" to
// the property name is adequate to ensure that it is unique. But any unique
// property name can be used, as long as the same one is used by the serializer
// and deserializer.
//
// Also note that we leave off the 3rd parameter in our call to
// convertToJsonHelper since it will be initialized within that function if it
// is not provided.
return convertToJsonHelper(obj, "@id");
}
在JavaScript中实现反序列化器
以下是反序列化器的示例工作JavaScript实现, 该实现将正确处理双向关系而不会引发任何异常。
var convertToObject = function(json) {
// Check if an object is an array
var isObject = function(value) {
return (typeof value === 'object');
}
// Iterate object properties and store all reference keys and references
var getKeys = function(obj, key) {
var keys = [];
for (var i in obj) {
// Skip methods
if (!obj.hasOwnProperty(i)) {
continue;
}
if (isObject(obj[i])) {
keys = keys.concat(getKeys(obj[i], key));
} else if (i === key) {
keys.push( { key: obj[key], obj: obj } );
}
}
return keys;
};
var convertToObjectHelper = function(json, key, keys) {
// Store all reference keys and references to object map
if(!keys) {
keys = getKeys(json, key);
var convertedKeys = {};
for(var i = 0; i < keys.length; i++) {
convertedKeys[keys[i].key] = keys[i].obj;
}
keys = convertedKeys;
}
var obj = json;
// Iterate all object properties and object children
// recursively and replace references with real objects
for (var j in obj) {
// Skip methods
if (!obj.hasOwnProperty(j)) {
continue;
}
if (isObject(obj[j])) {
// Property is an object, so process its children
// (note well: recursive call)
convertToObjectHelper(obj[j], key, keys);
} else if( j === key) {
// Remove reference id
delete obj[j];
} else if (keys[obj[j]]) {
// Replace reference with real object
obj[j] = keys[obj[j]];
}
}
return obj;
};
// As discussed above, the serializer needs to use some unique property name for
// the IDs it generates. Here we use "@id" since presumably prepending the "@" to
// the property name is adequate to ensure that it is unique. But any unique
// property name can be used, as long as the same one is used by the serializer
// and deserializer.
//
// Also note that we leave off the 3rd parameter in our call to
// convertToObjectHelper since it will be initialized within that function if it
// is not provided.
return convertToObjectHelper(json, "@id");
}
通过这两种方法传递一组对象(包括具有双向关系的对象)本质上是一个标识函数。即convertToObject(convertToJson(obj))=== obj的计算结果为true。
Java / Jackson示例
现在, 让我们看看流行的外部库如何支持这种方法。例如, 让我们看看如何使用Jackson库在Java中对其进行处理。
@JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, property="@id")
public class Parent implements Serializable {
private String name;
private List<Child> children = new ArrayList<>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Child> getChildren() {
return children;
}
public void setChildren(List<Child> children) {
this.children = children;
}
}
@JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, property="@id")
public class Child implements Serializable {
private String name;
private Parent parent;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
}
这两个Java类Parent和Child表示与本文开头的JavaScript示例相同的结构。这里的重点是使用@JsonIdentityInfo批注, 该批注将告诉Jackson如何对这些对象进行序列化/反序列化。
让我们来看一个例子:
Parent parent = new Parent();
parent.setName("I'm parent")
Child child1 = new Child();
child1.setName("I'm first child");
Child child2 = new Child();
child2.setName("I'm second child");
parent.setChildren(Arrays.asList(child1, child2));
将父实例序列化为JSON的结果是, 将返回与JavaScript示例相同的JSON结构。
{
"@id": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc", "name": "I'm parent", "children": [
{
"@id": "6616c598-0a0a-8263-7a56-fb0c0e16225a", "name": "I'm first child", "parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc"
}, {
"@id": "940e60e4-9497-7c0d-3467-297ff8bb9ef2", "name": "I'm second child", "parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc"
}, ]
}
另一个优势
所描述的处理JSON中双向关系的方法也可以用来帮助减小JSON文件的大小, 因为它使你能够简单地通过对象的唯一ID来引用对象, 而不需要包含同一对象的冗余副本。
考虑以下示例:
{
"@id": "44f47be7-af77-9a5a-8606-a1e6df299ec9", "id": 1, "name": "I'm parent", "children": [
{
"@id": "54f47be7-af77-9a5a-8606-a1e6df299eu8", "id": 10, "name": "I'm first child", "parent": "44f47be7-af77-9a5a-8606-a1e6df299ec9"
}, {
"@id": "98c47be7-af77-9a5a-8606-a1e6df299c7a", "id": 11, "name": "I'm second child", "parent": "44f47be7-af77-9a5a-8606-a1e6df299ec9"
}, {
"@id": "5jo47be7-af77-9a5a-8606-a1e6df2994g2", "id": 11, "name": "I'm third child", "parent": "44f47be7-af77-9a5a-8606-a1e6df299ec9"
}
], "filteredChildren": [
"54f47be7-af77-9a5a-8606-a1e6df299eu8", "5jo47be7-af77-9a5a-8606-a1e6df2994g2"
]
}
如filteredChildren数组所示, 我们可以在JSON中简单地包含对象引用, 而不是被引用对象及其内容的副本。
包起来
使用此解决方案, 你可以在以最小化对对象和数据约束的方式序列化JSON文件时, 消除与循环引用相关的异常。如果用于处理JSON文件序列化的库中尚无此类解决方案, 则可以根据提供的示例实现来实施自己的解决方案。希望对你有所帮助。
评论前必须登录!
注册