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,只有一种情况,所有者总是引用拥有方,而不是相反。 如果您有另一个序列化场景,您需要以不同的方式重新注释该属性,那么您将无法做到这一点。
问题
让我们使用两个类, 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 )序列化的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,您可以使用所需的@JsonView
filter注释您的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
发送到视图类B
或A
,则可能会导致无限循环。 你可以在你的类中写一个构造函数,像这样用你的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来解决问题。
- 使用Jackson将JSON反序列化为ArrayList <POJO>
- 如何使用Gradle将Boon或Jackson JSONparsing器添加到Android Studio?
- date格式映射到JSON Jackson
- 我应该如何在我的RESTful JAX-RS Web服务中logging未捕获的exception?
- 如何在jacksonparsing一个JSONstring到JsonNode?
- 不能从START_OBJECT标记反序列化java.util.ArrayList的实例
- 在Spring中编写JSON解串器或者扩展它的正确方法
- jackson禁用fail_on_empty_beans
- Jackson ObjectMapper – 指定对象属性的序列化顺序