杰克逊JSON和Hibernate JPA的无限递归问题

548
当尝试将具有双向关联的JPA对象转换为JSON时,我一直得到以下错误:
org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)

我找到的只有这个线程,基本上得出的结论是建议避免双向关联。有人对这个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;
    }    
}

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>

Trainee.bodyStats 中添加 @Transient - phil294
4
截至2017年,“@JsonIgnoreProperties”是最清晰的解决方案。请查看Zammel AlaaEddine的答案获取更多详细信息。 - Utku
这是春天的错吗? - Nathan Hughes
https://dev59.com/zOk5XIcBkEYKwwoY8efo#53124476 - Rishabh Agarwal
也许这会有帮助:https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion - Péter Baráth
29个回答

804

JsonIgnoreProperties [2017更新]:

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

(感谢As Zammel AlaaEddine指出这一点)。


JsonManagedReference 和 JsonBackReference

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

说明

为了使Jackson正常工作,关系的两个方面中应有一个不被序列化,以避免导致堆栈溢出错误的无限循环。

因此,Jackson获取引用的前半部分(Trainee类中的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;

现在应该一切正常了。

如果您需要更多信息,我在我的博客Keenformatics上写了一篇关于Json和Jackson Stackoverflow问题的文章。

编辑:

您可以查看另一个有用的注释@JsonIdentityInfo:使用它,每次Jackson序列化您的对象时,它都会向其添加一个ID(或您选择的其他属性),以便不必每次完全“扫描”它。当您的对象之间存在更多相互关联的对象链路(例如:订单->订单行->用户->订单等)时,这可能非常有用。

在这种情况下,您必须小心,因为您可能需要多次读取对象的属性(例如,在具有多个共享同一销售者的产品列表中),而此注释会防止您这样做。我建议始终查看Firebug日志以检查Json响应,并查看代码中发生了什么。

来源:


38
谢谢您清晰的回答。这比在后向引用上放置@JsonIgnore更方便的解决方案。 - Utku Özdemir
3
这绝对是正确的做法。如果你在服务器端这样做,因为你在那里使用了Jackson,那么客户端使用什么json映射器都无所谓,你也不必手动设置子项到父项的链接。它只是有效的。谢谢Kurt。 - flosk8
2
谢谢!@JsonIdentityInfo 可以解决涉及多个实体的循环引用问题,这些实体在许多重叠的循环中出现。 - n00b
1
使用了'@JsonManagedReference'和'@JsonBackReference'这两个注解,真的帮了我大忙。非常感谢! - erluxman
1
@Kurt:谢谢,伙计。这对我有用。我不明白为什么会得到递归无限结果,浏览器和IDE都卡住了。 - Akshat M
显示剩余21条评论

359

您可以使用@JsonIgnore来打破循环(参考链接)。

您需要导入org.codehaus.jackson.annotate.JsonIgnore(旧版)或者com.fasterxml.jackson.annotation.JsonIgnore(当前版本)。


47
自从 Jackson 1.6 版本以来,有一个更好的解决方案:您可以使用两个新的注释来解决无限递归问题,而不会在序列化过程中忽略 getter/setter。请查看下面我的回答获取详细信息。 - Kurt Bourbaki
1
@axtavt 感谢您的完美答案。顺便说一下,我想到了另一个解决方案:您可以简单地避免为此值创建getter,这样Spring在创建JSON时就无法访问它(但我认为这并不适用于每种情况,所以您的答案更好)。 - Semyon Danilov
12
以上所有解决方案似乎都需要通过添加注释来改变领域对象。如果我要序列化第三方类,而我无法修改它们,我该如何避免这个问题? - Jianwu Chen
4
在某些情况下,这种解决方案无法起作用。在使用JPA的关系型数据库中,如果您使用@JsonIgnore注释,当您更新实体时,"外键"将会变成空值。 - slim
1
我还添加了一个示例,使用@JsonView来解决问题。对我来说,这是所有讨论中最好的解决方案。 - fabioresner
显示剩余5条评论

134
新的注解 @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


@tero - 采用这种方法,我们也无法获取与实体相关联的数据。 - PAA
@PAA 嘿,PAA,我认为这与实体相关联!你为什么这么说? - tero17
1
@tero17,当你有超过2个类时,如何管理无限递归?例如:Class A -> Class B -> Class C -> Class A。我尝试使用JsonIgnoreProperties但没有成功。 - Villat
@Villat 这是另一个需要解决的问题,我建议为此开一个新的需求。 - tero17
对于代码示例点个赞,作为一个Jackson新手,仅通过阅读JavaDoc并不能完全理解@JsonIgnoreProperties的使用。 - Wecherowski

55

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

它应该能够运行。


你能解释一下“这个效果好多了”吗?托管引用有问题吗? - Utku Özdemir
@UtkuÖzdemir 我在上面的回答中添加了关于@JsonIdentityInfo的详细信息。 - Kurt Bourbaki
3
这是目前我们找到的最佳解决方案,因为当我们使用“@JsonManagedReference”时,get方法能够成功返回值而不会出现任何stackoverflow错误。但是,当我们尝试使用post保存数据时,它会返回一个415错误(不支持的媒体错误)。 - cuser
3
我已经为我的实体添加了@JsonIdentityInfo注释,但它并没有解决递归问题。只有@JsonBackReference@JsonManagedReference可以解决,但它们会从JSON中删除映射的属性。 - Oleg Abrazhaev

19

此外,Jackson 1.6版本支持双向引用的处理,看起来这正是您所需要的(此博客文章中也提到了该功能)。

截至2011年7月,还有一个名为“jackson-module-hibernate”的模块,它可以在处理Hibernate对象时提供一些帮助,尽管不一定适用于特定的情况(该模块确实需要注释)。


2
链接已经失效,您介意更新它们或编辑您的回答吗? - whatamidoingwithmylife

14

这对我完美地起作用了。在子类中提到父类的引用时,添加注释@JsonIgnore。

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

6
我认为 @JsonIgnore 会忽略该属性被传递到客户端。如果我需要获取该属性及其子属性,该怎么办? - Khasan 24-7
1
是的,我有同样的问题。但是没有人回答我。 - Kumaresan Perumal
@KumaresanPerumal 请尝试这个 https://dev59.com/aloU5IYBdhLWcg3wg3EI#37394318 - Antonio

12

11

对我来说运作良好 在使用Jackson时解决Json无限递归问题

这是我在oneToMany和ManyToOne映射中所做的。

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


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

我在Spring Boot应用程序中使用了Hibernate映射。 - Prabu M
你好,作者, 感谢您提供的优秀教程和精彩文章。然而我发现@JsonManagedReference@JsonBackReference不能获取与@OneToMany@ManyToOne相关联的数据,同时使用@JsonIgnoreProperties也会跳过关联实体数据。如何解决这个问题呢? - PAA

8

@JsonIgnoreProperties 就是答案。

可以像这样使用:

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

可以放心使用,因为我在JHipster生成的代码中看到它被使用。 - ifelse.codes
谢谢您的回答。然而,我发现@JsonManagedReference@JsonBackReference并不能获取与@OneToMany@ManyToOne场景相关联的数据,同时使用@JsonIgnoreProperties也会跳过关联实体数据。如何解决这个问题? - PAA

8

对我来说,最好的解决方案是使用 @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;
    }
}

以下是试图使用 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;
    }
}

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

@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 中的公司反向引用时,它会检查它是否未被注释并忽略序列化。您还可以选择哪些数据要通过 REST API 发送,这是一种强大而灵活的解决方案。

使用 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;

1
这是一篇不错的文章,解释了许多替代方案来解决递归问题:http://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion - Hugo Baés

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接