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

gdrx4gfi  于 2022-09-19  发布在  Spring
关注(0)|答案(28)|浏览(257)

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

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

我所发现的是this thread,其基本结论是建议避免双向关联。有人有解决这个spring bug的方法吗?
------编辑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>
6jygbczu

6jygbczu1#

请确保使用**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);
2sbarzqh

2sbarzqh2#

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

nzkunb0c

nzkunb0c3#

要点是将**@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;
uplii1fm

uplii1fm4#

我是一个迟到的人,这已经是一条很长的线索了。但我也花了几个小时试图弄清楚这一点,我想再举一个例子。
我尝试了JsonIgnore、JSONignoReperties和反向引用解决方案,但奇怪的是,它们好像没有被选中。
我使用了Lombok,并认为它可能会干扰,因为它会创建构造函数并覆盖toString(在StackOverflowerError堆栈中看到toString)。
最后,这不是Lombok的错-我使用了从数据库表自动生成JPA实体的NetBeans,没有考虑太多-好吧,添加到生成类中的注解之一是@XmlRootElement。一旦我把它取下来,一切都开始工作了。嗯。

vi4fp9gy

vi4fp9gy5#

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

ycl3bljg

ycl3bljg6#

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

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.

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

zwghvu4y

zwghvu4y7#

这篇文章:https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion有一个完整的解释。
如果您使用的是旧版本的Jackson,可以尝试@jsonmanagedreference+@jsonbackreference。如果您的Jackson高于2(据我所知,1.9也不起作用),请尝试@JsonIdentityInfo。

weylhg0b

weylhg0b8#

如果使用@JsonManagedReference@JsonBackReference@JsonIgnore注解,它会忽略一些字段,并使用Jackson JSON解决无限递归。
但是如果您使用@JsonIdentityInfo,这也避免了无限递归,您可以获得所有字段值,因此我建议您使用@JsonIdentityInfo注解。

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

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

owfi6suc

owfi6suc9#

出于某种原因,在我的情况下,它不适用于Set。我不得不将其更改为List并使用@JsonIgnore和@ToString。排除以使其工作。
用列表替换集合:

//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;
lstz6jyr

lstz6jyr10#

如果无法忽略属性,请尝试修改字段的可见性。在我们的例子中,旧代码仍然提交具有关系的实体,因此在我的例子中这是修复:

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

enyaitl311#

您可以使用DTO模式创建类TraineeDTO,而不使用任何选项hiberbnate,您可以使用jacksonMap器将受训者转换为TraineeDTO,然后返回错误消息disapeare:)

oxf4rvwz

oxf4rvwz12#

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

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

ukxgm1gy13#

**非常重要:*如果您使用LOMBOK,请使用舒尔排除集合属性,如集合、列表等。。。

这样地:

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

djp7away14#

您可以使用**@JsonIgnore**,但这将忽略由于外键关系而可以访问的json数据。因此,如果您需要外键数据(大多数情况下我们需要),那么**@JsonIgnore将无法帮助您。在这种情况下,请遵循以下解决方案。
由于
BodyStat类再次引用Trainee**对象,您将得到无限递归

身体统计

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

因此,您必须在受训人员中注解/省略上述部分

of1yzvn4

of1yzvn415#

JSONIGNOREPORTIES〔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;

现在一切都应该正常工作了。
如果您想了解更多信息,我在我的博客上写了一篇关于Json and Jackson Stackoverflow issues on Keenformatics的文章。

编辑:

您可以检查的另一个有用的注解是@JsonIdentityInfo:使用它,每次Jackson序列化对象时,它都会向其添加一个ID(或您选择的其他属性),这样它就不会每次都完全“扫描”它。当您在多个相互关联的对象之间有一个链循环时,这可能很有用(例如:订单->订单行->用户->订单,然后重复)。
在这种情况下,您必须小心,因为您可能需要多次读取对象的属性(例如,在产品列表中,多个产品共享同一卖家),并且此注解阻止您这样做。我建议始终查看firebug日志,以检查Json响应,并查看代码中发生了什么。

资料来源:

相关问题