无限递归与Jackson JSON和Hibernate JPA问题

gdrx4gfi  于 2022-09-19  发布在  Spring
关注(0)|答案(28)|浏览(256)

当尝试将具有双向关联的JPA对象转换为JSON时,我不断得到

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

我所发现的是this thread,其基本结论是建议避免双向关联。有人有解决这个spring bug的方法吗?
------编辑2010-07-24 16:26:22-------
代码片段:
业务对象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;
    }    
}

学员DAO的JPA实施:

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

of1yzvn41#

JSONIGNOREPORTIES〔2017年更新〕:

现在可以使用JsonIgnoreProperties抑制属性的序列化(在序列化期间),或忽略JSON属性读取的处理(在反序列化期间)。如果这不是您想要的,请继续阅读下面的内容。
(感谢As Zammel AlaaEddine指出这一点)。

JsonManagedReference和JsonBackReference

自Jackson 1.6以来,您可以使用两个注解来解决无限递归问题,而不会在序列化期间忽略getter/setter:@JsonManagedReference@JsonBackReference

解释

为了让Jackson正常工作,不应该序列化关系的两个方面之一,以避免导致stackoverflow错误的内联循环。
因此,Jackson获取引用的前向部分(学员类中的Set<BodyStat> bodyStats),并将其转换为类似json的存储格式;这就是所谓的编组过程。然后,Jackson查找引用的后面部分(即BodyStat类中的Trainee trainee),并保持原样,而不是序列化它。这部分关系将在前向引用的反序列化(解组)过程中重新构建。
您可以这样更改代码(我跳过了无用的部分):

业务对象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 and Jackson Stackoverflow issues on Keenformatics的文章。

编辑:

您可以检查的另一个有用的注解是@JsonIdentityInfo:使用它,每次Jackson序列化对象时,它都会向其添加一个ID(或您选择的其他属性),这样它就不会每次都完全“扫描”它。当您在多个相互关联的对象之间有一个链循环时,这可能很有用(例如:订单->订单行->用户->订单,然后重复)。
在这种情况下,您必须小心,因为您可能需要多次读取对象的属性(例如,在产品列表中,多个产品共享同一卖家),并且此注解阻止您这样做。我建议始终查看firebug日志,以检查Json响应,并查看代码中发生了什么。

资料来源:

mrzz3bfm

mrzz3bfm2#

您可以使用@JsonIgnore打破循环(参考)。
您需要导入org.codehaus.jackson.annotate.JsonIgnore(旧版本)或com.fasterxml.jackson.annotation.JsonIgnore(当前版本)。

2sbarzqh

2sbarzqh3#

新注解@jsonignoreperties解决了其他选项的许多问题。

@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

cgyqldqp

cgyqldqp4#

此外,使用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 {

它应该会起作用。

sg24os4d

sg24os4d5#

此外,Jackson 1.6支持handling bi-directional references…这似乎是您正在寻找的功能(this blog entry也提到了该功能)
截至2011年7月,还有“jackson-module-hibernate”,它可能在处理Hibernate对象的某些方面有所帮助,但不一定是这个特定的对象(需要注解)。

x0fgdtte

x0fgdtte7#

这对我来说非常好。在子类中添加注解@JsonIgnore,其中提到了对父类的引用。

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

nom7f22z8#

对我来说工作正常Resolve Json Infinite Recursion problem when working with Jackson
这就是我在一对一和多对一Map中所做的

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

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

h9vpoimq9#

对我来说,最好的解决方案是使用@JsonView并为每个场景创建特定的过滤器。您也可以使用@JsonManagedReference@JsonBackReference,但这是一种硬编码解决方案,仅适用于一种情况,即所有者始终引用拥有方,而不是相反。如果您有另一个序列化场景,需要以不同的方式重新注解属性,则您将无法这样做。

问题

让我们使用两个类CompanyEmployee,它们之间有循环依赖关系:

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

以及尝试使用ObjectMapperSpring 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;
    }
}

并更改测试,以便序列化程序使用视图:

@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中公司的反向引用时,它会检查是否未对其进行注解,并忽略序列化。您还有一个强大而灵活的解决方案来选择要通过RESTAPI发送的数据。
使用Spring,您可以使用所需的@JsonView过滤器注解REST控制器方法,并将序列化透明地应用于返回对象。
以下是您需要检查时使用的导入:

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

fdx2calv10#

现在有一个Jackson模块(用于Jackson 2),专门用于处理序列化时的Hibernate延迟初始化问题。
https://github.com/FasterXML/jackson-datatype-hibernate
只需添加依赖项(注意Hibernate 3和Hibernate4有不同的依赖项):

<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代码。

w8f9ii69

w8f9ii6911#

***@JSONIGNOREPORTIES***是答案。

使用类似这样的东西::

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

czfnxgou12#

您应该将@JsonBackReference与@ManyTone实体一起使用,@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;
lvmkulzt

lvmkulzt13#

在我的情况下,将关系从:

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

致:

@OneToMany
private List<Town> towns;

另一个关系保持原样:

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

6yjfywim14#

我也遇到了同样的问题。我使用@JsonIdentityInfoObjectIdGenerators.PropertyGenerator.class发电机类型。
这是我的解决方案:

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

6jygbczu15#

请确保使用**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);

相关问题