Бесконечная рекурсия с проблемой Jackson JSON и Hibernate JPA

при попытке преобразовать объект JPA, который имеет двунаправленную ассоциацию в JSON, я продолжаю получать

org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)
и этой теме который в основном заканчивается рекомендацией избегать двунаправленных ассоциаций. У кого-нибудь есть идея обходного пути для этой весенней ошибки?

------ изменить 2010-07-24 16:26:22 -------

Codesnippets:

Бизнес-Объект 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 ...

Бизнес-Объекта 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;
:
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-реализация 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();
    }
}

настойчивость.в 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>

18 ответов


вы можете использовать @JsonIgnore чтобы разорвать порочный круг.


JsonIgnoreProperties [Обновление 2017]:

теперь вы можете использовать JsonIgnoreProperties to подавить сериализацию свойств (во время сериализации) или игнорировать обработку чтения свойств JSON (во время десериализации). Если это не то, что вы ищете, пожалуйста, продолжайте читать ниже.

(спасибо as Zammel AlaaEddine за указание на это).


JsonManagedReference и JsonBackReference

начиная с Jackson 1.6 вы можете использовать две аннотации для решения проблемы бесконечной рекурсии, не игнорируя геттеры / сеттеры во время сериализации:@JsonManagedReference и @JsonBackReference.

объяснение

чтобы Джексон работал хорошо, одна из двух сторон отношений не должна быть сериализована, чтобы избежать цикла infite, который вызывает ошибку stackoverflow.

так, Джексон берет переднюю часть ссылки (your Set<BodyStat> bodyStats в классе стажера) и преобразует его в JSON-подобный формат хранения; это так называемый упорядочить


новая аннотация @JsonIgnoreProperties решает многие проблемы с другими параметрами.

@Entity

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

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

посмотреть здесь. Оно работает как раз как в documentation:
http://springquay.blogspot.com/2016/01/new-approach-to-solve-json-recursive.html


кроме того, с помощью Jackson 2.0+ вы можете использовать @JsonIdentityInfo. Это работало намного лучше для моих классов hibernate, чем @JsonBackReference и @JsonManagedReference, который имел проблемы для меня и не решил проблему. Просто добавьте что-то вроде:

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

и это должно сработать.


кроме того, Jackson 1.6 поддерживает обработка двунаправленных ссылок... что похоже на то, что вы ищете (запись в блоге также упоминает эту функцию)

и по состоянию на июль 2011 года, там тоже "Джексон-модуль-спящий режим", который может помочь в некоторых аспектах работы с объектами Hibernate, хотя и не обязательно этот конкретный (который требует аннотаций).


теперь Джексон поддерживает избегание циклов, не игнорируя поля:

Джексон - сериализации сущностей с отношениями birectional (избегая циклов)


это сработало отлично для меня. Добавьте аннотацию @JsonIgnore в дочерний класс, где вы упоминаете ссылку на родительский класс.

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

теперь есть модуль Джексона (для Джексона 2), специально разработанный для обработки проблем спящего режима ленивой инициализации при сериализации.

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

просто добавьте зависимость (обратите внимание, что существуют разные зависимости для Hibernate 3 и Hibernate 4):

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

а затем зарегистрировать модуль при intializing ObjectMapper Джексона:

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

документация в настоящее время это не здорово. Вижу код Hibernate4Module доступные варианты.


в моем случае достаточно было изменить отношение от:

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

в:

@OneToMany
private List<Town> towns;

другой родственник остался как был:

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

для меня лучшим решением является использование @JsonView и создать отдельные фильтры для каждого сценария. Вы также можете использовать @JsonManagedReference и @JsonBackReference, однако это жестко закодированное решение только для одной ситуации, когда владелец всегда ссылается на сторону владельца, а не наоборот. Если у вас есть другой сценарий сериализации, где вам нужно по-другому аннотировать атрибут, вы не сможете.

позволяет использовать два класса, Company и Employee где у вас есть циклическая зависимость между ними:

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

и тестовый класс, который пытается сериализовать с помощью ObjectMapper (Весна Загрузки):

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

если вы запустите этот код, вы получите:

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

Решение С Использованием '@JsonView'

@JsonView позволяет использовать фильтры и выбирать, какие поля должны быть включены при сериализации объектов. Фильтр - это просто ссылка на класс, используемая в качестве идентификатора. Итак, давайте сначала создайте фильтры:

public class Filter {

    public static interface EmployeeData {};

    public static interface CompanyData extends EmployeeData {};

} 

помните, что фильтры являются фиктивными классами, просто используемыми для указания полей с помощью @JsonView аннотации, так что вы можете создать столько, сколько вы хотите, и нужно. Давайте посмотрим его в действии, но Сначала нам нужно аннотировать наш Company класс:

public class Company {

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

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

    public Employee getEmployee() {
        return employee;
    }
}

и измените тест, чтобы сериализатор использовал представление:

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

теперь, если вы запустите этот код, проблема бесконечной рекурсии будет решена, потому что вы явно сказал, что вы просто хотите сериализовать атрибуты, которые были аннотированы с @JsonView(Filter.CompanyData.class).

когда он достигает задней ссылки для компании в Employee, он проверяет, что он не аннотирован и игнорирует сериализацию. У вас также есть мощное и гибкое решение для выбора данных, которые вы хотите отправить через API REST.

С Spring вы можете аннотировать свои методы контроллеров REST с помощью желаемого @JsonView фильтр и сериализация применяется прозрачно для возвращаемого объекта.

вот импорт, используемый в случае, если вам нужно проверить:

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;

убедитесь, что вы используете com.fasterxml.Джексон!--6--> везде. Я потратил много времени, чтобы выяснить это.

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

затем использовать @JsonManagedReference и @JsonBackReference.

наконец, вы можете сериализовать свою модель в JSON:

import com.fasterxml.jackson.databind.ObjectMapper;

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

вы можете использовать шаблон DTO создайте класс TraineeDTO без anotation hiberbnate, и вы можете использовать jackson mapper для преобразования стажера в TraineeDTO и бинго сообщение об ошибке disapeare:)


Я также столкнулся с той же проблемой. Я использовал @JsonIdentityInfo ' s ObjectIdGenerators.PropertyGenerator.class тип генератора.

вот мое решение:

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

можно использовать @JsonIgnore, но это будет игнорировать данные json, к которым можно получить доступ из-за отношения внешнего ключа. Поэтому, если вы reqiure данные внешнего ключа (большую часть времени мы требуем), то @JsonIgnore не поможет вам. В такой ситуации, пожалуйста, следуйте приведенному ниже решению.

вы получаете бесконечную рекурсию, из-за BodyStat класс снова ссылаясь на ученик объект

BodyStat

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

ученик

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

поэтому вы должны прокомментировать / опустить вышеуказанную часть в ученик


@JsonIgnoreProperties ответ.

использовать что-то вроде этого ::

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

у меня была эта проблема, но я не хотел использовать аннотацию в моих сущностях, поэтому я решил, создав конструктор для моего класса, этот конструктор не должен иметь ссылки на сущности, которые ссылаются на эту сущность. Допустим такой сценарий.

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

если вы попытаетесь отправить в представление класс B или A С @ResponseBody это может вызвать бесконечный цикл. Вы можете написать конструктор в своем классе и создать запрос с помощью entityManager как этот.

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

это класс с конструктором.

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

}

однако, есть некоторые ограничения об этом решении, как вы можете видеть, в конструкторе я не сделал ссылку на список bs это потому, что Hibernate не позволяет это, по крайней мере, в версия 3.6.10.Финал, поэтому, когда мне нужно показать обе сущности в представлении, я делаю следующее.

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

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

другая проблема с этим решением заключается в том, что если вы добавите или удалить свойство необходимо обновить конструктор и все запросы.


Если вы используете Spring Data Rest, проблема может быть решена путем создания репозиториев для каждого объекта, участвующего в циклических ссылках.


отлично работает для меня решить проблему бесконечной рекурсии Json при работе с Jackson

Это то, что я сделал в oneToMany и ManyToOne отображение

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


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