当尝试将具有双向关联的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>

当前回答

对我来说,最好的解决方案是使用@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;

其他回答

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

现在有一个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, JsonIgnoreProperties和BackReference解决方案,但奇怪的是,它们好像没有被选中。

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

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

对我来说,最好的解决方案是使用@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;

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