はじめに
コレは何?
Lombokをカスタマイズして、アノテーションの指定による引数チェックを行うライブラリを作成した際のメモ兼宣伝です。
ソースはGitHubに置いてあります。
動機
- メソッドの先頭に毎回、引数チェックの処理を記述する事に疲弊した。
- 他のメンバーが作成したAPIのリファレンスに、引数に指定出来る値の範囲が記載されていないくて立腹した。
- 他のメンバーが作成したAPIの、リファレンスに書かれている内容と実装とが異なっていて絶望した。
- 自分の作成したAPIのリファレンスを見直したら、引数に指定出来る値の範囲が記載されていないくて反省した。
- 自分の作成したAPIの、リファレンスに書かれている内容と実装とが異なっていて猛省した。
- JSR-305とFindBugsを試してみたが、業務アプリケーションにはあまり適合しない1と思った。
- Lombokでnullチェック以外のアサーションも出来れば良いと思った。
- なら作ってみよう。
方針
- 本家側の更新を楽に取り込むために、既存のコードは極力変更しない。
- デバッグの容易性を重視し2、実行時のライブラリ依存を許容する。
- 本家が提供するアノテーション(
@Setter
とか@Data
とか)との組み合わせが困難3なので、最低限の代替手段を提供する。
制限事項
- 前述の通り、本家が提供するアノテーションとは組み合わせられません(残念の極み)。
- この記事を書いている時点では、本家のv1.16.4をベースにしているため、Eclipse 4.5 (Mars)で色々と不具合がある模様です。
本家が用意してくれているビルドスクリプトの基本的な使い方
以下の手順は全て、リポジトリのルートディレクトリで行う前提です。
予め本家のリポジトリをそのままcloneするか、Forkしたリポジトリをcloneしておいて下さい。
また、LombokのビルドにはAntが必要になりますので、予めダウンロードしてパスを通しておいて下さい。
Eclipseにインポート
私はEclipseを常用しているので、下記コマンドでEclipseにインポート出来る形式(プロジェクト形式)に変換した後にインポートしました。
なお、プロジェクト名をlombok
から変更する場合は、build.xml
のeclipse
ターゲットを修正しておく必要があります4。
ant eclipse
ビルド
下記コマンドでJARを生成できます。
なお、プロジェクト名をlombok
から変更した場合はbuild.xml
やbuildScripts/website.ant.xml
等を色々と修正する必要があります4。
ant dist
テスト
以下のいずれかのコマンドを実行し、テストを行うJDKのバージョンを指定します。
なお、前述の手順でEclipseのプロジェクト形式に変換してある場合、Eclipseからテストを実行するためのRunLombokTest <JDK名>.launch
というファイルが生成されます。
ant setupJavaOpenJDK6TestEnvironment
ant setupJavaOpenJDK7TestEnvironment
ant setupJavaOracle7TestEnvironment
ant setupJavaOracle8TestEnvironment
JDKのバージョンを指定した後に、以下のコマンドによりテストを実行します。
なお、JDKのバージョンを切り替えてテストを行う場合は、前述のコマンドにより再度JDKのバージョンを指定した後に以下のコマンドを実行します。
ant test
Eclipseでの動作確認
下記コマンドを実行する事によりLombokizedEclipse.launch
というファイルがプロジェクト直下に生成され、メニューのRun As...
から、このプロジェクト(でant dist
で生成したJAR)を適用したEclipseを実行出来るようになります。
ant eclipseForDebugging
その他にやった事
- 独自の設定項目を追加するためにConfigurationKeys.javaに定義を追記6しました。
- 何故か
test/core/src/lombok/LombokTestSource.java
だけがLombokを使用しないとコンパイル出来ないコードとなっていたので修正しました(テストコードなのでリソースリークの可能性は気にしない)。 - 成果物をBintrayにアップロードするためにgradle-bintray-pluginを使用したかったので、かなり強引な
build.gradle
を作成しました。
実装方法
アノテーションを定義する
処理対象の起点となるアノテーションを定義します。
私の場合は、別のライブラリとしてアノテーションとチェックロジックを実装したため、当該のライブラリをbuildScript/ivy.xml
のdependencies
に追加しました。
なお、私の定義したアノテーションにはRetentionPolicy.RUNTIME
を指定してあります7が、特に理由が無ければRetentionPolicy.SOURCE
を指定する方が良さ気です。
ハンドラを実装する
JavacとECJ5とではASTの構造が微妙に異なり、APIは完全に別物です。
そのため、Javac向けのハンドラとECJ向けのハンドラをそれぞれ実装する必要があります8。
私の場合は、メンテナンスしやすい様に、Javac向けのハンドラとECJ向けのハンドラとで極力同じ構造にしました。
なお、ECJ向けのハンドラでは、Javadocコメントを扱う事が出来ない模様です。
テストする
/test/transform/resource
の下にある各サブディレクトリに、AST変換前のソースと、AST変換後のソースの期待値9やコンパイル時に出力されるメッセージの期待値等を配置します。
before
- AST変換前のソース
after-delombok
- Javac向けのハンドラによってAST変換された後のソースの期待値
after-ecj
- ECJ向けのハンドラによってAST変換された後のソースの期待値
messages-delombok
- Javacを使用してコンパイルした際に出力されるメッセージの期待値
messages-ecj
- ECJを使用してコンパイルした際に出力されるメッセージの期待値
messages-idempotent
-
after-delombok
にあるソースをコンパイルした際に出力されるメッセージの期待値
私の場合は、既存コードは基本的に修正しない方針(既存のソースはテストされている前提)としたので、自作のハンドラのみをテストするためのテストケースを作成してbuild.xml
のtest
ターゲットを修正しました。
ハマった事
アノテーションの単純名
com.develhack.annotation.assertion.NonNull
というアノテーションを定義したのですが、対応するハンドラが実行されませんでした。
どうやらLombokはアノテーションの単純名とハンドラの実装とを対応付けて管理している様で、Lombokが元々提供しているlombok.NonNull
と単純名が重複していたのが原因でした。
アノテーション名をcom.develhack.annotation.assertion.Nonnull
(nが小文字)に変更した所、期待した動作となりました。
ECJでのフィールド追加
EclipseAnnotationHandler#handle()
内でASTにフィールドを追加しようとしたら残念な結果になりました。
ECJでフィールドを追加する場合はEclipseAnnotationHandler#handle()
ではなくEclipseAnnotationHandler#preHandle()
で行う必要がある模様です。
所感
- Lombokステキ。
- AST変換も黒魔術だけど、バイトコード変換よりは白いと思う。
- バイトコード変換の仕組みが標準化されているんだから、AST変換の仕組みも標準化されれば良いと思った。
-
上限値の指定や下限値の指定、範囲の指定等が出来ない。 ↩
-
Lombokによって生成されたステートメントはソースコード上に現れないためデバッグが困難。 ↩
-
本家の実装が、各ハンドラ内で
@Nonnull
と@Nullable
の指定有無をそれぞれ(正規表現によるcase-insensitiveなマッチングで)判定しているため、それ以外のアノテーションに対応しようとすると修正箇所が非常に多くなってしまう。 ↩ -
最初は別のクラスとして定義しようとしたが、そっちのほうが修正箇所が多くなってしまいそうだった。 ↩
-
Lombokと組み合わせなくてもリフレクション等で使用出来るようにしておきたかった。 ↩
-
ぶっちゃけ、Javac向けのハンドラとECJ向けのハンドラの両者を実装するのは苦行です・・・。 ↩
-
AST変換はソースコードを変換する訳ではないので、正確には「変換後のASTをソースコードで表現した場合はこうなっているハズ」の期待値。 ↩