Recursión infinita con Jackson JSON y el problema HPA de Hibernate

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

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 asociaciones bidireccionales. ¿Alguien tiene una idea para una solución para este error de spring?

—— EDITAR 2010-07-24 16:26:22 ——-

Fragmentos de código:

Objeto comercial 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 bodyStats; @OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Column(nullable = true) private Set trainings; @OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Column(nullable = true) private Set exerciseTypes; public Trainee() { super(); } ... getters/setters ... 

Objeto comercial 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 trainees = new ConcurrentHashMap(); @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; } } 

Implementación JPA 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(); } } 

persistence.xml

   false     <!--  -->    

Puede usar @JsonIgnore para romper el ciclo.

JsonIgnoreProperties [2017 Update]:

Ahora puede usar JsonIgnoreProperties para suprimir la serialización de propiedades (durante la serialización), o ignorar el procesamiento de las propiedades JSON leídas (durante la deserialización) . Si esto no es lo que estás buscando, sigue 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 los getters / setters 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 ciclo de infite que causa el error de stackoverflow.

Entonces, Jackson toma la parte delantera de la referencia (su Set bodyStats en clase de aprendiz) y la convierte en un formato de almacenamiento similar a 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 ( descalificación ) de la referencia directa.

Puedes cambiar tu código así (me salteo las partes inútiles):

Objeto comercial 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 bodyStats; 

Objeto comercial 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 Json y Jackson Stackoverflow en Keenformatics , mi blog.

EDITAR:

Otra anotación útil que podría verificar es @JsonIdentityInfo : usándola, cada vez que Jackson serializa su objeto, agregará una ID (u otro atributo de su elección), de modo que no lo “escanee” por completo nuevamente cada vez. Esto puede ser útil cuando tiene un bucle de cadena entre más objetos interrelacionados (por ejemplo: Pedido -> Línea de pedido -> Usuario -> Pedido 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. Sugiero que siempre eche un vistazo a los registros de Firebug para verificar la respuesta de Json y ver qué está sucediendo en su código.

Fuentes:

  • Keenformatics – Cómo resolver JSON recursión infinita Stackoverflow (mi blog)
  • Jackson Referencias
  • Experiencia personal

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

 @Entity public class Material{ ... @JsonIgnoreProperties("costMaterials") private List costSuppliers = new ArrayList<>(); ... } @Entity public class Supplier{ ... @JsonIgnoreProperties("costSuppliers") private List 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

Además, usando 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

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

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

Ahora Jackson admite evitar ciclos sin ignorar los campos:

Jackson – serialización de entidades con relaciones birecionales (evitando ciclos)

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

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

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

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

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

  com.fasterxml.jackson.datatype jackson-datatype-hibernate4 2.4.0  

y luego registrar el módulo al inicializar el ObjectMapper de Jackson:

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

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

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

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

a:

 @OneToMany private List towns; 

otra relación se quedó como estaba:

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

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 en 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 donde necesita volver a anotar el atributo de manera diferente, no podrá hacerlo.

Problema

Usemos dos clases, Company y Employee donde tiene una dependencia cíclica entre ellos:

 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 usando 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á lo siguiente:

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

Solución usando `@ JsonView`

@JsonView permite usar filtros y elegir qué campos se deben incluir al serializar los objetos. Un filtro es solo una referencia de clase utilizada como identificador. Primero, creemos los filtros:

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

Recuerde que los filtros son clases ficticias, que solo se utilizan para especificar los campos con la anotación @JsonView , por lo que puede crear tantos como desee y necesite. Veámoslo en acción, pero primero debemos 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, el problema de Recursividad infinita está resuelto, porque ha dicho explícitamente que solo desea serializar los atributos que se anotaron con @JsonView(Filter.CompanyData.class) .

Cuando alcanza la referencia posterior para la empresa en el Employee , comprueba que no está anotado e ignora la serialización. También tiene una solución poderosa y flexible para elegir qué datos desea enviar a través de sus API REST.

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

Aquí están las importaciones usadas en caso de que necesite verificar:

 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; 

Asegúrese de utilizar com.fasterxml.jackson en todas partes. Pasé mucho tiempo para descubrirlo.

  2.9.2    com.fasterxml.jackson.core jackson-annotations ${fasterxml.jackson.version}    com.fasterxml.jackson.core jackson-databind ${fasterxml.jackson.version}  

Luego use @JsonManagedReference y @JsonBackReference .

Finalmente, puede serializar su modelo a JSON:

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

puedes usar el patrón DTO para crear la clase TraineeDTO sin ninguna anotación hiberbnate y puedes usar el mapeador jackson para convertir a Trainee en TraineeDTO y el bingo en el mensaje de error disapeare 🙂

También encontré 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 { ... 

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 necesita los datos de la clave externa (la mayoría de las veces lo necesitamos), entonces @JsonIgnore no lo ayudará. En tal situación, siga la solución a continuación.

está recibiendo recursión Infinita, debido a que la clase BodyStat vuelve a referir el 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 bodyStats; 

Por lo tanto, tienes que comentar / omitir la parte de arriba en aprendiz

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 de regreso 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 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. Puedes escribir un constructor en tu clase y crear una consulta con tu entityManager así.

 "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 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 List bs esto es porque Hibernate no lo permite, al menos en la versión 3.6.10.Final , así que cuando lo necesito para mostrar ambas entidades en una vista, hago lo siguiente.

 public A getAById(int id); //THE A id public List 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.

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

@JsonIgnoreProperties es la respuesta.

Use algo como esto ::

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

Funcionando bien para mí Resolve Json Infinite Recursion problem when working with Jackson

Esto es lo que he hecho en el mapeo OneToMany y ManyToOne

 @ManyToOne @JoinColumn(name="Key") @JsonBackReference private LgcyIsp Key; @OneToMany(mappedBy="LgcyIsp ") @JsonManagedReference private List safety;