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

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

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

我所找到的是这个线程,它基本上建议避免双向关联。有人有解决这个 spring bug 的方法吗?

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


@OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Column(nullable = true)
private Set trainings;


@OneToMany(mappedBy = "trainee", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Column(nullable = true)
private Set 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 trainees = new ConcurrentHashMap();


@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



false








410933 次浏览

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

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

另外,Jackson 1.6支持处理双向引用…看起来就像 你在寻找什么(这个博客条目也提到了该功能)

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

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

Jackson -具有双向关系的实体的序列化(避免循环)

JsonIgnoreProperties[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;

现在一切都应该正常工作了。

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

编辑:

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

在这种情况下,您必须小心,因为您可能需要多次读取对象的属性(例如,在具有多个共享同一卖家的产品列表中),而该注释阻止了您这样做。我建议经常查看firebug日志,检查Json响应,看看代码中发生了什么。

来源:

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

它应该会起作用。

在我的情况下,这足以改变关系:

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

:

@OneToMany
private List<Town> towns;

另一种关系保持不变:

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

现在有一个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代码

这对我来说非常好。 在提到父类引用的子类上添加注释@JsonIgnore。< / p >
@ManyToOne
@JoinColumn(name = "ID", nullable = false, updatable = false)
@JsonIgnore
private Member member;

你可以使用DTO模式 创建类TraineeDTO没有任何注释hibernate和你可以使用杰克逊映射器转换练习生到TraineeDTO和宾o错误消息消失:)

我有这个问题,但我不想在我的实体中使用注释,所以我通过为我的类创建一个构造函数来解决,这个构造函数必须没有对引用这个实体的实体的引用。假设这种情况。

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

如果你试图用@ResponseBody向视图发送类BA,可能会导致无限循环。你可以在你的类中写一个构造函数,并像这样用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不允许它,至少在版本3.6.10.Final中是这样,所以当我需要在一个视图中显示两个实体时,我执行以下操作。

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


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

这个解决方案的另一个问题是,如果您添加或删除一个属性,您必须更新构造函数和所有查询。

在使用Spring Data Rest的情况下,可以通过为循环引用中涉及的每个实体创建存储库来解决这个问题。

确保你在任何地方都使用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);

新的注释@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 < / p >

对我来说,最好的解决方案是使用@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 (春天的引导)序列化的测试类:

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

并更改Test以便序列化器使用视图:

@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中company的反向引用时,它检查它是否没有注释并忽略序列化。您还有一个强大而灵活的解决方案来选择希望通过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;

我也遇到了同样的问题。我使用了@JsonIdentityInfoObjectIdGenerators.PropertyGenerator.class生成器类型。

这就是我的解决方案:

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

你可以使用@JsonIgnore,但这将忽略由于外键关系而可以访问的json数据。因此,如果你需要外键数据(大多数时候我们需要),那么@JsonIgnore将无法帮助你。在这种情况下,请遵循以下解决方案。

你将得到无限递归,因为BodyStat类再次引用了实习对象

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;

因此,你必须在实习中注释/省略上述部分

< em > @JsonIgnoreProperties < / em >是答案。

使用如下代码::

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

对我来说很好 解决Json无限递归问题时,与杰克逊 < / p >

这是我在多对多和多对一映射中所做的

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




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

如果不能忽略该属性,请尝试修改字段的可见性。在我们的情况下,我们有旧的代码仍然提交实体与关系,所以在我的情况下,这是修复:

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

你应该使用@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;

我来晚了,而且已经有这么长一段了。但我也花了几个小时试图弄清楚这一点,我想把我的情况作为另一个例子。

我尝试了JsonIgnore, JsonIgnoreProperties和BackReference解决方案,但奇怪的是,它们好像没有被选中。

我使用Lombok,并认为它可能会干扰,因为它创建构造函数并覆盖toString(在stackoverflowerror堆栈中看到toString)。

最后,这不是Lombok的错——我使用了从数据库表自动生成JPA实体的NetBeans,没有考虑太多——而且,添加到生成的类中的注释之一是@XmlRootElement。一旦我把它取出来,一切都开始工作了。哦。

重点是将@JsonIgnore放在setter方法中,如下所示。对我来说。

Township.java

@Access(AccessType.PROPERTY)
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name="townshipId", nullable=false ,insertable=false, updatable=false)
public List<Village> getVillages() {
return villages;
}


@JsonIgnore
@Access(AccessType.PROPERTY)
public void setVillages(List<Village> villages) {
this.villages = villages;
}

Village.java

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "townshipId", insertable=false, updatable=false)
Township township;


@Column(name = "townshipId", nullable=false)
Long townshipId;

出于某种原因,在我的情况下,它不能与赛特一起工作。我必须将其更改为List并使用@JsonIgnore和@ToString。排除让它工作。

用List替换Set:

//before
@OneToMany(mappedBy="client")
private Set<address> addressess;


//after
@OneToMany(mappedBy="client")
private List<address> addressess;

并添加@JsonIgnore和@ToString。排除注释:

@ManyToOne
@JoinColumn(name="client_id", nullable = false)
@JsonIgnore
@ToString.Exclude
private Client client;

在做了更多的分析后,我也有同样的问题,我知道,我们也可以通过在OneToMany注释中保留@JsonBackReference来获得映射实体

@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)
@JsonBackReference
private Set<BodyStat> bodyStats;

非常重要:如果你正在使用LOMBOK,使shure排除集合的属性,如Set, List等…

是这样的:

@EqualsAndHashCode(exclude = {"attributeOfTypeList", "attributeOfTypeSet"})

我也遇到过同样的问题,添加jsonbackref和jsonmanagedref,请确保@override equals和hashCode方法,这肯定会修复这个问题。

如果你使用@JsonManagedReference@JsonBackReference@JsonIgnore注释,它会忽略一些字段,并使用Jackson JSON解决无限递归。

但如果你使用@JsonIdentityInfo,这也避免了无限递归,你可以得到所有的字段值,所以我建议你使用@JsonIdentityInfo注释。

@JsonIdentityInfo(generator= ObjectIdGenerators.UUIDGenerator.class, property="@id")

请参阅本文https://www.toptal.com/javascript/bidirectional-relationship-in-json以更好地理解@JsonIdentityInfo注释。

这篇文章:https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion

如果您正在使用旧版本的Jackson,可以尝试@jsonmanagedreference + @jsonbackreference。如果你的Jackson值超过2(1.9也不行,我知道),试试@JsonIdentityInfo。