web-dev-qa-db-fra.com

Infinite Recursion avec Jackson JSON et Hibernate JPA

En essayant de convertir un objet JPA associé à une association bidirectionnelle en JSON, je continue à obtenir

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

Tout ce que j'ai trouvé est ce fil qui se termine essentiellement par la recommandation d'éviter les associations bidirectionnelles. Quelqu'un a-t-il une idée de solution de contournement pour ce virus du printemps?

------ EDIT 2010-07-24 16:26:22 -------

Extraits de code:

Objet métier 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 ...

Objet métier 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;

Manette:

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-implémentation du stagiaire 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

<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>
358
Ta Sas

Vous pouvez utiliser @JsonIgnore pour interrompre le cycle.

258
axtavt

JsonIgnoreProperties [Mise à jour 2017]:

Vous pouvez maintenant utiliser JsonIgnoreProperties pour supprimer la sérialisation des propriétés (pendant la sérialisation) ou ignorer le traitement des propriétés JSON lues (pendant la désérialisation) . Si ce n'est pas ce que vous cherchez, continuez à lire ci-dessous.

(Merci à As Zammel AlaaEddine de l'avoir signalé).


JsonManagedReference et JsonBackReference

Depuis Jackson 1.6, vous pouvez utiliser deux annotations pour résoudre le problème de la récursion infinie sans ignorer les getters/setters lors de la sérialisation: @JsonManagedReference et @JsonBackReference .

Explication

Pour que Jackson fonctionne correctement, l'un des deux côtés de la relation ne doit pas être sérialisé afin d'éviter la boucle infite qui provoque votre erreur stackoverflow.

Donc, Jackson prend la partie avant de la référence (votre _Set<BodyStat> bodyStats_ dans la classe Trainee) et la convertit dans un format de stockage semblable à JSON; c'est le processus dit de marshalling . Ensuite, Jackson cherche la partie arrière de la référence (c'est-à-dire _Trainee trainee_ dans la classe BodyStat) et la laisse telle quelle, sans la sérialiser. Cette partie de la relation sera reconstruite lors de la désérialisation ( unmarshalling ) de la référence aval.

Vous pouvez changer votre code comme ceci (je saute les parties inutiles):

Objet métier 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;
_

Objet métier 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;
_

Maintenant, tout devrait fonctionner correctement.

Si vous voulez plus d'informations, j'ai écrit un article sur Json et Jackson Stackoverflow sous Keenformatics , mon blog.

EDIT:

Une autre annotation utile que vous pouvez vérifier est @ JsonIdentityInfo : en l'utilisant, chaque fois que Jackson sérialise votre objet, il y ajoute un ID (ou un autre attribut de votre choix), de sorte qu'il ne " numérisez-le à chaque fois. Cela peut être utile lorsque vous avez une boucle de chaîne entre des objets plus interdépendants (par exemple: Order -> OrderLine -> User -> Order, et ainsi de suite).

Dans ce cas, vous devez faire attention, car vous pourriez avoir besoin de lire les attributs de votre objet plus d'une fois (par exemple, dans une liste de produits avec plus de produits partageant le même vendeur) et cette annotation vous empêche de le faire. Je suggère de toujours examiner les journaux de Firebug pour vérifier la réponse de Json et voir ce qui se passe dans votre code.

Sources:

552
Kurt Bourbaki

La nouvelle annotation @JsonIgnoreProperties résout bon nombre des problèmes liés aux autres options.

@Entity

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

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

Vérifiez le ici. Cela fonctionne comme dans la documentation:
http://springquay.blogspot.com/2016/01/new-approach-to-solve-json-recursive.html

87
tero17

En outre, en utilisant Jackson 2.0+, vous pouvez utiliser @JsonIdentityInfo. Cela fonctionnait beaucoup mieux pour mes classes d'hibernation que @JsonBackReference et @JsonManagedReference, qui avaient des problèmes pour moi et ne résolvaient pas le problème. Ajoutez juste quelque chose comme:

@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 {

et ça devrait marcher.

44
Marcus

En outre, Jackson 1.6 prend en charge gestion des références bidirectionnelles ... ce qui semble être ce que vous recherchez ( cette entrée de blog mentionne également la fonctionnalité)

Et à compter de juillet 2011, il existe également " jackson-module-hibernate " qui pourrait vous aider dans certains aspects du traitement des objets Hibernate, bien que pas nécessairement celui-ci (qui nécessite des annotations).

19
StaxMan

Maintenant, Jackson accepte d’éviter les cycles sans ignorer les champs:

Jackson - sérialisation d'entités avec des relations bidirectionnelles (évitant les cycles)

11
Eugene Retunsky

Cela a parfaitement fonctionné pour moi. Ajoutez l'annotation @JsonIgnore sur la classe enfant où vous mentionnez la référence à la classe parent.

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

Travaille bien pour moi Résoudre le problème de Json Infinite Recursion en travaillant avec Jackson

C'est ce que j'ai fait dans OneToMany et ManyToOne Mapping

@ManyToOne
@JoinColumn(name="Key")
@JsonBackReference
private LgcyIsp Key;


@OneToMany(mappedBy="LgcyIsp ")
@JsonManagedReference
private List<Safety> safety;
6
Prabu M

Il existe maintenant un module Jackson (pour Jackson 2) spécialement conçu pour gérer les problèmes d'initialisation paresseux d'Hibernate lors de la sérialisation.

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

Ajoutez simplement la dépendance (notez qu'il existe différentes dépendances pour Hibernate 3 et Hibernate 4):

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

puis enregistrez le module lors de l’initialisation de ObjectMapper de Jackson:

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

La documentation n'est actuellement pas géniale. Voir le code Hibernate4Module pour les options disponibles.

6
Shane

Pour moi, la meilleure solution consiste à utiliser @JsonView et à créer des filtres spécifiques pour chaque scénario. Vous pouvez également utiliser @JsonManagedReference et @JsonBackReference, mais il s’agit d’une solution codée en dur pour une seule situation, où le propriétaire fait toujours référence au côté propriétaire, et jamais l’inverse. Si vous avez un autre scénario de sérialisation dans lequel vous devez annoter différemment l'attribut, vous ne pourrez pas le faire.

Problème

Utilisons deux classes, Company et Employee où vous avez une dépendance cyclique entre elles:

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

Et la classe de test qui tente de sérialiser en utilisant 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 vous exécutez ce code, vous obtiendrez le:

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

Solution utilisant `@ JsonView`

@JsonView vous permet d'utiliser des filtres et de choisir les champs à inclure lors de la sérialisation des objets. Un filtre est juste une référence de classe utilisée comme identifiant. Commençons par créer les filtres:

public class Filter {

    public static interface EmployeeData {};

    public static interface CompanyData extends EmployeeData {};

} 

N'oubliez pas que les filtres sont des classes factices, utilisées uniquement pour spécifier les champs avec l'annotation @JsonView, afin que vous puissiez en créer autant que vous le souhaitez et dont vous avez besoin. Voyons cela en action, mais nous devons d’abord annoter notre classe Company:

public class Company {

    @JsonView(Filter.CompanyData.class)
    private Employee employee;

    public Company(Employee employee) {
        this.employee = employee;
    }

    public Employee getEmployee() {
        return employee;
    }
}

et modifiez le test pour que le sérialiseur utilise la vue:

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

Maintenant, si vous exécutez ce code, le problème de la récursion infinie est résolu car vous avez explicitement dit que vous souhaitez simplement sérialiser les attributs annotés avec @JsonView(Filter.CompanyData.class).

Lorsqu'il atteint la référence arrière de la société dans la variable Employee, il vérifie qu'il n'est pas annoté et ignore la sérialisation. Vous disposez également d’une solution puissante et flexible vous permettant de choisir les données que vous souhaitez envoyer via vos API REST.

Avec Spring, vous pouvez annoter vos méthodes de contrôleur REST avec le filtre @JsonView souhaité et la sérialisation est appliquée de manière transparente à l'objet renvoyé.

Voici les importations utilisées au cas où vous auriez besoin de vérifier:

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;
5
fabioresner

@ JsonIgnoreProperties est la réponse.

Utilisez quelque chose comme ça ::

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

Assurez-vous d'utiliser com.fasterxml.jackson partout. J'ai passé beaucoup de temps à le découvrir.

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

Ensuite, utilisez @JsonManagedReference et @JsonBackReference.

Enfin, vous pouvez sérialiser votre modèle en JSON:

import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(model);
4
Sveteek

Dans mon cas, il suffisait de changer de relation de:

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

à:

@OneToMany
private List<Town> towns;

une autre relation est restée telle quelle:

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

Vous pouvez utiliser @ JsonIgnore, mais cela ignorera les données json auxquelles il est possible d'accéder en raison de la relation de clé étrangère. Par conséquent, si vous avez besoin des données de clé étrangère (la plupart du temps, nous en avons besoin), alors @ JsonIgnore ne vous aidera pas. Dans une telle situation, veuillez suivre la solution ci-dessous.

vous obtenez une récursion infinie, à cause de la classe BodyStat renvoyant de nouveau à l'objet Trainee

BodyStat

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

stagiaire

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

Par conséquent, vous devez commenter/omettre la partie ci-dessus dans stagiaire

2
RAJ KASHWAN

J'ai aussi rencontré le même problème. J'ai utilisé le type de générateur @JsonIdentityInfoObjectIdGenerators.PropertyGenerator.class.

C'est ma solution:

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

vous pouvez utiliser la structure de création de modèles DTO Trainee DTO sans annotation en veille prolongée et vous pouvez utiliser jackson mapper pour convertir Trainee en TraineeDTO et bingo le message d'erreur disparaître :)

1
Dahar Youssef

Vous devez utiliser @JsonBackReference avec les entités @ManyToOne et @JsonManagedReference avec les classes d'entité @onetomany.

@OneToMany(
            mappedBy = "queue_group",fetch = FetchType.LAZY,
            cascade = CascadeType.ALL
        )
    @JsonManagedReference
    private Set<Queue> queues;



@ManyToOne(cascade=CascadeType.ALL)
        @JoinColumn(name = "qid")
       // @JsonIgnore
        @JsonBackReference
        private Queue_group queue_group;
1
Shubham

Si vous utilisez Spring Data Rest, le problème peut être résolu en créant des référentiels pour chaque entité impliquée dans des références cycliques.

0
Morik

Si vous ne pouvez pas ignorer la propriété, essayez de modifier la visibilité du champ. Dans notre cas, l'ancien code soumettait toujours les entités avec la relation. Dans mon cas, la solution était la suivante:

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private Trainee trainee;
0
Scott Langeberg

J'ai eu ce problème, mais je ne voulais pas utiliser d'annotation dans mes entités, alors j'ai résolu en créant un constructeur pour ma classe, ce constructeur ne doit pas avoir de référence aux entités qui font référence à cette entité. Disons ce scénario.

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 vous essayez d'envoyer à la vue la classe B ou A avec @ResponseBody, une boucle infinie risque de se produire. Vous pouvez écrire un constructeur dans votre classe et créer une requête avec votre entityManager comme ceci.

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

C'est la classe avec le constructeur.

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

}

Cependant, comme vous pouvez le constater, il existe certaines restrictions à propos de cette solution. Dans le constructeur, je n'ai pas fait référence à List bs car Hibernate ne le permet pas, du moins dans version 3.6.10.Final, donc, lorsque je dois afficher les deux entités dans une vue, je procède comme suit.

public A getAById(int id); //THE A id

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

L’autre problème de cette solution est que si vous ajoutez ou supprimez une propriété, vous devez mettre à jour votre constructeur et toutes vos requêtes.

0
OJVM