working tutorial not jsonmanagedreference jsonidentityinfo jsonbackreference java json orm spring-mvc jackson

java - tutorial - Recursión infinita con Jackson JSON y Hibernate JPA



jsonmanagedreference not working (18)

JsonIgnoreProperties [Actualización 2017]:

Ahora puede usar JsonIgnoreProperties para suprimir la serialización de propiedades (durante la serialización), o ignorar el procesamiento de las propiedades de JSON leídas (durante la deserialización) . Si esto no es lo que está buscando, siga leyendo a continuación.

(Gracias a As Zammel AlaaEddine por señalar esto).

JsonManagedReference y JsonBackReference

Desde Jackson 1.6, puede usar dos anotaciones para resolver el problema de recursión infinita sin ignorar a los captadores / @JsonManagedReference durante la serialización: @JsonManagedReference y @JsonBackReference .

Explicación

Para que Jackson funcione bien, uno de los dos lados de la relación no debe ser serializado, para evitar el bucle infinito que causa el error de stackoverflow.

Entonces, Jackson toma la parte delantera de la referencia (su Set<BodyStat> bodyStats en la clase Trainee), y la convierte en un formato de almacenamiento tipo json; Este es el llamado proceso de clasificación . Luego, Jackson busca la parte posterior de la referencia (es decir, Trainee trainee en Trainee trainee en la clase BodyStat) y la deja tal como está, sin serializarla. Esta parte de la relación se reconstruirá durante la deserialización (no desagradable ) de la referencia directa.

Puedes cambiar tu código de esta manera (omito las partes inútiles):

Objeto de negocio 1:

@Entity @Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) public class Trainee extends BusinessObject { @OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Column(nullable = true) @JsonManagedReference private Set<BodyStat> bodyStats;

Objeto de negocio 2:

@Entity @Table(name = "ta_bodystat", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) public class BodyStat extends BusinessObject { @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn(name="trainee_fk") @JsonBackReference private Trainee trainee;

Ahora todo debería funcionar correctamente.

Si desea obtener más información, escribí un artículo sobre los problemas de Stackoverflow de Json y Jackson en Keenformatics , mi blog.

EDITAR:

Otra anotación útil que podría revisar es @JsonIdentityInfo : al usarlo, cada vez que Jackson serializa su objeto, le agregará una ID (u otro atributo de su elección), para que no lo "escanee" completamente cada vez. Esto puede ser útil cuando tienes un bucle de cadena entre objetos más interrelacionados (por ejemplo: Orden -> Línea de orden -> Usuario -> Orden y otra vez).

En este caso, debe tener cuidado, ya que podría necesitar leer los atributos de su objeto más de una vez (por ejemplo, en una lista de productos con más productos que comparten el mismo vendedor), y esta anotación le impide hacerlo. Le sugiero que siempre mire los registros de firebug para verificar la respuesta de Json y ver qué ocurre en su código.

Fuentes:

Al intentar convertir un objeto JPA que tiene una asociación bidireccional en JSON, sigo obteniendo

org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)

Todo lo que encontré es este hilo que básicamente concluye con la recomendación de evitar las asociaciones bidireccionales. ¿Alguien tiene una idea para una solución para este error de primavera?

------ EDICIÓN 2010-07-24 16:26:22 -------

Fragmentos de código:

Objeto de negocio 1:

@Entity @Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) public class Trainee extends BusinessObject { @Id @GeneratedValue(strategy = GenerationType.TABLE) @Column(name = "id", nullable = false) private Integer id; @Column(name = "name", nullable = true) private String name; @Column(name = "surname", nullable = true) private String surname; @OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Column(nullable = true) private Set<BodyStat> bodyStats; @OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Column(nullable = true) private Set<Training> trainings; @OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Column(nullable = true) private Set<ExerciseType> exerciseTypes; public Trainee() { super(); } ... getters/setters ...

Objeto de negocio 2:

import javax.persistence.*; import java.util.Date; @Entity @Table(name = "ta_bodystat", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) public class BodyStat extends BusinessObject { @Id @GeneratedValue(strategy = GenerationType.TABLE) @Column(name = "id", nullable = false) private Integer id; @Column(name = "height", nullable = true) private Float height; @Column(name = "measuretime", nullable = false) @Temporal(TemporalType.TIMESTAMP) private Date measureTime; @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn(name="trainee_fk") private Trainee trainee;

Controlador:

import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletResponse; import javax.validation.ConstraintViolation; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @Controller @RequestMapping(value = "/trainees") public class TraineesController { final Logger logger = LoggerFactory.getLogger(TraineesController.class); private Map<Long, Trainee> trainees = new ConcurrentHashMap<Long, Trainee>(); @Autowired private ITraineeDAO traineeDAO; /** * Return json repres. of all trainees */ @RequestMapping(value = "/getAllTrainees", method = RequestMethod.GET) @ResponseBody public Collection getAllTrainees() { Collection allTrainees = this.traineeDAO.getAll(); this.logger.debug("A total of " + allTrainees.size() + " trainees was read from db"); return allTrainees; } }

JPA-implementación del aprendiz DAO:

@Repository @Transactional public class TraineeDAO implements ITraineeDAO { @PersistenceContext private EntityManager em; @Transactional public Trainee save(Trainee trainee) { em.persist(trainee); return trainee; } @Transactional(readOnly = true) public Collection getAll() { return (Collection) em.createQuery("SELECT t FROM Trainee t").getResultList(); } }

persistencia.xml

<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0"> <persistence-unit name="RDBMS" transaction-type="RESOURCE_LOCAL"> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="hibernate.hbm2ddl.auto" value="validate"/> <property name="hibernate.archive.autodetection" value="class"/> <property name="dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/> <!-- <property name="dialect" value="org.hibernate.dialect.HSQLDialect"/> --> </properties> </persistence-unit> </persistence>


Además, Jackson 1.6 tiene soporte para manejar referencias bidireccionales ... lo que parece ser lo que estás buscando ( esta entrada de blog también menciona la función)

Y a partir de julio de 2011, también hay " jackson-module-hibernate " que puede ayudar en algunos aspectos de tratar con los objetos de Hibernate, aunque no necesariamente este en particular (que requiere anotaciones).


Además, utilizando Jackson 2.0+ puede usar @JsonIdentityInfo . Esto funcionó mucho mejor para mis clases de hibernación que @JsonBackReference y @JsonManagedReference , que tuvieron problemas para mí y no resolvieron el problema. Solo agrega algo como:

@Entity @Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) @JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@traineeId") public class Trainee extends BusinessObject { @Entity @Table(name = "ta_bodystat", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) @JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@bodyStatId") public class BodyStat extends BusinessObject {

y debería funcionar.



Ahora hay un módulo Jackson (para Jackson 2) diseñado específicamente para manejar los problemas de inicialización perezosa de Hibernate al serializar.

https://github.com/FasterXML/jackson-datatype-hibernate

Solo agregue la dependencia (tenga en cuenta que hay diferentes dependencias para Hibernate 3 y Hibernate 4):

<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-hibernate4</artifactId> <version>2.4.0</version> </dependency>

y luego registre el módulo cuando intialice el ObjectMapper de Jackson:

ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new Hibernate4Module());

La documentación actualmente no es genial. Consulte el código Hibernate4Module para ver las opciones disponibles.


Asegúrate de usar com.fasterxml.jackson en todas partes. Pasé mucho tiempo para averiguarlo.

<properties> <fasterxml.jackson.version>2.9.2</fasterxml.jackson.version> </properties> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>${fasterxml.jackson.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${fasterxml.jackson.version}</version> </dependency>

Luego use @JsonManagedReference y @JsonBackReference .

Finalmente, puedes serializar tu modelo a JSON:

import com.fasterxml.jackson.databind.ObjectMapper; ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(model);


En caso de que esté utilizando Spring Data Rest, el problema se puede resolver creando Repositorios para cada Entidad involucrada en referencias cíclicas.


En mi caso fue suficiente para cambiar la relación de:

@OneToMany(mappedBy = "county") private List<Town> towns;

a:

@OneToMany private List<Town> towns;

Otra relación quedó como estaba:

@ManyToOne @JoinColumn(name = "county_id") private County county;


Esto funcionó perfectamente bien para mí. Agregue la anotación @JsonIgnore en la clase secundaria en la que menciona la referencia a la clase principal.

@ManyToOne @JoinColumn(name = "ID", nullable = false, updatable = false) @JsonIgnore private Member member;


La nueva anotación @JsonIgnoreProperties resuelve muchos de los problemas con las otras opciones.

@Entity public class Material{ ... @JsonIgnoreProperties("costMaterials") private List<Supplier> costSuppliers = new ArrayList<>(); ... } @Entity public class Supplier{ ... @JsonIgnoreProperties("costSuppliers") private List<Material> costMaterials = new ArrayList<>(); .... }

Compruébalo aquí. Funciona igual que en la documentación:
http://springquay.blogspot.com/2016/01/new-approach-to-solve-json-recursive.html


Para mí, la mejor solución es usar @JsonView y crear filtros específicos para cada escenario. También puede usar @JsonManagedReference y @JsonBackReference , sin embargo, es una solución codificada para una sola situación, donde el propietario siempre hace referencia al lado propietario, y nunca al revés. Si tiene otro escenario de serialización en el que necesita volver a anotar el atributo de manera diferente, no podrá hacerlo.

Problema

Permite usar dos clases, Company y Employee en las que tiene una dependencia cíclica entre ellas:

public class Company { private Employee employee; public Company(Employee employee) { this.employee = employee; } public Employee getEmployee() { return employee; } } public class Employee { private Company company; public Company getCompany() { return company; } public void setCompany(Company company) { this.company = company; } }

Y la clase de prueba que intenta serializar utilizando ObjectMapper ( Spring Boot ):

@SpringBootTest @RunWith(SpringRunner.class) @Transactional public class CompanyTest { @Autowired public ObjectMapper mapper; @Test public void shouldSaveCompany() throws JsonProcessingException { Employee employee = new Employee(); Company company = new Company(employee); employee.setCompany(company); String jsonCompany = mapper.writeValueAsString(company); System.out.println(jsonCompany); assertTrue(true); } }

Si ejecuta este código, obtendrá el:

org.codehaus.jackson.map.JsonMappingException: Infinite recursion (Error)

Solución utilizando `@ JsonView`

@JsonView permite usar filtros y elegir qué campos deben incluirse al serializar los objetos. Un filtro es solo una referencia de clase utilizada como identificador. Así que primero vamos a crear los filtros:

public class Filter { public static interface EmployeeData {}; public static interface CompanyData extends EmployeeData {}; }

Recuerde, los filtros son clases ficticias, solo se utilizan para especificar los campos con la anotación @JsonView , para que pueda crear tantos como desee y necesite. Veamos esto en acción, pero primero necesitamos anotar nuestra clase de Company :

public class Company { @JsonView(Filter.CompanyData.class) private Employee employee; public Company(Employee employee) { this.employee = employee; } public Employee getEmployee() { return employee; } }

y cambie la Prueba para que el serializador use la Vista:

@SpringBootTest @RunWith(SpringRunner.class) @Transactional public class CompanyTest { @Autowired public ObjectMapper mapper; @Test public void shouldSaveCompany() throws JsonProcessingException { Employee employee = new Employee(); Company company = new Company(employee); employee.setCompany(company); ObjectWriter writter = mapper.writerWithView(Filter.CompanyData.class); String jsonCompany = writter.writeValueAsString(company); System.out.println(jsonCompany); assertTrue(true); } }

Ahora, si ejecuta este código, se resuelve el problema de Recursión infinita, porque ha dicho explícitamente que solo desea serializar los atributos anotados con @JsonView(Filter.CompanyData.class) .

Cuando llega a la referencia posterior para la compañía en el Employee , verifica que no esté anotada e ignora la serialización. También tiene una solución potente y flexible para elegir qué datos desea enviar a través de sus API REST.

Con Spring puede anotar sus métodos de controladores REST con el filtro deseado @JsonView y la serialización se aplica de manera transparente al objeto que regresa.

Aquí están las importaciones utilizadas en caso de que necesite comprobar:

import static org.junit.Assert.assertTrue; import javax.transaction.Transactional; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.annotation.JsonView;



Puede usar @JsonIgnore , pero esto ignorará los datos json a los que se puede acceder debido a la relación de clave externa. Por lo tanto, si solicita los datos de la clave foránea (la mayoría de las veces que lo solicitamos ), @JsonIgnore no lo ayudará. En tal situación, siga la solución a continuación.

está recibiendo una recursión infinita, debido a que la clase BodyStat de nuevo se refiere al objeto Trainee

BodyStat

@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn(name="trainee_fk") private Trainee trainee;

Aprendiz

@OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Column(nullable = true) private Set<BodyStat> bodyStats;

Por lo tanto, debe comentar / omitir la parte anterior en Trainee


También me encontré con el mismo problema. @JsonIdentityInfo el tipo de generador ObjectIdGenerators.PropertyGenerator.class @JsonIdentityInfo .

Esa es mi solución:

@Entity @Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})}) @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Trainee extends BusinessObject { ...



Tuve este problema, pero no quería usar la anotación en mis entidades, así que resolví creando un constructor para mi clase, este constructor no debe tener una referencia a las entidades que hacen referencia a esta entidad. Digamos este escenario.

public class A{ private int id; private String code; private String name; private List<B> bs; } public class B{ private int id; private String code; private String name; private A a; }

Si intenta enviar a la vista la clase B o A con @ResponseBody , puede causar un bucle infinito. Puede escribir un constructor en su clase y crear una consulta con su entityManager como este.

"select new A(id, code, name) from A"

Esta es la clase con el constructor.

public class A{ private int id; private String code; private String name; private List<B> bs; public A(){ } public A(int id, String code, String name){ this.id = id; this.code = code; this.name = name; } }

Sin embargo, hay algunas restricciones sobre esta solución, como puede ver, en el constructor No hice una referencia a la Lista bs porque esto es porque Hibernate no lo permite, al menos en la versión 3.6.10.Final , por lo que cuando lo necesite Para mostrar ambas entidades en una vista hago lo siguiente.

public A getAById(int id); //THE A id public List<B> getBsByAId(int idA); //the A id.

El otro problema con esta solución es que si agrega o elimina una propiedad, debe actualizar su constructor y todas sus consultas.


puede usar el patrón DTO crear la clase TraineeDTO sin ningún hiberbnate de anotación y puede usar el mapeador jackson para convertir el Trainee en TraineeDTO y el mensaje de error desaparece :)


@JsonIgnoreProperties es la respuesta.

Usa algo como esto:

@OneToMany(mappedBy = "course",fetch=FetchType.EAGER) @JsonIgnoreProperties("course") private Set<Student> students;