Бесконечная рекурсия с выпуском Jackson JSON и Hibernate JPA

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

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

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

------ EDIT 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();
    }
}

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>

Ответ 1

Вы можете использовать @JsonIgnore чтобы разорвать цикл.

Ответ 2

JsonIgnoreProperties [обновление 2017 года]:

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

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


JsonManagedReference и JsonBackReference

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

Объяснение

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

Итак, Джексон берет переднюю часть ссылки (ваш Set<BodyStat> bodyStats в классе Trainee) и преобразует ее в json-подобный формат хранения; это так называемый процесс сортировки. Затем Джексон ищет заднюю часть ссылки (т.е. Trainee trainee в классе BodyStat) и оставляет ее как есть, не сериализовав ее. Эта часть отношений будет восстановлена во время десериализации (unmarshalling) прямой ссылки.

Вы можете изменить свой код следующим образом (я пропускаю ненужные части):

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

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

Теперь все должно работать правильно.

Если вам нужна дополнительная информация, я написал статью о проблемах Json и Jackson Stackoverflow по Keenformatics, в моем блоге.

EDIT:

Другой полезной аннотацией, которую вы можете проверить, является @JsonIdentityInfo: используя его, каждый раз, когда Джексон сериализует ваш объект, он добавляет к нему идентификатор (или другой выбранный вами атрибут), чтобы он не "полностью" сканировал "это снова каждый раз. Это может быть полезно, когда у вас есть цепочка между более взаимосвязанными объектами (например: Order → OrderLine → User → Order и снова).

В этом случае вы должны быть осторожны, так как вам может потребоваться прочитать атрибуты вашего объекта более одного раза (например, в списке продуктов с большим количеством продуктов, имеющих одного и того же продавца), и эта аннотация не позволяет вам сделать это. Я предлагаю всегда просматривать журналы Firebug, чтобы проверить ответ Json и посмотреть, что происходит в вашем коде.

Источники:

Ответ 3

Новая аннотация @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<>();
   ....
}

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

Ответ 4

Кроме того, используя Jackson 2.0+, вы можете использовать @JsonIdentityInfo. Это было намного лучше для моих спящих классов, чем @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 {

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

Ответ 5

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

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

Ответ 7

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

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

Ответ 8

Там теперь есть модуль Jackson (для Jackson 2), специально предназначенный для обработки проблем с инициализацией Hibernate при инициализации.

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>

а затем зарегистрируйте модуль при инициализации Jackson ObjectMapper:

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

Документация в настоящее время невелика. См. Hibernate4Module code для доступных опций.

Ответ 10

Для меня лучшим решением является использование @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 (Spring Загрузка):

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

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

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

Теперь, если вы запустите этот код, проблема Infinite Recursion будет решена, потому что вы явно сказали, что хотите просто сериализовать атрибуты, которые были аннотированы с помощью @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;

Ответ 11

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

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

в

@OneToMany
private List<Town> towns;

оставалось еще одно соотношение:

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

Ответ 12

Убедитесь, что вы используете com.fasterxml.jackson везде. Я потратил много времени, чтобы узнать это.

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

Ответ 13

@JsonIgnoreProperties - это ответ.

Используйте что-то вроде этого::

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

Ответ 14

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

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

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;

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

Ответ 15

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

Ответ 16

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

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

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

Ответ 17

Вы должны использовать @JsonBackReference с сущностью @ManyToOne и @JsonManagedReference с @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;

Ответ 18

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

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 не позволяет его, по крайней мере, в version 3.6.10.Final, поэтому, когда мне нужно показать обе сущности в представлении, я делаю следующее.

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

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

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

Ответ 19

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

Ответ 20

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

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