¿Cómo resolver la referencia circular en el serializador json causada por el mapeo bidireccional de hibernación?

82

Estoy escribiendo un serializador para serializar POJO a JSON pero atascado en un problema de referencia circular. En la relación uno a muchos bidireccional de hibernación, el padre hace referencia a las referencias secundarias y secundarias al padre y aquí mi serializador muere. (ver código de ejemplo a continuación)
¿Cómo romper este ciclo? ¿Podemos obtener el árbol de propietarios de un objeto para ver si el objeto en sí existe en algún lugar de su propia jerarquía de propietarios? ¿Alguna otra forma de saber si la referencia va a ser circular? o alguna otra idea para resolver este problema?

WSK
fuente
¿Querías pegar algún código para que te ayudemos a resolver tu problema?
Russell
2
La solución basada en anotaciones de eugene está bien, pero en este caso no es necesario realizar anotaciones adicionales ni implementar ExclusionStrategy. Simplemente use la palabra clave ' transitoria ' de Java para eso. Funciona para la serialización de objetos Java estándar, pero también Gson lo respeta .
MeTTeO

Respuestas:

11

¿Se puede representar una relación bidireccional en JSON? Algunos formatos de datos no son adecuados para algunos tipos de modelado de datos.

Un método para lidiar con ciclos cuando se trata de atravesar gráficos de objetos es realizar un seguimiento de los objetos que ha visto hasta ahora (usando comparaciones de identidad), para evitar atravesar un ciclo infinito.

mate b
fuente
Hice lo mismo y funciona, pero no estoy seguro de que funcione en todos los escenarios de mapeo. Al menos por ahora estoy bien establecido y seguiré pensando en tener una idea más elegante
WSK
Por supuesto que pueden, simplemente no hay un tipo o estructura de datos nativos para esto. Pero cualquier cosa se puede representar en XML, JSON o la mayoría de los otros formatos de datos.
StaxMan
4
Tengo curiosidad, ¿cómo representaría una referencia circular en JSON?
Matt b
1
Múltiples formas: tenga en cuenta que generalmente no es solo JSON, sino una combinación de JSON y algunos metadatos, más comúnmente definiciones de clase que usa para enlazar hacia / desde JSON. Para JSON, es solo cuestión de si usar la identidad de objeto de algún tipo o recrear el enlace (Jackson lib, por ejemplo, tiene una forma específica de representar el enlace padre / hijo).
StaxMan
46

Confío en Google JSON para manejar este tipo de problema utilizando la función

Excluir campos de serialización y deserialización

Suponga una relación bidireccional entre las clases A y B de la siguiente manera

public class A implements Serializable {

    private B b;

}

Y B

public class B implements Serializable {

    private A a;

}

Ahora use GsonBuilder para obtener un objeto Gson personalizado de la siguiente manera ( observe el método 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();

Ahora nuestra referencia circular

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

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

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

Eche un vistazo a la clase de GsonBuilder

Arthur Ronald
fuente
Gracias Arthur por tu amable sugerencia, pero la pregunta real es cuál es la mejor manera de construir el método genérico "shouldSkipClass". Por ahora trabajé en la idea de Matt y resolví mi problema, pero sigo siendo escéptico, en el futuro esta solución puede fallar en ciertos escenarios.
WSK
8
Esto "resuelve" las referencias circulares eliminándolas. No hay forma de reconstruir la estructura de datos original a partir del JSON generado.
Sotirios Delimanolis
34

Jackson 1.6 (lanzado en septiembre de 2010) tiene soporte específico basado en anotaciones para manejar dicho vínculo padre / hijo, consulte http://wiki.fasterxml.com/JacksonFeatureBiDirReferences . ( Instantánea de Wayback )

Por supuesto, ya puede excluir la serialización del enlace principal que ya utiliza la mayoría de los paquetes de procesamiento JSON (jackson, gson y flex-json al menos lo admiten), pero el truco real está en cómo deserializarlo (volver a crear el enlace principal), no solo maneje el lado de la serialización. Aunque parece que por ahora solo la exclusión podría funcionar para usted.

EDITAR (abril de 2012): Jackson 2.0 ahora admite referencias de identidad verdaderas ( Wayback Snapshot ), por lo que también puede resolverlo de esta manera.

StaxMan
fuente
cómo hacer que esto funcione cuando la dirección no es siempre la misma. Intenté poner ambas anotaciones en ambos campos, pero no funcionó: Clase A {@JsonBackReference ("abc") @JsonManagedReference ("xyz") privado B b; } Clase B {@JsonManagedReference ("abc") @JsonBackReference ("xyz") privado A a; }
azi
Como se indicó anteriormente, Object Id (@JsonIdentityInfo) es la forma de hacer que las referencias generales funcionen. Las referencias administradas / posteriores requieren ciertas instrucciones, por lo que no funcionarán para su caso.
StaxMan
12

Al abordar este problema, tomé el siguiente enfoque (estandarizar el proceso en mi aplicación, hacer que el código sea claro y reutilizable):

  1. Cree una clase de anotación para usar en los campos que le gustaría excluir
  2. Definir una clase que implemente la interfaz ExclusionStrategy de Google
  3. Cree un método simple para generar el objeto GSON usando GsonBuilder (similar a la explicación de Arthur)
  4. Anote los campos que se excluirán según sea necesario
  5. Aplicar las reglas de serialización a su objeto com.google.gson.Gson
  6. Serializa tu objeto

Aquí está el código:

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)

En el primer caso, se proporciona un valor nulo al constructor, puede especificar que se excluya otra clase; ambas opciones se agregan a continuación

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

6)

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

o, para excluir el objeto Date

String jsonRepresentation = _gsonObj.toJson(_myobject);
Eugenio
fuente
1
esto evita que se procese el objeto secundario, ¿puedo incluir el json secundario y luego detener la ciclicidad?
ir2pid
3

Si está utilizando Jackon para serializar, simplemente aplique @JsonBackReference a su mapeo bidireccional. Resolverá el problema de referencia circular.

Nota: @JsonBackReference se usa para resolver la recursividad infinita (StackOverflowError)

Praveen Shendge
fuente
@JsonIgnore hizo mi JpaRepository error al mapear la propiedad, pero @JsonBackReferenceresolví la referencia circular y aún permitía el mapeo adecuado para el atributo problemático
Bramastic
2

Usé una solución similar a la de Arthur pero en lugar de setExclusionStrategiesusar

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

y @Exposeusé la anotación gson para los campos que necesito en el json, otros campos están excluidos.

gndp
fuente
Gracias amigo. Esto funcionó para mí. Después de una larga investigación, finalmente encontré una manera de salir de este error.
kepy97
2

si está usando Spring Boot, Jackson arroja un error al crear una respuesta a partir de datos circulares / bidireccionales, así que use

@JsonIgnoreProperties

ignorar la circularidad

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
fuente
1

Si está usando Javascript, hay una solución muy fácil para eso usando el replacerparámetro deJSON.stringify() método donde puede pasar una función para modificar el comportamiento de serialización predeterminado.

Así es como puedes usarlo. Considere el siguiente ejemplo con 4 nodos en un gráfico cíclico.

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

Más tarde, puede recrear fácilmente el objeto real con las referencias cíclicas analizando los datos serializados y modificando la nextpropiedad para que apunte al objeto real si está usando una referencia nombrada con un @like en este ejemplo.

abhishekcghosh
fuente
1

Así es como finalmente lo resolví en mi caso. Esto funciona al menos con 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;
} 
pirho
fuente
0

Este error puede aparecer cuando tiene dos objetos:

class object1{
    private object2 o2;
}

class object2{
    private object1 o1;
}

Con el uso de GSon para la serialización, tengo este error:

java.lang.IllegalStateException: circular reference error

Offending field: o1

Para resolver esto, simplemente agregue la palabra clave transitorio:

class object1{
    private object2 o2;
}

class object2{
    transient private object1 o1;
}

Como puede ver aquí: ¿Por qué Java tiene campos transitorios?

La palabra clave transitoria en Java se usa para indicar que un campo no debe serializarse.

Kevin ABRIOUX
fuente
0

Jackson proporciona JsonIdentityInfoanotaciones para evitar referencias circulares. Puedes consultar el tutorial aquí .

Ankur Mahajan
fuente
-3

la respuesta número 8 es mejor, creo que si sabe qué campo está arrojando un error, solo configura el campo en nulo y resuelto.

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

Para que el código sea legible, se mueve un gran comentario del comentario // **a continuación.

java.lang.StackOverflowError [Error en el procesamiento de la solicitud; La excepción anidada es org.springframework.http.converter.HttpMessageNotWritableException: No se pudo escribir JSON: Recursión infinita (StackOverflowError) (a través de la cadena de referencia: com.service.pegazo.bo.RequestMessageProfessional ["requestMessage"] -> com.service.pegazosage "). bo.RequestMessage ["requestMessageProfessionals"]

MarcelCH
fuente
1
No hay una "respuesta número 8", sería mejor dar el nombre del autor de la respuesta mencionada. El texto que publicaste aquí es ilegible, mira cómo se publican las respuestas y trata de presentarlas de manera ordenada. Finalmente, no entiendo cómo esto responde a la pregunta original. Agregue más detalles para explicar la respuesta.
AdrianHHH
-11

Por ejemplo, ProductBean tiene serialBean. El mapeo sería una relación bidireccional. Si ahora intentamos usar gson.toJson(), terminará con una referencia circular. Para evitar ese problema, puede seguir estos pasos:

  1. Recupere los resultados de la fuente de datos.
  2. Itere la lista y asegúrese de que serialBean no sea nulo, y luego
  3. Conjunto productBean.serialBean.productBean = null;
  4. Entonces intenta usar gson.toJson();

Eso debería resolver el problema

Don Bosco R
fuente