Jak rozwiązać odwołanie cykliczne w serializatorze json spowodowane przez hibernacyjne mapowanie dwukierunkowe?

82

Piszę serializator do serializacji POJO do JSON, ale utknąłem w problemie z odwołaniem cyklicznym. W hibernacji dwukierunkowej relacji jeden-do-wielu rodzic odwołuje się do odniesień potomnych i potomnych z powrotem do rodzica i tutaj mój serializator umiera. (patrz przykładowy kod poniżej)
Jak przerwać ten cykl? Czy możemy uzyskać drzewo właściciela obiektu, aby zobaczyć, czy sam obiekt istnieje gdzieś w swojej własnej hierarchii właścicieli? Czy jest jakiś inny sposób, aby sprawdzić, czy odniesienie będzie okrągłe? lub jakikolwiek inny pomysł na rozwiązanie tego problemu?

WSK
źródło
Czy chodziło Ci o wklejenie dla nas kodu, który pomoże Ci rozwiązać Twój problem?
Russell
2
Rozwiązanie eugene oparte na adnotacjach jest w porządku, ale w tym przypadku nie ma potrzeby stosowania dodatkowych adnotacji i implementacji ExclusionStrategy. Po prostu użyj do tego słowa kluczowego Java „ transient ”. Działa w przypadku standardowej serializacji obiektów Java, ale Gson również to szanuje .
MeTTeO

Odpowiedzi:

11

Czy relacja dwukierunkowa może być w ogóle reprezentowana w formacie JSON? Niektóre formaty danych nie nadają się dobrze do niektórych typów modelowania danych.

Jedną z metod radzenia sobie z cyklami podczas przechodzenia przez wykresy obiektów jest śledzenie obiektów, które widziałeś do tej pory (za pomocą porównań tożsamości), aby zapobiec przechodzeniu w dół nieskończonego cyklu.

matowe b
źródło
Zrobiłem to samo i działa, ale nie jestem pewien, czy zadziała we wszystkich scenariuszach mapowania. Przynajmniej na razie jestem dobrze ustawiony i będę dalej myśleć o bardziej eleganckim pomyśle
WSK
Oczywiście, że tak - po prostu nie ma do tego natywnego typu danych ani struktury. Ale wszystko można przedstawić w formacie XML, JSON lub większości innych formatów danych.
StaxMan
4
Jestem ciekawy - jak przedstawiłbyś odwołanie cykliczne w JSON?
mat b
1
Wiele sposobów: pamiętaj, że generalnie nie jest to tylko JSON, ale połączenie JSON i niektórych metadanych, najczęściej definicji klas, których używasz do tworzenia powiązań z / z JSON. W przypadku JSON chodzi tylko o to, czy użyć jakiejś tożsamości obiektu, czy odtworzyć powiązanie (na przykład Jackson lib ma określony sposób reprezentowania powiązania rodzic / dziecko).
StaxMan
46

Opieram się na Google JSON Aby poradzić sobie z tego rodzaju problemem za pomocą funkcji

Wykluczanie pól z serializacji i deserializacji

Załóżmy, że relacja dwukierunkowa między klasami A i B jest następująca

public class A implements Serializable {

    private B b;

}

Oraz b

public class B implements Serializable {

    private A a;

}

Teraz użyj GsonBuilder, aby uzyskać niestandardowy obiekt Gson w następujący sposób ( metoda Notice 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();

Teraz nasze okólne odniesienie

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

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

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

Spójrz na klasę GsonBuilder

Arthur Ronald
źródło
Dziękuję Arthur za życzliwą sugestię, ale pytanie brzmi: jaki jest najlepszy sposób na zbudowanie tak zwanej ogólnej metody „shouldSkipClass”. Na razie pracowałem nad pomysłem Matta i rozwiązałem swój problem, ale nadal jestem sceptyczny, w przyszłości to rozwiązanie może się załamać w pewnych scenariuszach.
WSK,
9
To „rozwiązuje” odwołania cykliczne poprzez ich usunięcie. Nie ma możliwości odbudowania oryginalnej struktury danych z wygenerowanego JSON.
Sotirios Delimanolis
34

Jackson 1.6 (wydany we wrześniu 2010) ma specyficzną obsługę adnotacji do obsługi takich powiązań rodzic / dziecko, patrz http://wiki.fasterxml.com/JacksonFeatureBiDirReferences . ( Migawka Wayback )

Możesz oczywiście wykluczyć serializację linku nadrzędnego już przy użyciu większości pakietów przetwarzania JSON (obsługują go przynajmniej jackson, gson i flex-json), ale prawdziwa sztuczka polega na tym, jak deserializować go z powrotem (ponownie utworzyć łącze nadrzędne), a nie wystarczy obsłużyć stronę serializacji. Chociaż wydaje się, że na razie tylko wykluczenie może zadziałać.

EDYCJA (kwiecień 2012): Jackson 2.0 obsługuje teraz odniesienia do prawdziwej tożsamości ( migawka Wayback ), więc możesz to rozwiązać również w ten sposób.

StaxMan
źródło
jak sprawić, by to działało, skoro kierunek nie zawsze jest taki sam. Próbowałem umieścić oba adnotacje w obu polach, ale to nie zadziałało: Klasa A {@JsonBackReference ("abc") @JsonManagedReference ("xyz") prywatne B b; } Klasa B {@JsonManagedReference ("abc") @JsonBackReference ("xyz") private A a; }
azi
Zgodnie z powyższym, Object Id (@JsonIdentityInfo) jest sposobem na sprawienie, aby ogólne odniesienia działały. Referencje zarządzane / wsteczne wymagają pewnych wskazówek, więc nie będą działać w Twoim przypadku.
StaxMan
12

Rozwiązując ten problem, przyjąłem następujące podejście (standaryzacja procesu w całej mojej aplikacji, uczynienie kodu przejrzystym i wielokrotnego użytku):

  1. Utwórz klasę adnotacji, która będzie używana w polach, które chcesz wykluczyć
  2. Zdefiniuj klasę, która implementuje interfejs ExclusionStrategy firmy Google
  3. Utwórz prostą metodę generowania obiektu GSON za pomocą GsonBuilder (podobnie do wyjaśnienia Arthura)
  4. W razie potrzeby opisz pola, które mają zostać wykluczone
  5. Zastosuj reguły serializacji do obiektu com.google.gson.Gson
  6. Serializuj swój obiekt

Oto kod:

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)

W pierwszym przypadku do konstruktora podawana jest wartość null, można określić inną klasę do wykluczenia - obie opcje są dodane poniżej

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

6)

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

lub, aby wykluczyć obiekt Date

String jsonRepresentation = _gsonObj.toJson(_myobject);
eugene
źródło
1
zapobiega to przetwarzaniu obiektu podrzędnego, czy mogę dołączyć podrzędny
plik
3

Jeśli używasz Jackona do serializacji, po prostu zastosuj @JsonBackReference do mapowania dwukierunkowego. To rozwiąże problem z odwołaniami cyklicznymi.

Uwaga: @JsonBackReference służy do rozwiązywania nieskończonej rekurencji (StackOverflowError)

Praveen Shendge
źródło
@JsonIgnorespowodował, że JpaRepositorynie udało mi się zmapować nieruchomości, ale @JsonBackReferencerozwiązałem odwołanie cykliczne i nadal umożliwiłem prawidłowe mapowanie dla problematycznego atrybutu
Bramastic
2

setExclusionStrategiesUżyłem rozwiązania podobnego do Arthura, ale zamiast tego użyłem

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

i @Exposeużyłem adnotacji gson dla pól, których potrzebuję w json, inne pola są wykluczone.

gndp
źródło
Dzięki stary. To działa dla mnie. Po długich poszukiwaniach w końcu znalazłem sposób na wyjście z tego błędu.
kepy97
2

jeśli używasz rozruchu sprężynowego, Jackson zgłasza błąd podczas tworzenia odpowiedzi z danych kołowych / dwukierunkowych, więc użyj

@JsonIgnoreProperties

ignorować cykliczność

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

At child:
@ManyToOne
@JoinColumn(name = "db_app_id")
@JsonIgnoreProperties("queries")
private DBApp dbApp;
U_R_Naveen UR_Naveen
źródło
1

Jeśli używasz JavaScript, istnieje bardzo proste rozwiązanie tego problemu, używając replacerparametruJSON.stringify() metody, w którym możesz przekazać funkcję, aby zmodyfikować domyślne zachowanie serializacji.

Oto, jak możesz z niego korzystać. Rozważ poniższy przykład z 4 węzłami na wykresie cyklicznym.

// 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);
}

Później można łatwo odtworzyć rzeczywisty obiekt z cyklicznymi odwołaniami, analizując zserializowane dane i modyfikując nextwłaściwość, aby wskazywała na rzeczywisty obiekt, jeśli używa on nazwanego odwołania z @podobnym w tym przykładzie.

abhishekcghosh
źródło
1

W ten sposób ostatecznie rozwiązałem to w moim przypadku. Działa to przynajmniej z Gson i Jacksonem.

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;
} 
pirho
źródło
0

Ten błąd może wystąpić, gdy masz dwa obiekty:

class object1{
    private object2 o2;
}

class object2{
    private object1 o1;
}

Używając GSon do serializacji, mam ten błąd:

java.lang.IllegalStateException: circular reference error

Offending field: o1

Aby rozwiązać ten problem, po prostu dodaj przejściowe słowo kluczowe:

class object1{
    private object2 o2;
}

class object2{
    transient private object1 o1;
}

Jak widać tutaj: Dlaczego Java ma pola przejściowe?

Słowo kluczowe transient w Javie służy do wskazania, że ​​pole nie powinno być serializowane.

Kevin ABRIOUX
źródło
0

Jackson zapewnia JsonIdentityInfoadnotację, aby zapobiec odwołaniom cyklicznym. Możesz sprawdzić samouczek tutaj .

Ankur Mahajan
źródło
0

Jeśli używasz GSON do konwersji klasy Java na JSON, możesz uniknąć pól, które powodują odwołanie cykliczne i pętlę bezokolicznika, wystarczy umieścić adnotację @Expose w polach, które mają się pojawić w JSON, a pola bez adnotacja @Expose nie pojawia się w JSON.

Cykliczne odwołanie pojawia się na przykład, jeśli próbujemy serializować klasę User za pomocą tras pól klasy Route, a klasa Route ma użytkownika pola klasy User, wtedy GSON próbuje serializować klasę User i gdy próbuje serializować trasy, serializuj klasę Route, aw klasie Route spróbuj zserializować użytkownika pola i ponownie spróbuj serializować klasę User, istnieje cykliczne odwołanie, które wywołuje bezokolicznikową pętlę. Pokazuję wspomnianą klasę User i Route.

import com.google.gson.annotations.Expose;

Użytkownik klasy

@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;

Trasa klasowa

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

Aby uniknąć nieskończonej pętli, używamy adnotacji @Expose, która oferuje GSON.

Pokazuję w formacie JSON wynik serializacji z GSON klasy User.

{
    "name": "ignacio"  
}

Widzimy, że pole route i polubienia nie istnieje w formacie JSON, tylko nazwa pola. Z tego powodu unika się cyklicznego odniesienia.

Jeśli chcemy z tego skorzystać, musimy w określony sposób stworzyć obiekt GSON.

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

Na koniec przekształcamy klasę java modelu hibernującego użytkownika za pomocą utworzonego konwertera GSON.

 User user = createUserWithHibernate();
 String json = converterJavaToJson.toJson(user);
Ignacio Marín Reyes
źródło
-3

odpowiedź numer 8 jest lepsza, myślę, że jeśli wiesz, które pole zgłasza błąd, ustawiasz tylko fild na null i rozwiązujesz.

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); // **
        }
    }

Aby kod był czytelny, duży komentarz jest przenoszony z komentarza // **poniżej.

java.lang.StackOverflowError [Przetwarzanie żądania nie powiodło się; zagnieżdżony wyjątek to org.springframework.http.converter.HttpMessageNotWritableException: Nie można zapisać JSON: nieskończona rekurencja (StackOverflowError) (poprzez łańcuch referencji: com.service.pegazo.bo.RequestMessageProfessional ["requestMessage"] -> com.service.pegaz. bo.RequestMessage ["requestMessageProfessionals"]

MarcelCH
źródło
1
Nie ma „odpowiedzi numer 8”, lepiej byłoby podać nazwisko autora przywoływanej odpowiedzi. Tekst, który tu zamieściłeś, był nieczytelny, spójrz na sposób zamieszczania odpowiedzi i spróbuj starannie je ułożyć. Wreszcie nie rozumiem, jak to odpowiada na pierwotne pytanie. Dodaj więcej szczegółów, aby wyjaśnić odpowiedź.
AdrianHHH
-11

Na przykład ProductBean ma serialBean. Odwzorowanie byłoby zależnością dwukierunkową. Jeśli teraz spróbujemy użyć gson.toJson(), zakończy się to cyklicznym odniesieniem. Aby uniknąć tego problemu, możesz wykonać następujące kroki:

  1. Pobierz wyniki ze źródła danych.
  2. Wykonaj iterację listy i upewnij się, że serialBean nie jest null, a następnie
  3. Zestaw productBean.serialBean.productBean = null;
  4. Następnie spróbuj użyć gson.toJson();

To powinno rozwiązać problem

Don Bosco R.
źródło