Бесконечная рекурсия с выпуском Jackson JSON и Hibernate JPA - PullRequest
351 голосов
/ 24 июля 2010

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

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

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

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

Ответы [ 19 ]

541 голосов
/ 17 августа 2013

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

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

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


JsonManagedReference и JsonBackReference

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

Объяснение

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

Итак, Джексон берет переднюю часть ссылки (ваш 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 и Джексоне Stackoverflow по Keenformatics , мой блог.

РЕДАКТИРОВАТЬ:

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

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

Источники:

247 голосов
/ 25 июля 2010

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

86 голосов
/ 19 сентября 2016

Новая аннотация @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

44 голосов
/ 03 декабря 2013

Также, используя 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 {

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

19 голосов
/ 08 ноября 2010

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

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

11 голосов
/ 10 апреля 2012

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

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

8 голосов
/ 10 июня 2015

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

@ManyToOne
@JoinColumn(name = "ID", nullable = false, updatable = false)
@JsonIgnore
private Member member;
6 голосов
/ 14 декабря 2014

Теперь есть модуль Джексона (для 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>

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

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

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

5 голосов
/ 19 июля 2017

Для меня лучшее решение - использовать @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 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);
    }
}

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

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

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

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

С помощью Spring вы можете аннотировать ваши методы REST Controllers с помощью желаемого фильтра @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;
4 голосов
/ 01 сентября 2016

Убедитесь, что вы используете 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);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...