Как решить циклическую ссылку при сериализации объекта, который имеет член класса с тем же типом этого объекта - PullRequest
0 голосов
/ 02 января 2019

Я сталкиваюсь с этой проблемой при использовании Gson для сериализации объекта, у которого есть член класса с таким же типом:

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, и игнорирование их при сериализации не является решением. Как это можно решить?

Относится к ответу, помеченному как Решение:

У меня есть такая структура: - Struct1 -- Таблица --- Переменная1

Объект до сериализации: enter image description here

И генерируется Json: enter image description here

Как видите, ParentId таблицы - это "Struct1", но ChildId для "Struct1" пуст и должен быть "Table"

* 1030 справочное издание *

Ответы [ 3 ]

0 голосов
/ 02 января 2019

Просто чтобы показать один способ сделать сериализацию (чтобы я не обрабатывал десериализацию) с TypeAdapter и ExclusionStrategy. Возможно, это не самая красивая реализация, но в любом случае она довольно общая.

В этом решении используется тот факт, что ваша структура является своего рода двунаправленным связанным списком, и с учетом любого узла в этом списке нам просто нужно разделить сериализацию родителей и детей, чтобы они сериализовались только в одном направлении. избегать циклических ссылок.

Сначала нам нужно настроить 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"
   }
 }
}

Имена в моем тестовом материале просто по умолчанию инициализируются с "name"+hashCode()

0 голосов
/ 08 января 2019

Извините, что вводите вас в заблуждение, ребята, основываясь на этом посте:

Есть ли решение по поводу "круговой ссылки" Gson?

"Не существует автоматического решения дляциклические ссылки в Gson. Единственная известная мне JSON-библиотека, которая обрабатывает циклические ссылки автоматически: XStream (с бэкендом Jettison). "

Но в этом случае вы не используете Джексона!Если вы уже используете 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();
0 голосов
/ 02 января 2019

Я думаю, что использование ExclusionStrategy не является правильным подходом для решения этой проблемы.

Я бы скорее предложил использовать JsonSerializer и JsonDeserializerнастроенный для вашего StructId класса.
(может быть, подход, использующий TypeAdapter, был бы даже лучше, но у меня не было достаточно опыта Gson, чтобы все заработало.)

Таким образом, вы должны создать свой экземпляр Gson следующим образом:

Gson gson = new GsonBuilder()
    .registerTypeAdapter(StructId.class, new StructIdSerializer())
    .registerTypeAdapter(StructId.class, new StructIdDeserializer())
    .setPrettyPrinting()
    .create();

Класс StructIdSerializer, указанный ниже, отвечает за преобразование 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 ниже отвечает за преобразование JSON в StructId.Он преобразует свойства JSON Name, Type и ChildId в соответствующие поля Java в StructId.Обратите внимание, что поле 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.

...