以前の記事の続きです。
予定としては この流れで作っておきたいツールがあるので それを作り切るまで 続けたいと思っています。
はじめに
開発環境
Java8、Maven、Netbeans8.2
コード
全てのリポジトリは以下のプロジェクトに格納しています。
https://bitbucket.org/account/user/vermeerlab/projects/AN
目指したところ
- ライブラリ群として沢山のAPIを作成しておいて必要なものだけを使用するような仕組みがあると嬉しい
- 統一した処理が出来るようにインターフェースを準備する
APIの作成と登録(基本)
基本となる機能や使い方についての説明です。
Git
vermeerlab / apt-core / source / — Bitbucket
実行するAPIを作成
実行クラスパスとしては「テスト」内なので違和感があるかもしれませんが、テスト実施と基本ライブラリにサンプルを持っておきたいと考えた結果、このような構成になっています。
対象を特定するAnnotationを作成
マーカーとなるAnnnotationインターフェース(TestScan.class
)を作成します。このインターフェースを記載している要素が処理対象となります。
@Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface TestScan { }
実行するコマンドクラスの作成
インターフェース:ProcessorCommandInterface
を実装したクラスを作成します。ラウンド毎に実行するコマンドです。
public class ProcessorCommand implements ProcessorCommandInterface { @Override public Class<? extends Annotation> getTargetAnnotation() { return org.vermeerlab.apt.command.test.TestScan.class; } @Override public void execute(ProcessingEnvironment processingEnvironment, Element element, Boolean isDebug) { CommandValidator validator = CommandValidator.of(TestScan.class); validator.validate(TestScanValidation.of(element)); ProcessingEnvironmentUtil util = ProcessingEnvironmentUtil.of(processingEnvironment); DebugUtil debugUtil = new DebugUtil(isDebug); debugUtil.print(util.getPackagePath(element)); debugUtil.print(util.getTree(element).toString()); TypeMirror typeMirror = element.asType(); Boolean isSameType = util.isSameType(typeMirror, Object.class); debugUtil.print("isSameType=" + isSameType); } }
メソッド:getTargetAnnotation
の戻り値として設定した値が処理対象となるAnnotationインターフェースです。
今回はTestScan.class
です。
メソッド:execute
をProcessorラウンドにて実行します。
今回は検証を行います。Javaコードの生成は行っていません*1。
最後に実行するコマンドクラス(任意)の作成
インターフェース:PostProcessorCommandInterface
を実装するクラスを作成します。
本クラスの作成は任意です。
用途としてはAnnotation
で処理全体の結果を踏まえて何かをしたい時に使用します。
サンプルでは処理件数を標準出力しています。
public class PostProcessorCommand implements PostProcessorCommandInterface { private Integer count = 0; @Override public Class<? extends Annotation> getTargetAnnotation() { return org.vermeerlab.apt.command.test.TestScan.class; } @Override public void execute(ProcessingEnvironment processingEnvironment, Element element, Boolean isDebug) { this.count++; } @Override public void postProcess(ProcessingEnvironment processingEnvironment, Boolean isDebug) { DebugUtil debugUtil = new DebugUtil(isDebug); debugUtil.print("TestScan.class count = " + this.count.toString() } }
検証クラス(任意)を作成
インターフェース:ValidationInterface
を実装するクラスを作成します。
本クラスの作成は任意であり、またヘルパークラス用のものなので使用も任意です。
用途としてはAnnotation
で取得した要素(Element)に対して何かしらの検証を行う際に使用する統一インターフェースです。
public class TestScanValidation implements ValidationInterface { private final Element element; private TestScanValidation(Element element) { this.element = element; } public static TestScanValidation of(Element element) { return new TestScanValidation(element); } @Override public ValidationResult validate() { ValidationResult result = ValidationResult.create(); if (this.element.getSimpleName().toString().equals("ErrorTestTarget")) { result.append(ValidationResult.of(ValidationResultDetail.of("TestScanValidationError"))); } return result; } }
コマンドクラスのexecute()
内でCommandValidator#validate
の引数にすることで、複数の検証クラスを一度に実行できるヘルパーに使用することが出来ます。したがって、自分で検証の仕組みを設けたい場合は、使う必要もありません。
サンプルではクラス名によって検証を行っています。*2
上述のコードだと以下の部分です。
CommandValidator validator = CommandValidator.of(TestScan.class);
validator.validate(TestScanValidation.of(element));
validator.validate
の引数は複数指定できるので、elementの種類毎に色々な検証をしたい場合にイテレーターパターンで検証をします。というか、それだけのヘルパーです。
作成したAPIを登録
実行クラスパスとして今回はテストクラスパス内のresources
配下のprocessor-command.xml
に実行するコマンドとして作成したコマンドクラスを指定します。
メインパッケージのresources
にprocessor-command.xml
が存在しますが、これはライブラリの基本構造を示すということと、基本ライブラリをそのままForkして拡張することを鑑みた対処です。したがってファイル内に実行するコマンドクラスの指定をする記載もしていません。
コンパイル
コンパイルによりAnnotationProcessor
が動いたように思えますが、実際はテストクラスにて実行されただけです。
今回のケースではコード生成をしていないのでtarget
配下にコードが作成されることはありません。
APIの作成と登録(拡張)
基本となるライブラリを使用して独自のAnnotationProcessor
ライブラリを作成してみたいと思います。
実際に作ったコマンドは前回の記事でやったこととほぼ同じことをするものにしました*3。
説明は基本編との差分のみにします。
Git
vermeerlab / apt-extend-sample / source / — Bitbucket
Maven
基本ライブラリを依存ライブラリとして追加します。
加えてAnnotationProcessor
を使うときにお約束になるコンパイル時の指定を追記します*4。
<dependency> <groupId>org.vermeerlab</groupId> <artifactId>annotation-processor-core</artifactId> <version>0.1.0</version> </dependency> … <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.0</version> <configuration> <source>1.8</source> <target>1.8</target> <!-- Disable annotation processing for ourselves. --> <compilerArgument>-proc:none</compilerArgument> <showDeprecation>true</showDeprecation> </configuration> </plugin>
実行するAPIを作成
『APIの作成と登録(基本)』ではテスト内に作成しましたが、今回はきちんとメインパッケージに実装をします。
コマンド実行クラス:SampleCommand.java
処理対象アノテーション:TargetClass.java
、TargetField.java
作成したAPIを登録
テストのためにテストパッケージのprocessor-command.xml
に登録をしています。
メインパッケージのresources
にprocessor-command.xml
を作成する必要もありませんし、もしファイルがあったとしても作成したコマンドを追記する必要はありません。
コンパイル
- 生成のトリガーとなるクラス
package test; import org.vermeerlab.annotation.processor.sample.TargetClass; import org.vermeerlab.annotation.processor.sample.TargetField; @TargetClass public class Sample { @TargetField private final String _name; @TargetField private final String _desc; public Sample(String name, String desc) { _name = name; _desc = desc; } public String getName() { return _name; } public String getDesc() { return _desc; } }
- 生成されたクラス
package test; import java.lang.String; public final class SampleFactory { public static Sample create(String _name, String _desc) { return new Sample(_name,_desc); } }
APIを利用するクライアント
作成したAPI群を使用するクライアント側プロジェクトです。
作成した拡張API群をフレームワークとして配布して それを使う側の手順になります。
Git
vermeerlab / apt-extend-sample-client / source / — Bitbucket
事前条件
拡張API群をMavenでコンパイルしてローカルにMavenリポジトリが存在していること*5。
Maven
pom.xml
(関連個所のみ。お約束も忘れないようにということで明記)
<dependency> <groupId>org.vermeerlab</groupId> <artifactId>annotation-processor-extend-sample</artifactId> <version>0.1.0</version> </dependency> … <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.0</version> <configuration> <source>1.8</source> <target>1.8</target> <!-- Disable annotation processing for ourselves. --> <compilerArgument>-proc:none</compilerArgument> <showDeprecation>true</showDeprecation> </configuration> </plugin>
実装
AnnotationProcessor
の処理対象となるAnnotation
インターフェース(@TargetClass
)が設定されている生成トリガーとなるコードは、拡張APIのテストコードとほぼ同じなので省略します。
生成する予定のクラスを参照しているクラス(GeneratedUser.java
)はリポジトリからクローンした直後は、コンパイルしていないということで まだコードが生成されていないので エラーになっています。
コンパイル
generated-sources/apt
配下にクラスが作成されて コンパイル前にエラーになっていたクラスからエラーが消えました。
さいごに
メリット
使い方がシンプル
クライアント側はライブラリの依存だけとAnnotationProcessor
を動かすための記述をすれば良いだけです。
ブラックボックスではない(と思う)
Processorのコマンドクラスをクライアント側で指定するので どういう操作をしようとしているのか分かるのでブラックボックスでは無いと思います。機能の提供側としては機能一覧としてprocessor-command.xml
に記載する内容を提示しておいて、利用側が必要なものを選択する、または不要なものを外すことで対応可能です。
デメリット
コマンドの記述が繁雑
実行ライブラリのクラスパスにprocessor-command.xml
を配置しないといけないません。つまり利用側が自分のresources
配下に機能一覧を追記したprocessor-command.xml
を配置しないといけません。最低でも提供側が定義ファイルに記述する内容をルートパッケージのJavaDocなどに記載したり、手引きに書いておかないと、何をどうしたら良いのかわかりません*6。
解消案としては、基本ライブラリのProcessorCommandManager
にてxmlからコマンドクラスのパス(文字列)を取得しているので、ハードコーディングをするということが挙げられるでしょうか。
*7
次回
今回はコマンド作成と登録と起動をメインに整理しました。次回はテストについて投稿したいと思っています。なお説明に使用するテストコードはすでに今回のプロジェクト内に入っているものを使う予定です。
追記
2017/8/19
ブログ記載時の記述に一致するブランチを作成してリンクを修正。
*1:基本となるライブラリへの依存ライブラリを最低限にしておきたかったためです
*2:単にテストケースの網羅性を高くするためだけのものです
*3:したがって、あえてコードの主要な記載はコピー&ペーストにしています
*4:こちらを忘れて毎回 コードが生成されているのにクラスが参照できなくて「何でだ!!」と頭を抱える
*5:本記事を上から順番に実施していたら出来ていると思います
*6:このあたりを良い感じにできれば良かったのですがクラスローダー周りの実装力が無かったため断念しました。
*7:個人的には このあたりについて自分なりの落としどころを考えて もう少し対応したいと思っていますが、とりあえず作ろうと思っているものを一通り作ってから改めて取り組もうと思っています。