Domanda Come risolvere il riferimento circolare nel serializzatore json causato dalla mappatura bidirezionale in letargo?


Sto scrivendo un serializzatore per serializzare POJO in JSON ma bloccato nel problema di riferimento circolare. Nella relazione uno-a-molti bidirezionale ibernata, i riferimenti genitore figlio e figlio rimandano al genitore e qui il mio serializzatore muore. (vedi esempio di codice sotto)
Come rompere questo ciclo? Possiamo ottenere l'albero proprietario di un oggetto per vedere se l'oggetto stesso esiste da qualche parte nella sua gerarchia di proprietari? Qualche altro modo per scoprire se il riferimento sarà circolare? o qualche altra idea per risolvere questo problema?


67
2017-07-27 03:11


origine


risposte:


Può una relazione bidirezionale essere rappresentata anche in JSON? Alcuni formati di dati non sono adatti per alcuni tipi di modellazione dei dati.

Un metodo per gestire i cicli quando si ha a che fare con i grafici degli oggetti che attraversano è quello di tenere traccia di quali oggetti sono stati visti finora (usando i confronti di identità), per evitare di attraversare un ciclo infinito.


10
2017-07-27 03:20



Conto su Google JSON Per gestire questo tipo di problema utilizzando la funzionalità

Esclusi i campi dalla serializzazione e dalla deserializzazione

Supponi una relazione bidirezionale tra classe A e B come segue

public class A implements Serializable {

    private B b;

}

E B

public class B implements Serializable {

    private A a;

}

Ora usa GsonBuilder Per ottenere un oggetto Gson personalizzato come segue (Nota setExclusionStrategies metodo)

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();

Ora il nostro riferimento circolare

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

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

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

Dare un'occhiata a GsonBuilder classe


45
2017-07-27 07:16



Jackson 1.6 (pubblicato a settembre 2010) ha un supporto specifico basato sull'annotazione per gestire tale collegamento genitore / figlio, vedi http://wiki.fasterxml.com/JacksonFeatureBiDirReferences. (Wayback Istantanea)

Ovviamente è già possibile escludere la serializzazione del collegamento principale già utilizzando la maggior parte dei pacchetti di elaborazione JSON (almeno Jackson, Gson e Flex-Json lo supportano), ma il vero trucco è in come deserializzare di nuovo (ricreare il link principale), non basta gestire il lato di serializzazione. Anche se sembra che per ora solo l'esclusione potrebbe funzionare per voi.

EDIT (aprile 2012): Jackson 2.0 ora supporta il vero riferimenti di identità (Wayback Istantanea), quindi puoi risolverlo anche in questo modo.


33
2017-07-29 06:12



Nell'affrontare questo problema, ho adottato il seguente approccio (standardizzando il processo attraverso la mia applicazione, rendendo il codice chiaro e riusabile):

  1. Crea una classe di annotazione da utilizzare nei campi che desideri escludere
  2. Definisci una classe che implementa l'interfaccia di ExclusionStrategy di Google
  3. Crea un metodo semplice per generare l'oggetto GSON usando GsonBuilder (simile alla spiegazione di Arthur)
  4. Annotare i campi da escludere secondo necessità
  5. Applica le regole di serializzazione al tuo oggetto com.google.gson.Gson
  6. Serializza il tuo oggetto

Ecco il codice:

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)

Nel primo caso, viene fornito un valore null al costruttore, è possibile specificare un'altra classe da escludere: entrambe le opzioni vengono aggiunte di seguito

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

6)

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

o, per escludere l'oggetto Date

String jsonRepresentation = _gsonObj.toJson(_myobject);

12
2018-04-17 06:48



Se stai usando Jackon per serializzare, basta applicare @JsonBackReference alla tua mappatura bidirezionale Risolverà il problema di riferimento circolare.

Nota: @JsonBackReference viene utilizzato per risolvere la ricorsione Infinite (StackOverflowError)


3
2018-06-06 06:34



Ha usato una soluzione simile a quella di Arthur ma invece di setExclusionStrategies ero solito

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

e usato @Expose annotazione gson per i campi di cui ho bisogno in JSON, altri campi sono esclusi.


2
2017-11-17 10:45



Se stai usando Javascript, c'è una soluzione molto semplice a quella che usa il replacer parametro di JSON.stringify() metodo in cui è possibile passare una funzione per modificare il comportamento di serializzazione predefinito.

Ecco come puoi usarlo. Considera l'esempio seguente con 4 nodi in un grafico ciclico.

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

Successivamente, è possibile ricreare facilmente l'oggetto reale con i riferimenti ciclici analizzando i dati serializzati e modificando il file next proprietà per puntare all'oggetto reale se sta usando un riferimento con un nome @ come in questo esempio.


1
2018-03-31 15:23



Ecco come ho finalmente risolto nel mio caso. Questo funziona almeno con Gson e 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;
} 

1
2017-11-17 09:35



Questo errore può essere aggiunto quando hai due oggetti:

class object1{
    private object2 o2;
}

class object2{
    private object1 o1;
}

Con l'utilizzo di GSon per la serializzazione, ho ricevuto questo errore:

java.lang.IllegalStateException: circular reference error

Offending field: o1

Per risolvere questo, basta aggiungere la parola chiave transiente:

class object1{
    private object2 o2;
}

class object2{
    transient private object1 o1;
}

Come puoi vedere qui: Perché Java ha campi transitori?

La parola chiave transitoria in Java viene utilizzata per indicare che un campo non deve essere serializzato.


0
2017-07-19 13:19



Jackson fornisce JsonIdentityInfo annotazione per evitare riferimenti circolari. Puoi controllare il tutorial Qui.


0
2018-06-18 11:13