Як вирішити циклічне посилання в json-серіалізаторі, викликане сплячим двонаправленим відображенням?


82

Я пишу серіалізатор для серіалізації POJO до JSON, але застряг у круговій проблемі посилань. У сплячому двонаправленому відношенні "один до багатьох" батьки посилаються на дочірні та дочірні посилання на батьківського, і тут мій серіалізатор помирає. (див. приклад коду нижче)
Як розірвати цей цикл? Чи можемо ми отримати дерево власника об’єкта, щоб побачити, чи існує сам об’єкт десь у його власній ієрархії власників? Будь-який інший спосіб дізнатись, чи посилання буде круговим? чи будь-яка інша ідея вирішити цю проблему?


Ви хотіли вставити якийсь код, щоб ми допомогли вам вирішити вашу проблему?
Рассел

2
Рішення на основі анотацій Євгена нормально, але в цьому випадку немає необхідності в додаткових анотаціях та реалізації ExclusionStrategy. Просто використовуйте для цього ключове слово Java " перехідне ". Це працює для стандартної серіалізації об'єктів Java, але також Gson поважає це .
MeTTeO

Відповіді:


11

Чи може двонаправлений взаємозв'язок бути представлений у JSON? Деякі формати даних не підходять для деяких типів моделювання даних.

Одним із методів роботи з циклами при роботі з графіками об’їздів об’єктів є відстеження об’єктів, які ви бачили до цього часу (за допомогою порівняння ідентичності), щоб запобігти собі об’їзду нескінченного циклу.


Я зробив те саме, і це працює, але не впевнений, чи буде це працювати у всіх сценаріях відображення. Принаймні, зараз я добре налаштований і буду продовжувати думати про те, щоб мати більш елегантну ідею
WSK

Звичайно, вони можуть - для цього просто не існує власного типу даних або структури. Але будь-що може бути представлене у форматі XML, JSON або більшості інших форматів даних.
StaxMan

4
Мені цікаво - як би ви представили циркулярне посилання в JSON?
matt b

1
Кілька способів: майте на увазі, що загалом це не просто JSON, а комбінація JSON та деяких метаданих, найчастіше визначення класів, які ви використовуєте для прив’язки до / з JSON. Для JSON це просто питання, чи слід використовувати якусь ідентичність об’єкта, або відтворювати зв’язок (наприклад, Jackson lib має певний спосіб представити батьківські / дочірні зв’язки).
StaxMan

46

Я покладаюсь на Google JSON, щоб вирішити подібні проблеми за допомогою функції

Виключення полів із серіалізації та десеріалізації

Припустимо, двосторонній зв’язок між класами A та B наступним чином

public class A implements Serializable {

    private B b;

}

І Б

public class B implements Serializable {

    private A a;

}

Тепер використовуйте GsonBuilder, щоб отримати власний об'єкт Gson наступним чином (Зауважте метод setExclusionStrategies )

Gson gson = new GsonBuilder()
    .setExclusionStrategies(new ExclusionStrategy() {

        public boolean shouldSkipClass(Class<?> clazz) {
            return (clazz == B.class);
        }

        /**
          * Custom field exclusion goes here
          */
        public boolean shouldSkipField(FieldAttributes f) {
            return false;
        }

     })
    /**
      * Use serializeNulls method if you want To serialize null values 
      * By default, Gson does not serialize null values
      */
    .serializeNulls()
    .create();

Тепер наша кругова довідка

A a = new A();
B b = new B();

a.setB(b);
b.setA(a);

String json = gson.toJson(a);
System.out.println(json);

Погляньте на клас GsonBuilder


Дякую, Артур, за люб'язну пропозицію, але актуальне питання полягає в тому, який найкращий спосіб побудувати так званий загальний метод "shouldSkipClass". На даний момент я працював над ідеєю Matt і вирішив свою проблему, але все ще скептично, у майбутньому це рішення може порушити сертифікаційні сценарії.
WSK,

9
Це "вирішує" циклічні посилання, видаляючи їх. Немає можливості відновити вихідну структуру даних із генерованого JSON.
Sotirios Delimanolis

34

Jackson 1.6 (випущений у вересні 2010 р.) Має спеціальну підтримку на основі анотацій для обробки таких батьківських / дочірніх зв'язків, див. Http://wiki.fasterxml.com/JacksonFeatureBiDirReferences . ( Знімок шляху )

Звичайно, ви вже можете виключити серіалізацію батьківського посилання, використовуючи більшість пакетів обробки JSON (jackson, gson та flex-json, принаймні, підтримують це), але справжня хитрість полягає в тому, як десеріалізувати його назад (повторно створити батьківське посилання) просто обробляйте сторону серіалізації. Хоча на сьогодні звучить так, що просто виключення може для вас спрацювати.

EDIT (квітень 2012 р.): Jackson 2.0 тепер підтримує справжні посилання на ідентичність ( Wayback Snapshot ), тому ви можете вирішити це також таким чином.


як змусити це працювати, коли напрямок не завжди однаковий .. я спробував помістити обидві анотації в обидва поля, але це не спрацювало: Клас A {@JsonBackReference ("abc") @JsonManagedReference ("xyz") приватний В б; } Клас B {@JsonManagedReference ("abc") @JsonBackReference ("xyz") private A a; }
azi

Як зазначено вище, Object Id (@JsonIdentityInfo) - це спосіб загальних посилань працювати. Посилання на керовані / зворотні дані вимагають певних вказівок, тому вони не будуть працювати у вашому випадку.
StaxMan

12

Вирішуючи цю проблему, я застосував наступний підхід (стандартизуючи процес у моїй програмі, зробивши код зрозумілим та багаторазовим):

  1. Створіть клас анотацій для використання у полях, які ви хочете виключити
  2. Визначте клас, який реалізує інтерфейс Google ExclusionStrategy
  3. Створіть простий метод генерування об’єкта GSON за допомогою GsonBuilder (подібно до пояснення Артура)
  4. Помітьте поля, які потрібно виключити, за потреби
  5. Застосуйте правила серіалізації до об’єкта com.google.gson.Gson
  6. Серіалізуйте свій об’єкт

Ось код:

1)

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface GsonExclude {

}

2)

import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;

public class GsonExclusionStrategy implements ExclusionStrategy{

    private final Class<?> typeToExclude;

    public GsonExclusionStrategy(Class<?> clazz){
        this.typeToExclude = clazz;
    }

    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return ( this.typeToExclude != null && this.typeToExclude == clazz )
                    || clazz.getAnnotation(GsonExclude.class) != null;
    }

    @Override
    public boolean shouldSkipField(FieldAttributes f) {
        return f.getAnnotation(GsonExclude.class) != null;
    }

}

3)

static Gson createGsonFromBuilder( ExclusionStrategy exs ){
    GsonBuilder gsonbuilder = new GsonBuilder();
    gsonbuilder.setExclusionStrategies(exs);
    return gsonbuilder.serializeNulls().create();
}

4)

public class MyObjectToBeSerialized implements Serializable{

    private static final long serialVersionID = 123L;

    Integer serializeThis;
    String serializeThisToo;
    Date optionalSerialize;

    @GsonExclude
    @ManyToOne(fetch=FetchType.LAZY, optional=false)
    @JoinColumn(name="refobj_id", insertable=false, updatable=false, nullable=false)
    private MyObjectThatGetsCircular dontSerializeMe;

    ...GETTERS AND SETTERS...
}

5)

У першому випадку в конструктор подається null, ви можете вказати інший клас, який потрібно виключити - обидва варіанти додані нижче

Gson gsonObj = createGsonFromBuilder( new GsonExclusionStrategy(null) );
Gson _gsonObj = createGsonFromBuilder( new GsonExclusionStrategy(Date.class) );

6)

MyObjectToBeSerialized _myobject = someMethodThatGetsMyObject();
String jsonRepresentation = gsonObj.toJson(_myobject);

або, щоб виключити об'єкт Date

String jsonRepresentation = _gsonObj.toJson(_myobject);

1
це заважає обробці дочірнього об’єкта, чи можу я включити дочірній файл json, а потім зупинити циклічність
ir2pid

3

Якщо ви використовуєте Jackon для серіалізації, просто застосуйте @JsonBackReference до свого двонаправленого відображення. Це вирішить проблему кругового посилання.

Примітка: @JsonBackReference використовується для вирішення нескінченної рекурсії (StackOverflowError)


@JsonIgnoreзмусив мене JpaRepositoryне скласти карту об'єкта нерухомості, але @JsonBackReferenceвирішив циркулярне посилання і все-таки дозволив правильно відобразити проблемний атрибут
Bramastic

2

Використовував рішення, подібне до рішення Артура, але замість нього setExclusionStrategiesя використовував

Gson gson = new GsonBuilder()
                .excludeFieldsWithoutExposeAnnotation()
                .create();

і використовував @Exposeанотацію gson для полів, які мені потрібні в json, інші поля виключаються.


Дякую, чувак. Це спрацювало для мене. Після довгих досліджень я нарешті знайшов спосіб вийти з цієї помилки.
kepy97

2

якщо ви використовуєте весняне завантаження, Джексон видає помилку під час створення відповіді з кругових / двонаправлених даних, тому використовуйте

@JsonIgnoreProperties

ігнорувати циркулярність

At Parent:
@OneToMany(mappedBy="dbApp")
@JsonIgnoreProperties("dbApp")
private Set<DBQuery> queries;

At child:
@ManyToOne
@JoinColumn(name = "db_app_id")
@JsonIgnoreProperties("queries")
private DBApp dbApp;

1

Якщо ви використовуєте Javascript, це дуже просте рішення, використовуючи replacerпараметр JSON.stringify()методу, де ви можете передати функцію для модифікації поведінки серіалізації за замовчуванням.

Ось як ви можете ним користуватися. Розглянемо приклад нижче з 4 вузлами в циклічному графіку.

// node constructor
function Node(key, value) {
    this.name = key;
    this.value = value;
    this.next = null;
}

//create some nodes
var n1 = new Node("A", 1);
var n2 = new Node("B", 2);
var n3 = new Node("C", 3);
var n4 = new Node("D", 4);

// setup some cyclic references
n1.next = n2;
n2.next = n3;
n3.next = n4;
n4.next = n1;

function normalStringify(jsonObject) {
    // this will generate an error when trying to serialize
    // an object with cyclic references
    console.log(JSON.stringify(jsonObject));
}

function cyclicStringify(jsonObject) {
    // this will successfully serialize objects with cyclic
    // references by supplying @name for an object already
    // serialized instead of passing the actual object again,
    // thus breaking the vicious circle :)
    var alreadyVisited = [];
    var serializedData = JSON.stringify(jsonObject, function(key, value) {
        if (typeof value == "object") {
            if (alreadyVisited.indexOf(value.name) >= 0) {
                // do something other that putting the reference, like 
                // putting some name that you can use to build the 
                // reference again later, for eg.
                return "@" + value.name;
            }
            alreadyVisited.push(value.name);
        }
        return value;
    });
    console.log(serializedData);
}

Пізніше ви можете легко відтворити фактичний об'єкт з циклічними посиланнями, проаналізувавши серіалізовані дані та змінивши nextвластивість, щоб вказати на фактичний об'єкт, якщо він використовує іменовану посилання з @подібним у цьому прикладі.


1

Ось як я нарешті вирішив це у своєму випадку. Це працює принаймні з Gson & Jackson.

private static final Gson gson = buildGson();

private static Gson buildGson() {
    return new GsonBuilder().addSerializationExclusionStrategy( getExclusionStrategy() ).create();  
}

private static ExclusionStrategy getExclusionStrategy() {
    ExclusionStrategy exlStrategy = new ExclusionStrategy() {
        @Override
        public boolean shouldSkipField(FieldAttributes fas) {
            return ( null != fas.getAnnotation(ManyToOne.class) );
        }
        @Override
        public boolean shouldSkipClass(Class<?> classO) {
            return ( null != classO.getAnnotation(ManyToOne.class) );
        }
    };
    return exlStrategy;
} 

0

Ця помилка може з’явитися, якщо у вас є два об’єкти:

class object1{
    private object2 o2;
}

class object2{
    private object1 o1;
}

З використанням GSon для серіалізації у мене виникла така помилка:

java.lang.IllegalStateException: circular reference error

Offending field: o1

Щоб вирішити це, просто додайте ключове слово перехідний:

class object1{
    private object2 o2;
}

class object2{
    transient private object1 o1;
}

Як ви можете бачити тут: Чому Java має перехідні поля?

Перехідне ключове слово в Java використовується для позначення того, що поле не повинно бути серіалізованим.


0

Джексон надає JsonIdentityInfoанотації для запобігання круговим посиланням. Ви можете перевірити підручник тут .


0

Якщо ви використовуєте GSON для перетворення класу Java в JSON, ви можете уникнути полів, що викликають циклічну посилання та інфінітивний цикл, вам потрібно лише помістити анотацію @Expose у поля, які ви хочете відобразити в JSON, а поля без анотація @Expose не відображається в JSON.

Циклічне посилання з'являється, наприклад, якщо ми намагаємося серіалізувати клас User за допомогою польових маршрутів класу Route, а клас Route має польового користувача класу User, то GSON намагається серіалізувати клас User і при спробі серіалізувати маршрути, серіалізувати клас Route і в класі Route спробувати серіалізувати польового користувача, і знову спробувати серіалізувати клас User, є кругова посилання, що провокує інфінітивний цикл. Я показую згаданий клас User and Route.

import com.google.gson.annotations.Expose;

Користувач класу

@Entity
@Table(name = "user")
public class User {
    
@Column(name = "name", nullable = false)
@Expose
private String name;

@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
@OnDelete(action = OnDeleteAction.CASCADE)
private Set<Route> routes;

@ManyToMany(fetch = FetchType.EAGER)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinTable(name = "like_", joinColumns = @JoinColumn(name = "id_user"),
        inverseJoinColumns = @JoinColumn(name = "id_route"),
        foreignKey = @ForeignKey(name = ""),
        inverseForeignKey = @ForeignKey(name = ""))
private Set<Route> likes;

Класний маршрут

  @Entity
  @Table(name = "route")
  public class Route {
      
  @ManyToOne()
  @JoinColumn(nullable = false, name = "id_user", foreignKey = 
  @ForeignKey(name = "c"))    
  private User user;

Щоб уникнути інфінітивного циклу, ми використовуємо анотацію @Expose, що пропонує GSON.

Я показую у форматі JSON результат серіалізації за допомогою GSON класу User.

{
    "name": "ignacio"  
}

Ми бачимо, що маршрут поля та лайки не існують у форматі JSON, лише назва поля. Через це кругового посилання уникати.

Якщо ми хочемо цим скористатися, ми повинні створити об'єкт GSON певним чином.

Gson converterJavaToJson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().create();

Врешті-решт, ми перетворюємо Java-клас моделі сплячого користувача за допомогою створеного перетворювача GSON.

 User user = createUserWithHibernate();
 String json = converterJavaToJson.toJson(user);

-3

відповідь номер 8 тим краще, я думаю, так що якщо ви знаєте, яке поле видає помилку, ви лише встановлюєте fild в null і вирішуєте.

List<RequestMessage> requestMessages = lazyLoadPaginated(first, pageSize, sortField, sortOrder, filters, joinWith);
    for (RequestMessage requestMessage : requestMessages) {
        Hibernate.initialize(requestMessage.getService());
        Hibernate.initialize(requestMessage.getService().getGroupService());
        Hibernate.initialize(requestMessage.getRequestMessageProfessionals());
        for (RequestMessageProfessional rmp : requestMessage.getRequestMessageProfessionals()) {
            Hibernate.initialize(rmp.getProfessional());
            rmp.setRequestMessage(null); // **
        }
    }

Щоб зробити код читабельним, великий коментар переміщується з коментаря // **внизу.

java.lang.StackOverflowError [Помилка обробки запиту; вкладеним винятком є ​​org.springframework.http.converter.HttpMessageNotWritableException: Не вдалося записати JSON: нескінченна рекурсія (StackOverflowError) (через ланцюжок посилань: com.service.pegazo.bo.RequestMessageProfessional ["requestMessage"] -> com.zoserservice. bo.RequestMessage ["requestMessageProfessionals"]


1
Немає "відповіді номер 8", вам краще дати ім'я автора відповіді, на яку посилаються. Текст, який ви розмістили тут, був нечитабельним, подивіться, як розміщуються відповіді, і спробуйте викласти їх акуратно. Нарешті, я не розумію, як це відповідає на вихідне питання. Будь ласка, додайте більше деталей, щоб пояснити відповідь.
AdrianHHH

-11

Наприклад, ProductBean має serialBean. Картографування було б двонаправленим. Якщо ми зараз спробуємо використати gson.toJson(), це закінчиться циркулярним посиланням. Щоб уникнути цієї проблеми, ви можете виконати такі дії:

  1. Отримати результати з джерела даних.
  2. Ітераціюйте список і переконайтесь, що serialBean не має значення null, а потім
  3. Встановити productBean.serialBean.productBean = null;
  4. Тоді спробуйте використати gson.toJson();

Це має вирішити проблему

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.