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:
- Keenformatics - Cómo resolver la recursión infinita de JSON Stackoverflow (mi blog)
- Referencias de Jackson
- Experiencia personal
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 Jackson admite evitar ciclos sin ignorar los campos:
Jackson - serialización de entidades con relaciones bireccionales (evitando ciclos)
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
para interrumpir el ciclo.
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 {
...
Trabajando bien para mí Resuelva el problema de recursión infinita de Json cuando trabaje con Jackson
Esto es lo que he hecho en mapeo OneToMany y ManyToOne
@ManyToOne
@JoinColumn(name="Key")
@JsonBackReference
private LgcyIsp Key;
@OneToMany(mappedBy="LgcyIsp ")
@JsonManagedReference
private List<Safety> safety;
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;