MyBatisのLazy Load + Jacksonでシリアライズエラーになる件の回避方法


MyBatisのLazy Loadが適用されているオブジェクト(MyBatis内蔵のJavassistのProxyオブジェクト)を、Jacksonを使ってJSONにシリアライズしようとすると以下のようなエラーが発生することが報告されています。(https://github.com/mybatis/mybatis-3/issues/570)

com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory$EnhancedResultObjectProxyImpl and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.apache.ibatis.submitted.javassist.User_$$_jvst15a_0["handler"])

    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:284)
    at com.fasterxml.jackson.databind.SerializerProvider.mappingException(SerializerProvider.java:1110)
    ...

Note:

MyBatisは、JavassistをMyBatisのJarの中に内蔵するスタイルを採用しており、パッケージもMyBatis配下のパッケージ(org.apache.ibatis.javassist)にリパッケージしています。なので・・・本投稿の中で出てくるJavassistのインタフェースのパッケージはorg.apache.ibatis.javassistであるという点に注意してください!!

これは、Proxyオブジェクトの中で保持しているMethodHandler型のhandlerプロパティ(Proxyのメソッドを呼び出した時のハンドリングを行うためのオブジェクト=Lazy Load処理を制御するオブジェクト)を、JSONにシリアライズしようとして発生しています。
原因はスタックトレースにも出力されている通り、MethodHandlerの実装クラス(org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory$EnhancedResultObjectProxyImpl)をシリアライズするためのクラスが見つからなかったためです。

解決方法はいくつかありそうですが・・・本投稿では、

  • @JsonIgnoreを付与したインタフェースを作り
  • @JsonIgnoreを付与したインタフェースをJavassistのProxyインタフェースにMix-inさせる

ことで、シリアライズが不要なhandlerプロパティをシリアライズ対象から除外する方法を紹介したいと思います。

動作検証バージョン

  • MyBatis 3.4.2
  • Javassist 3.21.0-GA (MyBatis内蔵のJavassist)
  • Jackson 2.8.5 (2.4.4+が必須)

@JsonIgnoreを付与したインタフェースを作る

まず、@JsonIgnoreを付与したインタフェースを作ります。

public interface MyBatisJavassistProxyMixIn {
    @JsonIgnore MethodHandler getHandler(); // @JsonIgnoreを付与してhandlerプロパティをシリアライズ対象外に指定する
}

Note:

メソッドの返り値の型は、実はあまり意味がなく・・・Objectvoidでも動作します。ここでは実体にあわせてMethodHandlerにしています。

Proxyインタフェースに@JsonIgnoreを付与したインタフェースをMix-inする

次に、@JsonIgnoreを付与したインタフェースをMyBatis内蔵のJavassistのProxyインタフェースにMix-inします。

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.addMixIn(Proxy.class, MyBatisJavassistProxyMixIn.class); // Mix-in !!

こうすると・・・・
MyBatis内蔵のJavassistのProxyインタフェースを実装したクラスのhandlerプロパティを、シリアライズ対象から除外することができます。

まとめ

特にないのですが・・・
Jacksonに限らず・・・MyBatisのLazy Loadが適用されているオブジェクトをJSONにシリアライズする場合は、同様の問題が起こる可能性がありそうな気がするので、特定のプロパティをシリアライズ対象から除外する方法を事前に調べておくとハマらなくてすみそうです。
あと・・・MyBatisはCGLIBを使ったProxyもサポートしてるけど、今回は未検証です(あしからず・・・)。

参考サイト