jacksonJSON和Hibernate JPA问题的无限recursion

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

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

我发现的是这个线程 ,基本上推荐避免双向关联。 有没有人有一个解决方法这个春季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 ... 

业务对象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> 

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

由于Jackson 1.6可以使用两个注释来解决无限recursion问题,而不会在序列化过程中忽略getter / setter: @JsonManagedReference@JsonBackReference

说明

对于jackson的工作,双方的关系不应该序列化,以避免导致您的计算器错误的infite循环。

所以,Jackson把引用的前面部分(你的Set<BodyStat> bodyStats在Trainee类中),并以类似json的存储格式进行转换; 这就是所谓的编组过程。 然后,jackson查找引用的后面部分(即BodyStat类中的Trainee trainee )并保持原样,而不是序列化它。 这部分关系将在前向引用的反序列化( 解组 )过程中重新构build。

你可以像这样改变你的代码(我跳过了无用的部分):

业务对象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和Jackson Stackoverflow问题的文章。

编辑:

你可以检查的另一个有用的注解是@JsonIdentityInfo :使用它,每次Jackson序列化你的对象,它会添加一个ID(或者你select的另一个属性)给它,这样它就不会每次都“扫描”它。 当你有更多的相互关联的对象(例如:Order – > OrderLine – > User – > Order)之间的链式循环时,这会很有用。

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

资料来源:

  • Keenformatics – 如何解决JSON无限recursionStackoverflow (我的博客)
  • jackson参考
  • 个人经验

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

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

它应该工作。

此外,jackson1.6支持处理双向引用 …这似乎是你在找什么( 这个博客条目也提到了这个function)

截至2011年7月,还有“ jackson-module-hibernate ”,这可能有助于处理Hibernate对象的某些方面,尽pipe不一定需要注释。

现在jackson支持避免周期而不忽略字段:

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

这对我来说工作得很好。 在提到对父类的引用的子类上添加注释@JsonIgnore。

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

现在有一个Jackson模块(用于Jackson 2)专门devise来处理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代码以获取可用选项。

就我而言,这足以改变以下关系:

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

至:

 @OneToMany private List<Town> towns; 

另一个关系保持原样:

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

对我来说最好的解决scheme是使用@JsonView并为每个场景创build特定的filter。 你也可以使用@JsonManagedReference@JsonBackReference ,但是它只是一个硬编码的解决scheme,只有一种情况,所有者总是引用拥有方,而不是相反。 如果您有另一个序列化场景,您需要以不同的方式重新注释该属性,那么您将无法做到这一点。

问题

让我们使用两个类, 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 )序列化的testing类:

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

解决scheme使用`@ JsonView`

@JsonView可以使用filter,并在序列化对象时select包含哪些字段。 filter只是一个用作标识符的类引用。 所以我们先来创buildfilter:

 public class Filter { public static interface EmployeeData {}; public static interface CompanyData extends EmployeeData {}; } 

请记住,filter是虚拟类,只是用@JsonView注释来指定字段,所以你可以创build尽可能多的你想要和需要的。 让我们看看它的行动,但首先我们需要注释我们的Company类:

 public class Company { @JsonView(Filter.CompanyData.class) private Employee employee; public Company(Employee employee) { this.employee = employee; } public Employee getEmployee() { return employee; } } 

并更改testing为了序列化程序使用视图:

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

现在如果你运行这个代码,那么无限recursion问题就解决了,因为你已经明确地说过你只想序列化用@JsonView(Filter.CompanyData.class)注解的属性。

当它到达Employee公司的后台引用时,它会检查是否没有注释并忽略序列化。 您还拥有一个function强大且灵活的解决scheme,可以select要通过REST API发送哪些数据。

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

以下是您需要检查的情况下使用的import产品:

 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; 

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

你可以使用DTO模式创build类TraineeDTO没有任何hiberbnate任何注释,你可以使用jackson映射器将学员转换为TraineeDTO和宾果错误消息disapeare 🙂

我有这个问题,但我不想在我的实体中使用注释,所以我通过为我的类创build一个构造函数来解决,这个构造函数不能有一个引用返回到引用这个实体的实体。 我们来说这个场景。

 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创build一个查询。

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

但是,这个解决scheme有一些限制,你可以看到,在构造函数中,我没有引用List bs,这是因为Hibernate不允许它,至less在3.6.10.Final版本中 ,所以当我需要在视图中显示两个实体我做了以下。

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

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

如果您使用的是Spring Data Rest,则可以通过为涉及循环引用的每个实体创buildRepositories来解决问题。