システム開発で思うところ

JavaEEを主にシステム開発をしながら思うところをツラツラを綴る

Pluggable Annotation Processing API Sample(実践編1)

以前の記事の続きです。

vermeer.hatenablog.jp

予定としては この流れで作っておきたいツールがあるので それを作り切るまで 続けたいと思っています。

はじめに

開発環境

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です。

メソッド:executeProcessorラウンドにて実行します。
今回は検証を行います。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に実行するコマンドとして作成したコマンドクラスを指定します。

メインパッケージのresourcesprocessor-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.javaTargetField.java

作成したAPIを登録

テストのためにテストパッケージのprocessor-command.xmlに登録をしています。

メインパッケージのresourcesprocessor-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)はリポジトリからクローンした直後は、コンパイルしていないということで まだコードが生成されていないので エラーになっています。

f:id:vermeer-1977:20170818225945p:plain

コンパイル

  • generated-sources/apt配下にクラスが作成されて コンパイル前にエラーになっていたクラスからエラーが消えました。

f:id:vermeer-1977:20170818230318p:plain

さいごに

メリット

使い方がシンプル

クライアント側はライブラリの依存だけと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:個人的には このあたりについて自分なりの落としどころを考えて もう少し対応したいと思っていますが、とりあえず作ろうと思っているものを一通り作ってから改めて取り組もうと思っています。