Programming

Jackson JSON 및 Hibernate JPA 문제로 무한 재귀

procodes 2020. 2. 27. 22:30
반응형

Jackson JSON 및 Hibernate JPA 문제로 무한 재귀


양방향 연관이있는 JPA 객체를 JSON으로 변환하려고 할 때 계속

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

모든 I의 FOUND는 이 스레드 기본적으로 양방향 연결을 방지하기 위해 추천으로 결론. 이 봄 버그에 대한 해결 방법에 대한 아이디어가 있습니까?

------ 편집 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>

@JsonIgnore사이클을 중단하는 데 사용할 수 있습니다 .


JsonIgnoreProperties [2017 업데이트] :

이제 JsonIgnoreProperties사용 하여 특성 직렬화억제하거나 (직렬화 중) JSON 특성 읽기 처리 (직렬화 해제 중)를 무시할 수 있습니다. 원하는 것이 아닌 경우 아래를 계속 읽으십시오.

(이 점을 지적한 As Zammel AlaaEddine에게 감사합니다).


JsonManagedReference 및 JsonBackReference

잭슨 1.6 이후 당신은 직렬화하는 동안 게터 / 세터를 무시하지 않고 무한 재귀 문제를 해결하기 위해 두 개의 주석을 사용할 수 있습니다 @JsonManagedReference@JsonBackReference.

설명

Jackson이 제대로 작동하려면 스택 오버플로 오류를 일으키는 인피 트 루프를 피하기 위해 관계의 양면 중 하나를 직렬화하면 안됩니다.

따라서 Jackson은 참조의 앞 부분 ( Set<BodyStat> bodyStatsTrainee 클래스)을 가져 와서 json과 같은 저장 형식으로 변환합니다. 이것이 소위 마샬링 프로세스입니다. 그런 다음 Jackson은 참조의 뒷면 부분 (예 : Trainee traineeBodyStat 클래스)을 찾아 직렬화하지 않고 그대로 둡니다. 관계의이 부분은 순방향 참조 의 역 직렬화 ( 비 정렬 화 ) 중에 재구성됩니다 .

다음과 같이 코드를 변경할 수 있습니다 (쓸모없는 부분은 건너 뜁니다).

사업 목표 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;

이제 모두 제대로 작동합니다.

더 많은 정보를 원한다면 내 블로그 인 KeenformaticsJson and Jackson Stackoverflow 문제에 관한 기사를 썼습니다 .

편집하다:

확인할 수있는 또 다른 유용한 주석은 @JsonIdentityInfo입니다 . Jackson을 사용하여 객체를 직렬화 할 때마다 ID (또는 선택한 다른 속성)가 추가되어 매번 다시 완전히 "스캔"되지 않습니다. 이것은 더 많은 상호 관련 된 객체 (예 : 주문-> 주문 라인-> 사용자-> 주문 이상) 사이에 체인 루프가있는 경우 유용 할 수 있습니다.

이 경우 객체 속성을 두 번 이상 읽어야하므로 (예 : 동일한 판매자를 공유하는 더 많은 제품이있는 제품 목록에서)이 주석을 사용하면 그렇게 할 수 없으므로주의해야합니다. Json 응답을 확인하고 코드에서 무슨 일이 일어나고 있는지 확인하려면 항상 Firebug 로그를 살펴 보는 것이 좋습니다.

출처 :


새로운 주석 @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


또한 Jackson 2.0 이상을 사용하면을 사용할 수 있습니다 @JsonIdentityInfo. 이것은 @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 {

작동해야합니다.


또한 Jackson 1.6은 양방향 참조 처리 를 지원합니다 ... 원하는 것 같습니다 ( 이 블로그 항목 에도 기능이 언급되어 있습니다)

그리고 2011 년 7 월 현재, " jackson-module-hibernate "도 있는데,이 특성은 반드시 주석이 필요한 특정 오브젝트는 아니지만 Hibernate 오브젝트 처리의 일부 측면에서 도움이 될 수 있습니다.


이제 Jackson은 필드를 무시하지 않고 사이클을 피할 수 있습니다.

Jackson-쌍방향 관계를 가진 엔티티의 직렬화 (주기를 피함)


이것은 나를 위해 완벽하게 작동했습니다. 부모 클래스에 대한 참조를 언급하는 자식 클래스에 주석 @JsonIgnore를 추가하십시오.

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

직렬화 할 때 Hibernate 지연 초기화 문제를 처리하도록 특별히 설계된 Jackson 모듈 (Jackson 2 용)이 있습니다.

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 코드참조하십시오 .


나를 위해 작동 벌금 잭슨과 함께 작업 할 때 JSON 무한 재귀 문제 해결

이것이 oneToMany 및 ManyToOne 매핑에서 수행 한 작업입니다.

@ManyToOne
@JoinColumn(name="Key")
@JsonBackReference
private LgcyIsp Key;


@OneToMany(mappedBy="LgcyIsp ")
@JsonManagedReference
private List<Safety> safety;

나에게 가장 좋은 해결책은 @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;
    }
}

serializer가 View를 사용하도록 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주석이 없는지 확인하고 직렬화를 무시합니다. 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;

제 경우에는 다음과 같은 관계를 변경하기에 충분했습니다.

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

에:

@OneToMany
private List<Town> towns;

다른 관계는 그대로 유지되었습니다.

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

어디에서나 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);

@JsonIgnoreProperties 가 답입니다.

이런 식으로 사용하십시오 ::

@OneToMany(mappedBy = "course",fetch=FetchType.EAGER)
@JsonIgnoreProperties("course")
private Set<Student> students;

@JsonIgnore 를 사용할 수 있지만 외래 키 관계로 인해 액세스 할 수있는 json 데이터는 무시됩니다. 따라서 외래 키 데이터를 필요로하는 경우 (대부분 필요한 경우) @JsonIgnore 는 도움이되지 않습니다. 이러한 상황에서는 아래 해결책을 따르십시오.

BodyStat 클래스가 다시 Trainee 객체를 참조 하기 때문에 무한 재귀를 얻습니다.

BodyStat

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

따라서 연수생 에게 위의 부분을 언급 / 생략해야합니다.


DTO 패턴을 사용하여 주석 최대 절전 모드없이 클래스 TraineeDTO를 만들 수 있으며 Jackson Jackson을 사용하여 Trainee를 TraineeDTO로 변환하고 오류 메시지 disapeare를 빙고 할 수 있습니다 :)


나는 또한 같은 문제를 만났다. 내가 사용 @JsonIdentityInfoObjectIdGenerators.PropertyGenerator.class발전기 유형입니다.

그게 내 해결책입니다.

@Entity
@Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})})
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Trainee extends BusinessObject {
...

@ManyToOne 엔티티에는 @JsonBackReference를, 엔티티 클래스를 포함하는 @onetomany에는 @JsonManagedReference를 사용해야합니다.

@OneToMany(
            mappedBy = "queue_group",fetch = FetchType.LAZY,
            cascade = CascadeType.ALL
        )
    @JsonManagedReference
    private Set<Queue> queues;



@ManyToOne(cascade=CascadeType.ALL)
        @JoinColumn(name = "qid")
       // @JsonIgnore
        @JsonBackReference
        private Queue_group queue_group;

이 문제가 있었지만 엔티티에 주석을 사용하고 싶지 않았으므로 클래스의 생성자를 작성하여 해결했습니다.이 생성자는이 엔티티를 참조하는 엔티티에 대한 참조를 가져서는 안됩니다. 이 시나리오를 가정 해 봅시다.

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

당신이보기에 클래스 보내려고하면 BA@ResponseBody그것을 무한 루프가 발생할 수 있습니다. 클래스에서 생성자를 작성하고 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.

이 솔루션의 다른 문제는 속성을 추가하거나 제거하면 생성자와 모든 쿼리를 업데이트해야한다는 것입니다.


Spring Data Rest를 사용하는 경우 주기적 참조와 관련된 모든 엔티티에 대해 저장소를 작성하여 문제를 해결할 수 있습니다.


속성을 무시할 수 없으면 필드의 가시성을 수정하십시오. 우리의 경우에는 오래된 코드가 여전히 관계가있는 엔티티를 제출하므로 내 경우에는 이것이 수정되었습니다.

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

참고 URL : https://stackoverflow.com/questions/3325387/infinite-recursion-with-jackson-json-and-hibernate-jpa-issue



반응형