Lombok の Java9以降対応の話


TL;DR

  • Lombok が Java9 + IntelliJ に対応してなかったので、修正プルリク送って取り込んでもらった(AUTHORSに自分の名前が入った)
  • どういう変更をしたのかを解説(誰得)
  • Lombok は JDK の内部クラスにどっぷり依存しているので、JDKバージョンアップの度に追従が大変。JDKだけじゃなくてIDEの実装にも依存がある。
  • 今後 JDK のライフサイクルが変わるのでさらに大変。追従が遅れる可能性は多分にある。
  • がっつり Lombok に依存するとそういうリスクがあることを意識しておいたほうがいい。

Lombok について

  • Lombok は Java コードにアノテーションをつけるだけで Setter, Getter を作ってくれるなど、大変便利な開発ツールです。
  • Lombok は AST(Abstract Syntax Tree;抽象構文木)をコンパイル時に変更することで、必要な機能追加を行うようになっています。ソースコードやバイトコードを変換するわけではありません。
  • 本記事は Lombok の紹介記事ではないので、あとは本家Lombok 使い方メモあたりを見てください。

Lombok の Java9 + IntelliJ 対応

Java9 + IntelliJ でビルドが通らない問題があり修正を行ったのですが、どういう修正を行っていったのか書いていきます。

関係する部分の内部構造を知らないと説明できないので、簡単なクラス図を用意しました (今回の修正に関連する部分だけです。全体のごく一部)

JDK 内のクラス

まず、JDK の公開クラスとして、javax.tools パッケージ内に以下のクラスがあります。すべてソースコードやクラスファイルを操作するために使うクラスです。

  • JavaFileObject: ファイル(.java, .classなど)を操作するための interface。
  • JavaFileManager: JavaFileObject を生成したりするための interface。
  • StandardJavaFileManager: JavaFileManagerを継承した interface で、標準的なメソッドを追加したもの。

上記 interface の実装クラスが com.sun.tools.javac パッケージ以下にあります。こちらは JDK の内部クラスになります。

  • BaseFileManager: JavaFileManager の実装
  • BaseFileObject: JavaFileObject の実装 (javac6 と javac7 でパッケージが違う)
  • PathFileObject: JavaFileObject の実装

Lombok のファイルアクセス周りの実装

LombokFileObject

Lombok はAST変換を Annotation Processor で行っていますが、AST変換処理以外にもクラスファイル書き込みにも割り込むようになっています。
(詳細は PostCompiler.wrapOutputStream() を参照。バイトコード書き換えを行っています。@SneakyThrows や null チェック周りの不要コードの削除を実施している。)
(2018/2/22 認識誤りがあったので修正)

ファイル書き込みへの割り込みを実現するのが InterceptiongJavaFileObject です。このクラスは、LombokProcessor の初期化時に Javac 側にリフレクションで投入されるようになっています。
このクラスは LombokFileObject interface を implements しています。LombokFileObject は JavaFileObject を継承しています。

Lombok はこれを javac 側に渡して処理をさせるわけですが、一つ問題があります。javac 側は上記の BaseFileObject ないし PathFileObject を内部的に要求するようになっている(中で cast している)ので、JavaFileObject interface の実装を受け付けてくれません(正直、これは javac の実装がクソだと思いますが、、、)
なので、BaseFileObject なり PathFileObject を継承したクラスを用意し、Lombok 側に移譲するようにしなければなりません。Lombok ではこれを行うために以下の3つの Wrapper クラスを用意しています。

この時点で少々イヤな予感がしてくるわけですが、要は javac のバージョンごとに実装が違う wrapper を用意しているわけです。将来 JDK のバージョンが上がるごとにどんどん増えていく可能性は否定できないです。
いずれのクラスも、単に LombokFileObject に全メソッドを delegate するだけのものです。

LombokFileObjects

ここからやっと本番。LombokFileObjectsで JavaFileManager の wrap などの処理を行います。

まず LombokFileObjects に Compiler interface があります。これは JavaFileObject を wrap して LombokFileObject を返すためのクラスです (wrap()メソッドで実施)。これも javac 6, 7, 9 ごとに異なるクラス実装となっています。それぞれ上述の Javac[679]BaseFileObjectWrapper を返す実装になっています。

Compiler を返すのは LombokFileObjects.getCompiler(JavaFileManager jfm) メソッドです。引数に渡された JavaFileManager の実装クラスを調べて、適切な Compiler を返すという実装です。KNOWN_JAVA9_FILE_MANAGERS なんていう既知の JavaFileManager 実装クラス名のリストなんかがあって、イヤな感じが満載ですね。

JDK9 での問題と対応方法

javac9 に対応する Compiler 実装は Java9Compiler です。6, 7 にくらべるとこの実装は少々複雑です。これは Java9BaseFileObjectWrapper のベースクラスである PathFileObject がコンストラクタで BaseFileManager を要求するためです。

getCompiler() に渡される JavaFileManager が BaseFileManager のインスタンスであれば問題ない(キャストすればよい)です。しかし、これは別のクラスである場合があります。例えば IntelliJ から呼び出されるときは、StandardJavaFileManager を実装したクラスが渡されてきます。この場合はキャストしただけでは当然動きません。

このため、私は BaseFileManager を継承した FileManagerWrapper クラスを新しく用意し、これを使って移譲を行うように改造を行いました。プルリクはこちら です。本修正は無事取り込まれ、現在 1.16.20 としてリリースされています。
これでなんとか IntelliJ でもビルドが正しく通るようになりました。

ツライところ

  • JDK の実装によって内部動作が変わるので、追従がツライ。
  • 修正しようと思ってもそもそもデバッグがツライ。
    • IDEでブレークポイントかけて止めるということができない(そもそも IDE の中のビルドプロセスの中なので)。
    • なので print デバッグするしかない。
    • Eclipse については、Eclipse 自体をデバッグできるようにしている模様(?)。でも他の IDE では今のところ手がない。

今後の課題

Lombok は JDK の内部クラス (com.sun.)にがっつり依存しているので、JDKのアップデートで内部構造が変更されたら動かなくなります。特に、今までは Java のアップデートサイクルは比較的長かったのですが、今後は6ヶ月単位でアップデートされていく(LTSなら3年ごとですが)ので、影響は大きいと思います。
実際、OpenJDK10 Early Access ではすでに動かなくなってます。うはー。

また、JDK だけでなく IDE やビルドツール(Maven/Gradleなど)の実装にも影響を受けるのも問題です。Lombok のコア開発メンバは Eclipse ベースで進めているので、IntelliJ や Netbeans などの対応は後手に回る可能性があります。自分は IntelliJ メインなので、修正してコントリビュートはしていくつもりですが。

Lombok をがっつり使っていくなら、こういうリスクはあるということは意識しておくべきと思います。