双方向の関連を持つJPAオブジェクトをJSONに変換しようとすると、
org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)
私が見つけたのは このスレッド だけです。誰もがこの春のバグに対する回避策のアイデアを持っていますか?
------編集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;
}
}
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
を使ってこのサイクルを止めることができます。
JsonIgnoreProperties を使用して、プロパティの直列化を抑制したり(直列化中)、読み取ったJSONプロパティの処理を無視することができます(直列化解除中)。これがあなたが探しているものではないならば、以下を読んでください。
(これを指摘してくれたAs Zammel AlaaEddineに感謝します)。
Jackson 1.6以降、シリアライゼーション中にゲッター/セッターを無視せずに無限再帰問題を解決するために2つのアノテーションを使うことができます: @JsonManagedReference
と @JsonBackReference
。
説明
Jacksonがうまく機能するためには、スタックオーバーフローエラーの原因となる無限ループを回避するために、関係の両側の一方をシリアライズしないでください。
そのため、Jacksonは参照の前半部分(Traineeクラスの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とJackson Stackoverflowの問題に関する記事をKeenformatics に書いています。
編集:
あなたがチェックすることができるもう一つの役に立つアノテーションは @JsonIdentityInfo :それを使うと、Jacksonがあなたのオブジェクトを直列化する度に、それはそれにID(あるいはあなたの選んだ別の属性)を加えるでしょう。また毎回。これは、相互に関連のあるオブジェクト間でチェーンループが発生した場合に便利です(たとえば、Order - > OrderLine - > User - > Orderなど)。
この場合、オブジェクトの属性を複数回読み取る必要がある可能性があるため(たとえば、同じ販売者を共有する複数の製品を含む製品リストなど)、このアノテーションを使用することはできません。私はいつもfirebugのログを見てJsonの反応をチェックし、あなたのコードで何が起こっているのかを見ることをお勧めします。
出典:
新しい注釈@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はフィールドを無視せずにサイクルを回避することをサポートしています。
これは私にとっては完全にうまくいきました。親クラスへの参照が言及されている子クラスに注釈@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コード を参照してください。
私にとって最良の解決策は@JsonView
を使用してシナリオごとに特定のフィルタを作成することです。 @JsonManagedReference
および@JsonBackReference
を使用することもできますが、所有者が常に所有側を参照し、その逆を絶対に行わないという、1つだけの状況に対するハードコーディングされた解決策です。別のシリアル化シナリオがあり、その属性に異なるアノテーションを付ける必要がある場合は、できません。
Company
とEmployee
の2つのクラスを使いましょう。
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
を使用すると、フィルタを使用して、オブジェクトをシリアル化するときに含めるフィールドを選択できます。フィルタは、識別子として使用される単なるクラス参照です。それでは、まずフィルタを作成しましょう。
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);
}
}
これで、このコードを実行するとInfinite Recursionの問題は解決されました。これは、@JsonView(Filter.CompanyData.class)
のアノテーションが付けられた属性をシリアライズしたいだけであると明示的に言ったからです。
それがEmployee
でcompanyの後方参照に達すると、注釈が付けられていないことを確認し、シリアル化を無視します。 REST APIを介して送信するデータを選択するための強力で柔軟なソリューションもあります。
Springでは、REST Controllersメソッドに希望の@JsonView
フィルタを付けてアノテーションを付けることができ、シリアル化は返されるオブジェクトに透過的に適用されます。
確認が必要な場合に使用するインポートは次のとおりです。
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;
私のためにうまく動く Jacksonと仕事をするときにJson Infinite Recursion問題を解決する
これが私がoneToManyとManyToOneのマッピングで行ったことです
@ManyToOne
@JoinColumn(name="Key")
@JsonBackReference
private LgcyIsp Key;
@OneToMany(mappedBy="LgcyIsp ")
@JsonManagedReference
private List<Safety> safety;
@ JsonIgnorePropertiesがその答えです。
このようなものを使う:
@OneToMany(mappedBy = "course",fetch=FetchType.EAGER)
@JsonIgnoreProperties("course")
private Set<Student> students;
必ずどこでも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);
私の場合は、関係を次のように変更すれば十分です。
@OneToMany(mappedBy = "county")
private List<Town> towns;
に:
@OneToMany
private List<Town> towns;
別の関係はそのままでした:
@ManyToOne
@JoinColumn(name = "county_id")
private County county;
@ JsonIgnoreを使用できますが、これは外部キー関係のためにアクセスできるjsonデータを無視します。したがって、外部キーデータが必要な場合(ほとんどの場合必要)、@ JsonIgnoreは役に立ちません。そのような状況では、以下の解決策に従ってください。
BodyStatクラスが再び研修生を参照しているので、無限再帰を取得していますオブジェクト
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;
そのため、研修生の中で上記の部分をコメント/省略する必要があります。
私も同じ問題に出会った。私は@JsonIdentityInfo
のObjectIdGenerators.PropertyGenerator.class
ジェネレータ型を使いました。
それが私の解決策です:
@Entity
@Table(name = "ta_trainee", uniqueConstraints = {@UniqueConstraint(columnNames = {"id"})})
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Trainee extends BusinessObject {
...
dTOパターンを使用してアノテーションを休止状態にせずにTrainee DTOクラスを作成することができます。また、Jackson Mapperを使用してTraineeをTraineeDTOに変換し、エラーメッセージdisingoareをビンゴできます。
@JsonBackReferenceは@ManyToOneエンティティで使用し、@ JsonManagedReferenceは@onetomanyを含むエンティティクラスで使用する必要があります。
@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;
Spring Data Restを使用している場合は、循環参照に関与するすべてのエンティティに対してリポジトリを作成することで問題を解決できます。
このプロパティを無視できない場合は、フィールドの表示設定を変更してみてください。私たちの場合は、古いコードでまだ関係を持つエンティティを送信していたので、私の場合はこれが修正方法です。
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private Trainee trainee;
私はこの問題を抱えていましたが、私のエンティティでアノテーションを使いたくなかったので、私は自分のクラスのコンストラクタを作成することによって解決しました。このシナリオを言いましょう。
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を参照していないため、いくつかの制限があります。 inバージョン3.6.10.Finalなので、ビューに両方のエンティティを表示する必要があるときは、次のようにします。
public A getAById(int id); //THE A id
public List<B> getBsByAId(int idA); //the A id.
この解決策のもう一つの問題は、プロパティを追加または削除した場合、コンストラクタとすべてのクエリを更新しなければならないことです。