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

Ta *_*Sas 375 java orm json spring-mvc jackson

在尝试将具有双向关联的JPA对象转换为JSON时,我不断获取

org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)
Run Code Online (Sandbox Code Playgroud)

我找到的只是这个线程,基本上建议避免双向关联.有没有人对这个春天的bug有一个解决方法?

------编辑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 ...
Run Code Online (Sandbox Code Playgroud)

业务对象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;
Run Code Online (Sandbox Code Playgroud)

控制器:

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;
    }    
}
Run Code Online (Sandbox Code Playgroud)

实习生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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

Kur*_*aki 565

JsonIgnoreProperties [2017年更新]:

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

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


JsonManagedReference和JsonBackReference

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

说明

为了让Jackson运行良好,关系的两个方面之一不应该被序列化,以避免导致堆栈溢出错误的infite循环.

因此,Jackson采用了引用的前向部分(您Set<BodyStat> bodyStats在Trainee类中),并将其转换为类似json的存储格式; 这就是所谓的编组过程.然后,杰克逊寻找引用的Trainee trainee后半部分(即在BodyStat类中)并保持原样,而不是序列化它.这部分关系将在前向引用的反序列化(解组)期间重新构建.

您可以像这样更改您的代码(我跳过无用的部分):

业务对象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;
Run Code Online (Sandbox Code Playgroud)

业务对象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;
Run Code Online (Sandbox Code Playgroud)

现在一切都应该正常运作.

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

编辑:

你可以检查的另一个有用的注释是@JsonIdentityInfo:使用它,每当Jackson序列化你的对象时,它会为它添加一个ID(或你选择的另一个属性),这样它就不会每次都完全"扫描"它.当你在更多相互关联的对象之间有一个链循环时(例如:Order - > OrderLine - > User - > Order and over),这可能很有用.

在这种情况下,您必须要小心,因为您可能需要多次读取对象的属性(例如,在产品列表中包含更多共享同一卖家的产品),并且此注释会阻止您这样做.我建议总是看看firebug日志来检查Json响应,看看你的代码中发生了什么.

资料来源:

  • 谢谢你的回答.这比将`@JsonIgnore`放在后引用上更方便. (28认同)
  • 这绝对是正确的方法.如果你在服务器端这样做,因为你在那里使用Jackson,那么你在客户端使用的json mapper并不重要,你也不必将子设置为父链接手册.它只是有效.谢谢Kurt (3认同)
  • 谢谢!@JsonIdentityInfo适用于涉及多个重叠循环中的多个实体的循环引用. (2认同)

axt*_*avt 268

你可以@JsonIgnore用来打破这个循环.

  • 从Jackson 1.6开始,有一个更好的解决方案:你可以使用两个新的注释来解决无限递归问题,而不会在序列化过程中忽略getter/setter.请参阅下面的答案了解详情. (38认同)
  • 以上所有解决方案似乎都需要通过添加注释来更改域对象.如果我序列化第三方类,我无法修改它们.我该如何避免这个问题? (10认同)
  • 此解决方案在某些情况下不起作用。在使用jpa的关系数据库中,如果您添加`@ JsonIgnore`,则在更新实体时,“外键”将为空... (2认同)

小智 95

新注释@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<>();
   ....
}
Run Code Online (Sandbox Code Playgroud)

在这里查看.它就像在文档中一样工作:http:
//springquay.blogspot.com/2016/01/new-approach-to-solve-json-recursive.html


Mar*_*cus 44

另外,使用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 {
Run Code Online (Sandbox Code Playgroud)

它应该工作.

  • 这是迄今为止我们找到的最佳解决方案,因为当我们使用“@JsonManagedReference”时,get 方法成功返回了值,没有任何 stackoverflow 错误。但是,当我们尝试使用帖子保存数据时,它返回了 415 错误(不支持的媒体错误) (3认同)
  • 我已向我的实体添加了“@JsonIdentityInfo”注释,但它并没有解决递归问题。只有“@JsonBackReference”和“@JsonManagedReference”可以解决,但它们从 JSON 中删除了映射属性。 (3认同)

Sta*_*Man 19

此外,杰克逊1.6支持处理双向引用 ...这似乎是你正在寻找的(这篇博文也提到了这个功能)

截至2011年7月,还有" jackson-module-hibernate "可能在处理Hibernate对象的某些方面有所帮助,尽管不一定是特定的(需要注释).

  • 链接已失效,您介意更新它们还是编辑您的答案吗? (2认同)

Eug*_*sky 11

现在Jackson支持避免循环而不忽略字段:

杰克逊 - 具有双向关系的实体的序列化(避免周期)


小智 9

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

@ManyToOne
@JoinColumn(name = "ID", nullable = false, updatable = false)
@JsonIgnore
private Member member;
Run Code Online (Sandbox Code Playgroud)

  • 我认为`@JsonIgnore`忽略了这个属性被检索到客户端.如果我需要这个属性与它的孩子(如果它有孩子)怎么办? (2认同)

Sha*_*ane 6

现在有一个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>
Run Code Online (Sandbox Code Playgroud)

然后在初始化Jackson的ObjectMapper时注册模块:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Hibernate4Module());
Run Code Online (Sandbox Code Playgroud)

文档目前不是很好.有关可用选项,请参阅Hibernate4Module代码.

  • 这解决了一个完全不同的问题. (4认同)

Pra*_*u M 6

对我而言 工作正常与杰克逊一起工作时解决Json Infinite Recursion问题

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

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


@OneToMany(mappedBy="LgcyIsp ")
@JsonManagedReference
private List<Safety> safety;
Run Code Online (Sandbox Code Playgroud)


Lea*_*ira 6

非常重要:如果您使用 LOMBOK,请确保排除 Set、List 等集合的属性...

像这样:

@EqualsAndHashCode(exclude = {"attributeOfTypeList", "attributeOfTypeSet"})
Run Code Online (Sandbox Code Playgroud)


Kla*_*503 5

在我的情况下,改变关系就足够了:

@OneToMany(mappedBy = "county")
private List<Town> towns;
Run Code Online (Sandbox Code Playgroud)

到:

@OneToMany
private List<Town> towns;
Run Code Online (Sandbox Code Playgroud)

另一个关系保持原样:

@ManyToOne
@JoinColumn(name = "county_id")
private County county;
Run Code Online (Sandbox Code Playgroud)

  • 我认为最好使用 Kurt 的解决方案。因为 JoinColumn 解决方案可能以未引用的数据死体结束。 (3认同)

fab*_*ner 5

对我而言,最好的解决方案是@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;
    }
}
Run Code Online (Sandbox Code Playgroud)

以及尝试使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

如果运行此代码,您将获得:

org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)
Run Code Online (Sandbox Code Playgroud)

使用`@ JsonView`的解决方案

@JsonView使您可以使用过滤器并选择在序列化对象时应包括哪些字段。过滤器只是用作标识符的类引用。因此,让我们首先创建过滤器:

public class Filter {

    public static interface EmployeeData {};

    public static interface CompanyData extends EmployeeData {};

} 
Run Code Online (Sandbox Code Playgroud)

请记住,过滤器是伪类,仅用于通过@JsonView注释指定字段,因此您可以根据需要创建任意多个。让我们来看一下它的作用,但是首先我们需要注释我们的Company类:

public class Company {

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

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

    public Employee getEmployee() {
        return employee;
    }
}
Run Code Online (Sandbox Code Playgroud)

并更改“测试”以使序列化程序使用“视图”:

@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);
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果您运行此代码,则可以解决“无限递归”问题,因为您已明确表示只想序列化使用注释的属性@JsonView(Filter.CompanyData.class)

当到达中的company的后向引用时Employee,它将检查是否未注释并忽略序列化。您还具有强大而灵活的解决方案,可以选择要通过REST API发送的数据。

使用Spring,您可以使用所需的@JsonView过滤器注释REST Controllers方法,并将序列化透明地应用于返回的对象。

这是您需要检查的导入文件:

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;
Run Code Online (Sandbox Code Playgroud)


Ari*_*car 5

我也遇到了同样的问题。我用的@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 {
...
Run Code Online (Sandbox Code Playgroud)


Sum*_*tra 5

答案就是 @JsonIgnoreProperties

使用这样的东西::

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


小智 5

您应该将@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;
Run Code Online (Sandbox Code Playgroud)