Une récursion infinie avec Jackson JSON et Hibernate JPA issue

en essayant de convertir un objet JPA qui a une association bi-directionnelle 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 pour contourner ce bogue de printemps?

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

Codesnippets:

Objet D'Activité 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 ...

Business Object 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;

contrôleur:

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-la mise en œuvre de la 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();
    }
}

la persistance.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>
291
demandé sur Filip Spiridonov 2010-07-24 18:00:35

18 réponses

vous pouvez utiliser @JsonIgnore pour briser le cycle.

214
répondu axtavt 2014-07-01 09:25:44

Jsonignorepropriétés [Mise À Jour 2017]:

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

(merci à As Zammel AlaaEddine d'avoir souligné cela).


JsonManagedReference et JsonBackReference

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

explication

Pour Jackson, 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.

ainsi, Jackson prend la partie avant de la référence (votre Set<BodyStat> bodyStats dans la classe de stagiaire), et la convertit dans un format de stockage JSON-like; ceci est le soi-disant la formation processus. Ensuite, Jackson cherche la partie arrière de la référence (i.e. Trainee trainee dans la classe Bodenstat) et la laisse telle quelle, sans la sérialiser. Cette partie de la relation sera reconstruit lors de la désérialisation ( unmarshalling ) de la référence avant.

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

Business Object 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;

Business Object 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 en voulez plus informations, j'ai écrit un article sur JSON et Jackson Stackoverflow issues sur 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 ajoutera un ID (ou un autre attribut de votre choix), de sorte qu'il ne sera pas entièrement" scan " à chaque fois. Cela peut être utile lorsque vous avez une chaîne de boucle entre plusieurs objets interdépendants (par exemple: Commande -> OrderLine -> Utilisateur -> et encore).

dans ce cas, vous devez être prudent, 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 qui partagent le même vendeur), et cette annotation vous empêche de le faire. Je suggère de toujours jeter un coup d'oeil aux bûches coupe-feu pour vérifier la réponse de Json et voir ce qui se passe en votre code.

Sources:

467
répondu Kurt Bourbaki 2018-01-24 21:12:13

la nouvelle annotation @Jsonignorepropriétés résout de nombreux problèmes avec les 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<>();
   ....
}

Regardez ici. Cela fonctionne comme dans la documentation:

http://springquay.blogspot.com/2016/01/new-approach-to-solve-json-recursive.html

71
répondu tero17 2017-11-03 20:36:18

aussi, en utilisant Jackson 2.0+ vous pouvez utiliser @JsonIdentityInfo . Cela a fonctionné beaucoup mieux pour mes classes d'hibernation que @JsonBackReference et @JsonManagedReference , qui ont eu des problèmes pour moi et n'a pas résolu le problème. Il suffit d'ajouter 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.

41
répondu Marcus 2014-06-02 18:56:49

aussi, Jackson 1.6 a un support pour la manipulation des références bidirectionnelles ... ce qui semble être ce que vous recherchez ( cette entrée de blog mentionne également la fonctionnalité)

et à partir de juillet 2011, il y a aussi " jackson-module-hibernate " qui pourrait aider dans certains aspects du traitement des objets Hibernés, mais pas nécessairement celui-ci en particulier (qui nécessite des annotations).

19
répondu StaxMan 2011-07-30 17:41:46

maintenant Jackson soutient éviter les cycles sans ignorer les champs:

Jackson - la sérialisation des entités avec birectional relations (en évitant les cycles)

11
répondu Eugene Retunsky 2017-05-23 11:47:28

cela a parfaitement bien 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
répondu Manjunath BR 2015-06-09 21:02:26

il y a maintenant un module Jackson (pour Jackson 2) spécialement conçu pour gérer les problèmes D'initialisation paresseuse lors de la sérialisation.

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

ajoutez simplement la dépendance (notez qu'il y a des dépendances différentes pour Hibernate 3 et Hibernate 4):

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

et ensuite enregistrer le module lors de L'intialisation de L'ObjectMapper de Jackson:

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

n'est pas bonne pour le moment. Voir le Hibernate4Module code pour les options disponibles.

6
répondu Shane 2014-12-14 04:50:52

dans mon cas il suffisait de changer la 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
répondu Klapsa2503 2014-08-29 17:31:20

pour moi, la meilleure solution est d'utiliser @JsonView et de créer des filtres spécifiques pour chaque scénario. Vous pouvez également utiliser @JsonManagedReference et @JsonBackReference , mais c'est une solution hardcodée à une seule situation, où le propriétaire fait toujours référence à la partie propriétaire, et jamais le contraire. Si vous avez un autre scénario de sérialisation où vous devez ré-annoter l'attribut différemment, vous ne pourrez pas.

problème

permet d'utiliser 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 d'essai qui essaie 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 utilisez ce code, vous obtiendrez le:

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

Solution À L'Aide De `@JsonView`

@JsonView vous permet d'utiliser des filtres et de choisir quels champs doit être inclus lors de la sérialisation des objets. Un filtre est juste une référence de classe utilisée comme identifiant. Alors créons d'abord les filtres:

public class Filter {

    public static interface EmployeeData {};

    public static interface CompanyData extends EmployeeData {};

} 

rappelez-vous, les filtres sont des classes factices, juste utilisés pour spécifier les champs avec l'annotation @JsonView , de sorte que vous pouvez créer autant que vous voulez et avez besoin. Voyons cela en action, mais d'abord nous devons 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 modifier l'essai dans l'ordre pour le serializer d'utiliser 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 récursion infinie est résolu, parce que vous avez explicitement dit que vous voulez juste sérialiser les attributs qui ont été annotés avec @JsonView(Filter.CompanyData.class) .

quand il atteint la référence arrière pour la société dans le Employee , il vérifie qu'il n'est pas annoté et ignore la sérialisation. Vous avez également une solution puissante et flexible pour choisir les données que vous souhaitez envoyer par le biais de vos API REST.

avec Spring vous pouvez annoter vos méthodes de contrôleurs REST avec le filtre @JsonView désiré et la sérialisation est appliquée de manière transparente à l'objet de retour.

Voici les importations utilisées dans le cas où vous devez 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;
4
répondu fabioresner 2017-10-19 17:41:36

assurez-vous d'utiliser com.fasterxml.jackson partout. J'ai passé beaucoup de temps à la trouver.

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

, puis @JsonManagedReference et @JsonBackReference .

enfin, vous pouvez sérialiser votre modèle à JSON:

import com.fasterxml.jackson.databind.ObjectMapper;

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(model);
3
répondu Sveteek 2017-10-19 17:36:40

vous pouvez utiliser le modèle DTO créer une classe TraineeDTO sans aucune anotation hiberbnate et vous pouvez utiliser jackson mappeur pour convertir Stagiaire à TraineeDTO et bingo le message d'erreur disapeare :)

1
répondu Dahar Youssef 2015-08-20 15:00:47

j'ai aussi rencontré le même problème. J'ai utilisé un générateur de type @JsonIdentityInfo 's ObjectIdGenerators.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
répondu Arif Acar 2018-03-14 14:39:50

vous pouvez utiliser @JsonIgnore , mais cela ignorera les données json qui peuvent être accédées en raison de la relation clé étrangère. Par conséquent, si vous utilisez les données de clé étrangère (la plupart du temps nous avons besoin), alors @JsonIgnore ne vous aidera pas. Dans une telle situation, veuillez suivre la solution ci-dessous.

vous obtenez la récursion infinie, en raison de la classe Bodenstat se référant à nouveau à la stagiaire objet

Bodenstat

@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

1
répondu RAJ KASHWAN 2018-03-27 14:28:59

@Jsonignorepropriétés est la réponse.

utilisez quelque chose comme ceci::

@OneToMany(mappedBy = "course",fetch=FetchType.EAGER)
@JsonIgnoreProperties("course")
private Set<Student> students;
1
répondu SumanP 2018-05-02 00:45:38

j'ai eu ce problème, mais je ne voulais pas utiliser l'annotation dans mes entités, donc j'ai résolu en créant un constructeur pour ma classe, ce constructeur ne doit pas avoir une référence en arrière aux entités qui font référence à cette entité. Disons que 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 il peut causer une boucle infinie. Vous pouvez écrire un constructeur dans votre classe et de 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, il y a quelques restrictions sur cette solution, comme vous pouvez le voir, dans le constructeur Je n'ai pas fait une référence à liste bs c'est parce que Hibernate ne le permet pas, au moins dans version 3.6.10.Final , donc quand j'ai besoin de montrer les deux entités dans une vue je fais ce qui suit.

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

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

L'autre problème avec cette solution est que si vous ajoutez ou supprimez une propriété, vous devez mettre à jour votre constructeur et toutes vos questions.

0
répondu OJVM 2015-11-30 21:16:37

dans le cas où vous utilisez Spring Data Rest, le problème peut être résolu en créant des dépôts pour chaque entité impliquée dans les références cycliques.

0
répondu Morik 2016-04-20 20:54:28

travaille bien pour moi résoudre le problème de récursion infinie de Json en travaillant avec Jackson

C'est ce que j'ai fait en oneToMany et ManyToOne Cartographie

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


@OneToMany(mappedBy="LgcyIsp ")
@JsonManagedReference
private List<Safety> safety;
0
répondu Prabu M 2018-05-28 05:19:56