誰も教えてくれなかった Lombok のこと

(2014年1月14日:重要な追記があります)

昨年の夏頃、Java界隈でLombokというライブラリが話題になっていました。Lombokは、大雑把に言うとgetter/setterなどの定型コード(ボイラープレート・コード)をコンパイル時に補完してくれるものです。火付け役になったのはおそらくきしださんブログ記事ですが、菊田さんブログで1つのカテゴリとして出来上がっていたり、あちこちのブログで絶賛されるなど、大勢が望んでいたプロダクトである(そして同じ数だけgetter/setterに辟易していた)と言えるでしょう。

筆者は一昨年のJava Advent Calendar槙さんが取り上げたことで初めて存在を知りました。少し使ってみてその可能性に魅力を感じていたものの、当時のバージョンでは局面によって(確かJPAを使った時だと思います)上手く動かない場合があり、また本業のSIerではLombokのような「万人がからくりを理解できない」プロダクトを徹底的に忌避する傾向にあるため、なかなか出番がありませんでした。

今回、年末年始休暇を使って数年前に壊して家計簿を復活させようと計画しており、そこでLombokを導入しようと企んだのですが、由々しき問題が発生しましたので纏めておこうと思います。

1. Maven依存関係のスコープは「provided」で

Lombokはlombok.jarというファイル1つで、IDEへのアタッチメントから定型コードの自動生成まで丸ごとやってくれます。jarを直接プロジェクトに組み込む場合は、他のライブラリと同じように扱っても特に妙な動作をすることはないはずです。

一方、最近はMavenが普及していて、JavaFXアプリケーションを除いてMavenプロジェクトとなっていることがほとんどです。筆者はJigsawによる標準化の早期実現を望んでいるのですが、デファクト・スタンダードは無視できませんので。

Mavenで依存ライブラリをpom.xmlに追加する際は、多くの場合スコープを「compile」にすると思われます。Lombokは、現在ではスコープを「provided」と指定するようになっています。その証拠に、Lombokのサイトでは、以下のように設定するよう指示があります(2014-01-03現在)。

<dependencies>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.12.2</version>
    <scope>provided</scope>
  </dependency>
</dependencies>

ちなみに、先述のきしださんのブログ記事では、Lombok v0.12.0を使用していましたが、秋頃にJDK8対応をとりあえず済ませたv1.12.2がリリースされ、現在はこちらが安定版とされています。2013年11月以降のLombok紹介記事でv0.12.0を最新と称しているものがあれば、おそらくはきしださんの記事のパクりです。

筆者がまず解せなかったのは、スコープが「compile」でなく「provided」にするよう指示があること、そして日本のブロガーが誰ひとりとしてそのことを追求しようとしていないことでした。スコープを「provided」にするとlombok.jarは成果物に含まれず、かといって実行環境が提供するものでもないため、ライブラリのスコープとしては適切ではないのではないか?という疑いです。

結論から言うと、スコープは「provided」が妥当です。それはLombokの動作について考えてみるとおよそ察しが付きます。

lombok.png

Javaプログラムのコンパイルは概ね上図のように進行します。コンパイル時のクラスパスにLombokが含まれていると、コンパイル時にLombokが入力(ソースファイル)を解析して定型コードを生成した後、コンパイラに渡します。もう少し詳しく説明すると、コンパイラがソースファイルを読み込んでAST(Abstract Sintax Tree; 抽象構文木)という内部形式に変換して、本来ならそのままコンパイルを行うべきところを、LombokがASTを横取りして定型コードを付け加えた上で、変更後のASTでコンパイルを行います。従って、ソースコードレベルではgetter/setterが存在しなくても、クラスファイル(バイトコード)レベルではgetter/setterが存在するのです。

以上のことから、lombok.jarはコンパイル時にはクラスパス上に存在しなければならないが、コンパイル後はお役御免になるということが分かります。そのためプロジェクトに内包されることを前提とする「compile」や、実行時(テストを含む)のクラスパス上に存在しなければならない「runtime」ではなく、コンパイル時(と実運用時)にだけクラスパスにいれば良い「provided」が適切な選択肢だとわかります。

how lombok works? (StackOverflow)
http://stackoverflow.com/questions/6107197/how-lombok-works

Project Lombok によるカスタム AST 変換 (IBM developerWorks)
http://www.ibm.com/developerworks/jp/java/library/j-lombok/

これと似たようなことがIDEでも行われます。現在のIDEはリアルタイムで差分ビルドを行い、文法チェックやクラスに関する情報の更新を行っています。そこにLombokが入り込むことで、ソースコード上ではgetter/setterが存在しなくても、クラス構造のビュー上ではきちんとgetter/setterが存在している、という状況が作り出せます。

Lombokに対応しているIDEは、現在のところEclipse(および派生IDE)とNetBeansだけです。IntelliJ IDEAへの対応は早期(4年半前!)から要望に上がっていますが、開発サイドは消極的(というかほとんどやる気がない)です。代わりにサードパーティのプラグインが公開されており、その他にも連携手段はいくつかあるようです。

2. Eclipse KeplerのJava 8対応パッチでは使えない

以前、Eclipse KeplerにJava 8対応パッチ済みビルドが公開されているとお伝えしました(その後、本家のJDTでもバイナリのスナップショットを公開しています)。

実は、Eclipse KeplerにLombokをインストールして実際に使ってみると、「Lombok annotation handler class lombok.eclipse.handlers.<Class-name> failed - See error log.」というAPTレベルのエラーが出てしまうのです。ややこしいことに、@Getterや@ToString等では発生せず、@Data、@Setter、@EqualsAndHashCodeを使うと発生してしまうというおまけ付き。

原因は、Eclipse JDTの内蔵JavaコンパイラであるECJ(Eclipse Compiler for Java)のJava 8対応パッチのバグでした。その証拠に、エラーを無視してMavenでビルドすると正常終了し、Eclispe Kepler本来のJava 7版ECJでも何の問題も発生しませんでした。Eclipseはコーディング時には高速なECJを使用し、AntやMavenでビルドする際には確実なjavacを使うという戦略を採っています。これはEclipse誕生当初の高速ビルドに大きく貢献しました(ビルド自体は今でもNetBeansやIntelliJ IDEAより高速)が、今回はそれが裏目に出てしまったようです。

実は筆者がブログで紹介したパッチ済みビルドと、その後本家で公開されたバイナリパッチでは、JDTのバージョンが若干違っていたのですが、動作確認を行った結果双方とも同じ現象が発生しました。

LombokユーザーにはEclipse愛用者が相当数いるようで、この件もしっかりIssue Trackerに上がっていました。

開発サイドではこれがJava 8対応ECJのバグだと見抜き、Eclipse本家にエスカレーションしています。

報告が去年の12月28日(日本時間だと29日かも?)なので、修正まで多少時間がかかると思われます。修正は次期リリースEclipse 4.4 Lunaに反映されますが、Keplerにバックポートされるかは今のところ不明です。

2014年1月13日 追記

上記のバグは2013年12月28日に報告され、年明け2014年1月5日(日本時間では1月6日)に修正されました。最新のEclipse 4.4向けJDTにマージされているほか、Eclipse 4.3のJDT向けパッチにも同様の修正が適用されていますので、JDTパッチを最新版に更新することで問題は発生しなくなります。