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?
82
Odpowiedzi:
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.
źródło
Opieram się na Google JSON Aby poradzić sobie z tego rodzaju problemem za pomocą funkcji
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
źródło
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.
źródło
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):
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)
lub, aby wykluczyć obiekt Date
źródło
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)
źródło
@JsonIgnore
spowodował, żeJpaRepository
nie udało mi się zmapować nieruchomości, ale@JsonBackReference
rozwiązałem odwołanie cykliczne i nadal umożliwiłem prawidłowe mapowanie dla problematycznego atrybutusetExclusionStrategies
Użyłem rozwiązania podobnego do Arthura, ale zamiast tego użyłemGson gson = new GsonBuilder() .excludeFieldsWithoutExposeAnnotation() .create();
i
@Expose
użyłem adnotacji gson dla pól, których potrzebuję w json, inne pola są wykluczone.źródło
jeśli używasz rozruchu sprężynowego, Jackson zgłasza błąd podczas tworzenia odpowiedzi z danych kołowych / dwukierunkowych, więc użyj
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;
źródło
Jeśli używasz JavaScript, istnieje bardzo proste rozwiązanie tego problemu, używając
replacer
parametruJSON.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
next
właściwość, aby wskazywała na rzeczywisty obiekt, jeśli używa on nazwanego odwołania z@
podobnym w tym przykładzie.źródło
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; }
źródło
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:
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?
źródło
Jackson zapewnia
JsonIdentityInfo
adnotację, aby zapobiec odwołaniom cyklicznym. Możesz sprawdzić samouczek tutaj .źródło
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.
źródło
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.źródło
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:productBean.serialBean.productBean = null;
gson.toJson();
To powinno rozwiązać problem
źródło