Lombok利用時のStackOverflow - toStringメソッド

Lombokはとても便利ですよね。Javaで必要だけどあまり書きたくないGetterやSetter、toStringメソッドなどを自動的に生成してくれるので、私は積極的に利用しています。ただ、使っていく過程ではまることもあるのですが、最初の頃にはまった内容をまとめます。

LombokでStackOverflow

Lombokを使用していると、処理中に例外が発生した場合など下記のような循環参照でStackOverflowとなることがあります。

 at java.lang.StringBuilder.append(StringBuilder.java:131) ~[na:1.8.0_60]
    at net.beaglesoft.girafa.domain.YayoiShohizeiKubun.toString(YayoiShohizeiKubun.java:23) ~[classes/:na]
    at java.lang.String.valueOf(String.java:2994) ~[na:1.8.0_60]
    at java.lang.StringBuilder.append(StringBuilder.java:131) ~[na:1.8.0_60]
    at net.beaglesoft.girafa.domain.YayoiKaikeiShohizeiKubunConf.toString(YayoiKaikeiShohizeiKubunConf.java:21) ~[classes/:na]
    at java.lang.String.valueOf(String.java:2994) ~[na:1.8.0_60]
    at java.lang.StringBuilder.append(StringBuilder.java:131) ~[na:1.8.0_60]
    at java.util.AbstractCollection.toString(AbstractCollection.java:462) ~[na:1.8.0_60]
    at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:527) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final]
    at java.lang.String.valueOf(String.java:2994) ~[na:1.8.0_60]
    at java.lang.StringBuilder.append(StringBuilder.java:131) ~[na:1.8.0_60]
    ...
    at java.lang.StringBuilder.append(StringBuilder.java:131) ~[na:1.8.0_60]
    at java.util.AbstractCollection.toString(AbstractCollection.java:462) ~[na:1.8.0_60]
2015-11-13 20:10:44.823 DEBUG [http-nio-8080-exec-1] o.s.s.w.c.SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed 
2015-11-13 20:10:44.823 DEBUG [http-nio-8080-exec-1] o.s.d.r.core.RedisConnectionUtils - Opening RedisConnection 
2015-11-13 20:10:44.823 DEBUG [http-nio-8080-exec-1] o.s.d.r.core.RedisConnectionUtils - Closing Redis Connection 
2015-11-13 20:10:44.824 DEBUG [http-nio-8080-exec-1] o.s.d.r.core.RedisConnectionUtils - Opening RedisConnection 
2015-11-13 20:10:44.824 DEBUG [http-nio-8080-exec-1] o.s.d.r.core.RedisConnectionUtils - Closing Redis Connection 
2015-11-13 20:10:44.824 DEBUG [http-nio-8080-exec-1] o.s.d.r.core.RedisConnectionUtils - Opening RedisConnection 
2015-11-13 20:10:44.824 DEBUG [http-nio-8080-exec-1] o.s.d.r.core.RedisConnectionUtils - Closing Redis Connection 
2015-11-13 20:10:44.824 DEBUG [http-nio-8080-exec-1] o.s.d.r.core.RedisConnectionUtils - Opening RedisConnection 
2015-11-13 20:10:44.824 DEBUG [http-nio-8080-exec-1] o.s.d.r.core.RedisConnectionUtils - Closing Redis Connection 
2015-11-13 20:10:44.824 DEBUG [http-nio-8080-exec-1] o.s.d.r.core.RedisConnectionUtils - Opening RedisConnection 
2015-11-13 20:10:44.824 DEBUG [http-nio-8080-exec-1] o.s.d.r.core.RedisConnectionUtils - Closing Redis Connection 
2015-11-13 20:10:44.825 ERROR [http-nio-8080-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler processing failed; nested exception is java.lang.StackOverflowError] with root cause 
java.lang.StackOverflowError: null
    at java.lang.Integer.toString(Integer.java:400) ~[na:1.8.0_60]
    at java.sql.Timestamp.toString(Timestamp.java:312) ~[na:1.8.0_60]
    at java.lang.String.valueOf(String.java:2994) ~[na:1.8.0_60]
    at java.lang.StringBuilder.append(StringBuilder.java:131) ~[na:1.8.0_60]
    at net.beaglesoft.girafa.domain.user.User.toString(User.java:64) ~[classes/:na]
    at java.lang.String.valueOf(String.java:2994) ~[na:1.8.0_60]
    at java.lang.StringBuilder.append(StringBuilder.java:131) ~[na:1.8.0_60]
    at net.beaglesoft.girafa.domain.YayoiShohizeiKubun.toString(YayoiShohizeiKubun.java:23) ~[classes/:na]
    at java.lang.String.valueOf(String.java:2994) ~[na:1.8.0_60]
    ...

発生する原因

これは、オブジェクトの循環が原因で発生します。理屈としては以下のケースで発生します。

  1. 例外が発生時に例外メッセージでtoStringメソッドが呼ばれたときに、該当するオブジェクトに循環したオブジェクトが存在する。
  2. LombokでtoStringメソッドを自動生成している。

で、具体的にはというと以下のようなケースが該当します。

まず、UserクラスにはGroupという関連が定義されています。

@Data
@NoArgsConstructor
@Entity
@Table(name = "users")
@JsonIgnoreProperties(value = {"handler", "hibernateLazyInitializer"})
public class User implements Serializable {
    private Group group;
}

つぎに、GroupクラスにはUserクラスの参照を更新ユーザーとして定義しています。

@Data
@NoArgsConstructor
@Entity
@Table(name = "users")
@JsonIgnoreProperties(value = {"handler", "hibernateLazyInitializer"})
public class Group implements Serializable {
    ...
    private User updateUser;
}

このときUserクラスで例外が発生するとします。例外の中ではtoStringメソッドが呼ばれることもありますが、今回は呼ばれるとします。

UserクラスのtoStringメソッドを実行すると、Lombokが生成したtoStringメソッドではGroupクラスのtoStringメソッドが実行され流ことになります。

すると、GroupクラスのtoStringメソッド内で定義されているupdateUser変数がtoStringメソッドで呼ばれます。このときにはさらにUserクラスのtoStringメソッドを実行することになり次にUserクラスの中にあるGroupクラスのtoStringメソッドが実行される…というように循環するわけです。

解決方法

さて、この循環参照を避けるための方法です。循環の絶てば良いのでその方法を確認すると、以下の通りLombokに関するアノテーションを循環元のクラス(Userクラス)に設定すればOKです。

@Data
@ToString(exclude = {"group"})          // ←ここでtoStringから除外するフィールドを指定する。
@NoArgsConstructor
@Entity
@Table(name = "users")
@JsonIgnoreProperties(value = {"handler", "hibernateLazyInitializer"})
public class User implements Serializable {
    ...
    private Group group;
}

これで例外が発生したときもStackOverflowにならないので安心です。もっとも、別な方法として自分でtoStringメソッドを実装するという方法もあります。こちらでは、循環させないようにすれば問題が起こることはありません。

SpringBootを始めるならこの書籍がおすすめです。というか、この書籍しかありません…。

JPAについて知りたいならこの書籍がおすすめです。

Pro JPA 2 (Expert's Voice in Java)

Pro JPA 2 (Expert's Voice in Java)