当尝试将具有双向关联的JPA对象转换为JSON时,我不断得到
org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)
我所找到的是这个帖子,基本上是建议避免双向关联。有人有解决这个春季bug的方法吗?
------ edit 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>
JsonIgnoreProperties[2017年更新]:
您现在可以使用JsonIgnoreProperties来抑制属性的序列化(在序列化期间),或者忽略JSON属性读取的处理(在反序列化期间)。如果这不是你想要的,请继续阅读下面的内容。
(感谢As Zammel AlaaEddine指出这一点)。
JsonManagedReference和JsonBackReference
从Jackson 1.6开始,您可以使用@JsonManagedReference和@JsonBackReference这两个注释来解决无限递归问题,而不必在序列化期间忽略getter /setter。
解释
为了使Jackson能够正常工作,关系的两个方面之一不应该被序列化,以避免导致stackoverflow错误的无限循环。
因此,Jackson取引用的前部分(您的Set<BodyStat> bodyStats in练习生类),并将其转换为类似json的存储格式;这就是所谓的编组过程。然后,Jackson查找引用的后面部分(即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;
业务对象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响应,看看代码中发生了什么。
来源:
Keenformatics如何解决JSON无限递归Stackoverflow(我的博客)
杰克逊的引用
个人经验
我有这个问题,但我不想在我的实体中使用注释,所以我通过为我的类创建一个构造函数来解决,这个构造函数必须没有对引用这个实体的实体的引用。假设这种情况。
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发送类B或A到视图,可能会导致一个无限循环。您可以在类中编写构造函数,并像这样使用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;
}
}
然而,这个解决方案有一些限制,正如你可以看到的,在构造函数中我没有引用List bs,这是因为Hibernate不允许它,至少在3.6.10版本中。最后,所以当我需要在一个视图中显示两个实体时,我做以下工作。
public A getAById(int id); //THE A id
public List<B> getBsByAId(int idA); //the A id.
这个解决方案的另一个问题是,如果您添加或删除一个属性,您必须更新构造函数和所有查询。
对我来说,最好的解决方案是使用@JsonView,并为每个场景创建特定的过滤器。你也可以使用@JsonManagedReference和@JsonBackReference,但是这是一种硬编码的解决方案,只针对一种情况,即所有者总是引用拥有方,而不是相反。如果您有另一个序列化场景,需要以不同的方式重新注释属性,则不能这样做。
问题
让我们使用两个类,Company和Employee,它们之间有循环依赖关系:
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;
}
}
并更改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;