序列化具有与该对象相同类型的 class 成员的对象时如何解决循环引用
How to solve circular reference when serializing an object which have a class member with the same type of that object
我在使用 Gson 序列化具有相同类型的 class 成员的对象时遇到了这个问题:
https://github.com/google/gson/issues/1447
对象:
public class StructId implements Serializable {
private static final long serialVersionUID = 1L;
public String Name;
public StructType Type;
public StructId ParentId;
public StructId ChildId;
并且由于 StructId 包含相同类型的 ParentId/ChildId 我在尝试序列化它时遇到了无限循环,所以我所做的是:
private Gson gson = new GsonBuilder()
.setExclusionStrategies(new ExclusionStrategy() {
public boolean shouldSkipClass(Class<?> clazz) {
return false; //(clazz == StructId.class);
}
/**
* Custom field exclusion goes here
*/
public boolean shouldSkipField(FieldAttributes f) {
//Ignore inner StructIds to solve circular serialization
return ( f.getName().equals("ParentId") || f.getName().equals("ChildId") );
}
})
/**
* Use serializeNulls method if you want To serialize null values
* By default, Gson does not serialize null values
*/
.serializeNulls()
.create();
但这还不够好,因为我需要 Parent/Child 中的数据并在序列化时忽略它们不是解决方案。
怎么可能解决?
与标记为解决方案的答案相关:
我有这样一个结构:
- 结构 1
-- Table
--- 变量 1
序列化前的对象为:
而生成的Json是:
可以看到,Table的ParentId是"Struct1",但是"Struct1"的ChildId是空的,应该是"Table"
B.R.
我认为使用 ExclusionStrategy
不是解决这个问题的正确方法。
我宁愿建议使用 JsonSerializer
and JsonDeserializer
为您的 StructId
class.
定制
(可能使用 TypeAdapter
的方法会更好,
但我没有足够的 Gson 经验来完成这项工作。)
因此您将通过以下方式创建 Gson
实例:
Gson gson = new GsonBuilder()
.registerTypeAdapter(StructId.class, new StructIdSerializer())
.registerTypeAdapter(StructId.class, new StructIdDeserializer())
.setPrettyPrinting()
.create();
下面的StructIdSerializer
class负责把一个StructId
转换成JSON。
它将其属性 Name
、Type
和 ChildId
转换为 JSON。
请注意,它不会将 属性 ParentId
转换为 JSON,
因为这样做会产生无限递归。
public class StructIdSerializer implements JsonSerializer<StructId> {
@Override
public JsonElement serialize(StructId src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("Name", src.Name);
jsonObject.add("Type", context.serialize(src.Type));
jsonObject.add("ChildId", context.serialize(src.ChildId)); // recursion!
return jsonObject;
}
}
下面的StructIdDeserializer
class负责将JSON转换成StructId
。
它转换 JSON 属性 Name
、Type
和 ChildId
StructId
中相应的 Java 字段。
请注意,ParentId
Java 字段是从 JSON 嵌套结构重建的,
因为它没有直接包含为 JSON 属性.
public class StructIdDeserializer implements JsonDeserializer<StructId> {
@Override
public StructId deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
StructId id = new StructId();
id.Name = json.getAsJsonObject().get("Name").getAsString();
id.Type = context.deserialize(json.getAsJsonObject().get("Type"), StructType.class);
JsonElement childJson = json.getAsJsonObject().get("ChildId");
if (childJson != null) {
id.ChildId = context.deserialize(childJson, StructId.class); // recursion!
id.ChildId.ParentId = id;
}
return id;
}
}
我用这个 JSON 输入示例测试了上面的代码
{
"Name": "John",
"Type": "A",
"ChildId": {
"Name": "Jane",
"Type": "B",
"ChildId": {
"Name": "Joe",
"Type": "A"
}
}
}
通过
反序列化
StructId root = gson.fromJson(new FileReader("example.json"), StructId.class);
,
然后用
序列化它
System.out.println(gson.toJson(root));
并再次获得原始 JSON。
只是为了展示一种使用 TypeAdapter
和 ExclusionStrategy
进行序列化的方法(所以我不处理 de-serialization)。这可能不是最漂亮的实现,但它非常通用。
这个解决方案利用了这样一个事实,即您的结构是某种 bi-directional 链表,给定该列表中的任何节点,我们只需要将 parents 和 [= 的序列化分开35=] 以便仅在一个方向上序列化以避免循环引用。
首先我们需要可配置的 ExclusionStrategy
如:
public class FieldExclusionStrategy implements ExclusionStrategy {
private final List<String> skipFields;
public FieldExclusionStrategy(String... fieldNames) {
skipFields = Arrays.asList(fieldNames);
}
@Override
public boolean shouldSkipField(FieldAttributes f) {
return skipFields.contains(f.getName());
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
}
那么 TypeAdapter
就像:
public class LinkedListAdapter extends TypeAdapter<StructId> {
private static final String PARENT_ID = "ParentId";
private static final String CHILD_ID = "ChildId";
private Gson gson;
@Override
public void write(JsonWriter out, StructId value) throws IOException {
// First serialize everything but StructIds
// You could also use type based exclusion strategy
// but for brevity I use just this one
gson = new GsonBuilder()
.addSerializationExclusionStrategy(
new FieldExclusionStrategy(CHILD_ID, PARENT_ID))
.create();
JsonObject structObject = gson.toJsonTree(value).getAsJsonObject();
JsonObject structParentObject;
JsonObject structChildObject;
// If exists go through the ParentId side in one direction.
if(null!=value.ParentId) {
gson = new GsonBuilder()
.addSerializationExclusionStrategy(new FieldExclusionStrategy(CHILD_ID))
.create();
structObject.add(PARENT_ID, gson.toJsonTree(value.ParentId));
if(null!=value.ParentId.ChildId) {
gson = new GsonBuilder()
.addSerializationExclusionStrategy(new FieldExclusionStrategy(PARENT_ID))
.create();
structParentObject = structObject.get(PARENT_ID).getAsJsonObject();
structParentObject.add(CHILD_ID, gson.toJsonTree(value.ParentId.ChildId).getAsJsonObject());
}
}
// And also if exists go through the ChildId side in one direction.
if(null!=value.ChildId) {
gson = new GsonBuilder()
.addSerializationExclusionStrategy(new FieldExclusionStrategy(PARENT_ID))
.create();
structObject.add(CHILD_ID, gson.toJsonTree(value.ChildId));
if(null!=value.ChildId.ParentId) {
gson = new GsonBuilder()
.addSerializationExclusionStrategy(new FieldExclusionStrategy(CHILD_ID))
.create();
structChildObject = structObject.get(CHILD_ID).getAsJsonObject();
structChildObject.add(PARENT_ID, gson.toJsonTree(value.ChildId.ParentId).getAsJsonObject());
}
}
// Finally write the combined result out. No need to initialize gson anymore
// since just writing JsonElement
gson.toJson(structObject, out);
}
@Override
public StructId read(JsonReader in) throws IOException {
return null;
}}
正在测试:
@Slf4j
public class TestIt extends BaseGsonTest {
@Test
public void test1() {
StructId grandParent = new StructId();
StructId parent = new StructId();
grandParent.ChildId = parent;
parent.ParentId = grandParent;
StructId child = new StructId();
parent.ChildId = child;
child.ParentId = parent;
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(StructId.class, new LinkedListAdapter())
.create();
log.info("\n{}", gson.toJson(parent));
}}
会给你类似的东西:
{
"Name": "name1237598030",
"Type": {
"name": "name688766789"
},
"ParentId": {
"Name": "name1169146729",
"Type": {
"name": "name2040352617"
}
},
"ChildId": {
"Name": "name302155142",
"Type": {
"name": "name24606376"
}
}
}
我测试中的名称 material 只是默认初始化为 "name"+hashCode()
抱歉误导大家,基于此 post :
Is there a solution about Gson "circular reference"?
"there is no automated solution for circular references in Gson. The only JSON-producing library I know of that handles circular references automatically is XStream (with Jettison backend)."
但那是你不使用 Jackson 的情况!如果您已经在使用 Jackson 来构建您的 REST API 控制器,那么为什么不使用它来进行序列化呢。不需要外部组件,例如:Gson 或 XStream.
杰克逊的解决方案:
序列化:
ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
try {
jsonDesttinationIdString = ow.writeValueAsString(destinationId);
} catch (JsonProcessingException ex) {
throw new SpecificationException(ex.getMessage());
}
反序列化:
ObjectMapper mapper = new ObjectMapper();
try {
destinationStructId = destinationId.isEmpty() ? null : mapper.readValue(URLDecoder.decode(destinationId, ENCODING), StructId.class);
} catch (IOException e) {
throw new SpecificationException(e.getMessage());
}
最重要的是,您必须使用 @JsonIdentityInfo 注释:
//@JsonIdentityInfo(
// generator = ObjectIdGenerators.PropertyGenerator.class,
// property = "Name")
@JsonIdentityInfo(
generator = ObjectIdGenerators.UUIDGenerator.class,
property = "id")
public class StructId implements Serializable {
private static final long serialVersionUID = 1L;
@JsonProperty("id") // I added this field to have a unique identfier
private UUID id = UUID.randomUUID();
我在使用 Gson 序列化具有相同类型的 class 成员的对象时遇到了这个问题:
https://github.com/google/gson/issues/1447
对象:
public class StructId implements Serializable {
private static final long serialVersionUID = 1L;
public String Name;
public StructType Type;
public StructId ParentId;
public StructId ChildId;
并且由于 StructId 包含相同类型的 ParentId/ChildId 我在尝试序列化它时遇到了无限循环,所以我所做的是:
private Gson gson = new GsonBuilder()
.setExclusionStrategies(new ExclusionStrategy() {
public boolean shouldSkipClass(Class<?> clazz) {
return false; //(clazz == StructId.class);
}
/**
* Custom field exclusion goes here
*/
public boolean shouldSkipField(FieldAttributes f) {
//Ignore inner StructIds to solve circular serialization
return ( f.getName().equals("ParentId") || f.getName().equals("ChildId") );
}
})
/**
* Use serializeNulls method if you want To serialize null values
* By default, Gson does not serialize null values
*/
.serializeNulls()
.create();
但这还不够好,因为我需要 Parent/Child 中的数据并在序列化时忽略它们不是解决方案。 怎么可能解决?
与标记为解决方案的答案相关:
我有这样一个结构: - 结构 1 -- Table --- 变量 1
序列化前的对象为:
而生成的Json是:
可以看到,Table的ParentId是"Struct1",但是"Struct1"的ChildId是空的,应该是"Table"
B.R.
我认为使用 ExclusionStrategy
不是解决这个问题的正确方法。
我宁愿建议使用 JsonSerializer
and JsonDeserializer
为您的 StructId
class.
定制
(可能使用 TypeAdapter
的方法会更好,
但我没有足够的 Gson 经验来完成这项工作。)
因此您将通过以下方式创建 Gson
实例:
Gson gson = new GsonBuilder()
.registerTypeAdapter(StructId.class, new StructIdSerializer())
.registerTypeAdapter(StructId.class, new StructIdDeserializer())
.setPrettyPrinting()
.create();
下面的StructIdSerializer
class负责把一个StructId
转换成JSON。
它将其属性 Name
、Type
和 ChildId
转换为 JSON。
请注意,它不会将 属性 ParentId
转换为 JSON,
因为这样做会产生无限递归。
public class StructIdSerializer implements JsonSerializer<StructId> {
@Override
public JsonElement serialize(StructId src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("Name", src.Name);
jsonObject.add("Type", context.serialize(src.Type));
jsonObject.add("ChildId", context.serialize(src.ChildId)); // recursion!
return jsonObject;
}
}
下面的StructIdDeserializer
class负责将JSON转换成StructId
。
它转换 JSON 属性 Name
、Type
和 ChildId
StructId
中相应的 Java 字段。
请注意,ParentId
Java 字段是从 JSON 嵌套结构重建的,
因为它没有直接包含为 JSON 属性.
public class StructIdDeserializer implements JsonDeserializer<StructId> {
@Override
public StructId deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
StructId id = new StructId();
id.Name = json.getAsJsonObject().get("Name").getAsString();
id.Type = context.deserialize(json.getAsJsonObject().get("Type"), StructType.class);
JsonElement childJson = json.getAsJsonObject().get("ChildId");
if (childJson != null) {
id.ChildId = context.deserialize(childJson, StructId.class); // recursion!
id.ChildId.ParentId = id;
}
return id;
}
}
我用这个 JSON 输入示例测试了上面的代码
{
"Name": "John",
"Type": "A",
"ChildId": {
"Name": "Jane",
"Type": "B",
"ChildId": {
"Name": "Joe",
"Type": "A"
}
}
}
通过
反序列化
StructId root = gson.fromJson(new FileReader("example.json"), StructId.class);
,
然后用
序列化它
System.out.println(gson.toJson(root));
并再次获得原始 JSON。
只是为了展示一种使用 TypeAdapter
和 ExclusionStrategy
进行序列化的方法(所以我不处理 de-serialization)。这可能不是最漂亮的实现,但它非常通用。
这个解决方案利用了这样一个事实,即您的结构是某种 bi-directional 链表,给定该列表中的任何节点,我们只需要将 parents 和 [= 的序列化分开35=] 以便仅在一个方向上序列化以避免循环引用。
首先我们需要可配置的 ExclusionStrategy
如:
public class FieldExclusionStrategy implements ExclusionStrategy {
private final List<String> skipFields;
public FieldExclusionStrategy(String... fieldNames) {
skipFields = Arrays.asList(fieldNames);
}
@Override
public boolean shouldSkipField(FieldAttributes f) {
return skipFields.contains(f.getName());
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
}
那么 TypeAdapter
就像:
public class LinkedListAdapter extends TypeAdapter<StructId> {
private static final String PARENT_ID = "ParentId";
private static final String CHILD_ID = "ChildId";
private Gson gson;
@Override
public void write(JsonWriter out, StructId value) throws IOException {
// First serialize everything but StructIds
// You could also use type based exclusion strategy
// but for brevity I use just this one
gson = new GsonBuilder()
.addSerializationExclusionStrategy(
new FieldExclusionStrategy(CHILD_ID, PARENT_ID))
.create();
JsonObject structObject = gson.toJsonTree(value).getAsJsonObject();
JsonObject structParentObject;
JsonObject structChildObject;
// If exists go through the ParentId side in one direction.
if(null!=value.ParentId) {
gson = new GsonBuilder()
.addSerializationExclusionStrategy(new FieldExclusionStrategy(CHILD_ID))
.create();
structObject.add(PARENT_ID, gson.toJsonTree(value.ParentId));
if(null!=value.ParentId.ChildId) {
gson = new GsonBuilder()
.addSerializationExclusionStrategy(new FieldExclusionStrategy(PARENT_ID))
.create();
structParentObject = structObject.get(PARENT_ID).getAsJsonObject();
structParentObject.add(CHILD_ID, gson.toJsonTree(value.ParentId.ChildId).getAsJsonObject());
}
}
// And also if exists go through the ChildId side in one direction.
if(null!=value.ChildId) {
gson = new GsonBuilder()
.addSerializationExclusionStrategy(new FieldExclusionStrategy(PARENT_ID))
.create();
structObject.add(CHILD_ID, gson.toJsonTree(value.ChildId));
if(null!=value.ChildId.ParentId) {
gson = new GsonBuilder()
.addSerializationExclusionStrategy(new FieldExclusionStrategy(CHILD_ID))
.create();
structChildObject = structObject.get(CHILD_ID).getAsJsonObject();
structChildObject.add(PARENT_ID, gson.toJsonTree(value.ChildId.ParentId).getAsJsonObject());
}
}
// Finally write the combined result out. No need to initialize gson anymore
// since just writing JsonElement
gson.toJson(structObject, out);
}
@Override
public StructId read(JsonReader in) throws IOException {
return null;
}}
正在测试:
@Slf4j
public class TestIt extends BaseGsonTest {
@Test
public void test1() {
StructId grandParent = new StructId();
StructId parent = new StructId();
grandParent.ChildId = parent;
parent.ParentId = grandParent;
StructId child = new StructId();
parent.ChildId = child;
child.ParentId = parent;
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(StructId.class, new LinkedListAdapter())
.create();
log.info("\n{}", gson.toJson(parent));
}}
会给你类似的东西:
{
"Name": "name1237598030",
"Type": {
"name": "name688766789"
},
"ParentId": {
"Name": "name1169146729",
"Type": {
"name": "name2040352617"
}
},
"ChildId": {
"Name": "name302155142",
"Type": {
"name": "name24606376"
}
}
}
我测试中的名称 material 只是默认初始化为 "name"+hashCode()
抱歉误导大家,基于此 post :
Is there a solution about Gson "circular reference"?
"there is no automated solution for circular references in Gson. The only JSON-producing library I know of that handles circular references automatically is XStream (with Jettison backend)."
但那是你不使用 Jackson 的情况!如果您已经在使用 Jackson 来构建您的 REST API 控制器,那么为什么不使用它来进行序列化呢。不需要外部组件,例如:Gson 或 XStream.
杰克逊的解决方案:
序列化:
ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
try {
jsonDesttinationIdString = ow.writeValueAsString(destinationId);
} catch (JsonProcessingException ex) {
throw new SpecificationException(ex.getMessage());
}
反序列化:
ObjectMapper mapper = new ObjectMapper();
try {
destinationStructId = destinationId.isEmpty() ? null : mapper.readValue(URLDecoder.decode(destinationId, ENCODING), StructId.class);
} catch (IOException e) {
throw new SpecificationException(e.getMessage());
}
最重要的是,您必须使用 @JsonIdentityInfo 注释:
//@JsonIdentityInfo(
// generator = ObjectIdGenerators.PropertyGenerator.class,
// property = "Name")
@JsonIdentityInfo(
generator = ObjectIdGenerators.UUIDGenerator.class,
property = "id")
public class StructId implements Serializable {
private static final long serialVersionUID = 1L;
@JsonProperty("id") // I added this field to have a unique identfier
private UUID id = UUID.randomUUID();