techium

このブログは何かに追われないと頑張れない人たちが週一更新をノルマに技術情報を発信するブログです。もし何か調査して欲しい内容がありましたら、@kobashinG or @muchiki0226 までいただけますと気が向いたら調査するかもしれません。

Androidで独自Annotationを使ってコードの自動生成したコードを使う

独自のAnnotationを定義したものを利用してコードの自動生成を行う方法について説明します。
独自のAnnotationの作り方はこちらを参照して下さい。

コードの自動生成をするにはAnnotation Processorという機能を利用して行います。
最近まではandroid-aptというライブラリを利用してAnnotation Processorを利用することが多かったのですが、AndroidのAndroid Gradle Plugin version 2.2.0-alpha1以上ならばandroid-aptを利用しなくてもAnnotation Processorを利用することができます。

今回はAnnotation Processorを使って自動生成するライブラリを作り、android-aptを利用しないでコードの自動生成をしていましょう。

準備

まずはAndroid Studioでモジュール作成時にJava Libraryモジュールを作成します。 適当なプロジェクトを作成し、作成が完了したらツールバーのFile > New > New Module…を選択します。 選択すると下図が表示されます。

f:id:muchiki0226:20170311192137p:plain:w400

そこでJava Libraryを選択すると下図のようにライブラリ名とパッケージ名、新規モジュール内に自動生成するファイル名を聞かれます。

f:id:muchiki0226:20170311192233p:plain:w400

今回は例として次のように設定しました。

  • Library name : lib
  • Java package name : com.hatenablog.techium.annotation.processor
  • Java class name : AnnotationProcessorSample

そうすると下記のようなbuild.gradleとAnnotationProcessorSample.javaが生成されます。

[build.gradle]

apply plugin: 'java'

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

[AnnotationProcessorSample.java]

package com.hatenablog.techium.annotation.processor;

public class AnnotationProcessorSample {

}

これで準備が整いました。

独自Annotationの定義

まずは独自Annotaitonを作成します。 今回は例としてCustomAnnotationというAnnotaionを下記のように作成しました。

package com.hatenablog.techium.annotation.processor;

public @interface CustomAnnotation {
}

Annotationはこまかく設定することができますが今回の例では説明しません。 知りたい方はこちらを参考にしてください。

Annotation Processorの定義

Annotation Processorでファイルを自動生成するためにはAbstractProcessorを継承したクラスを作成しMETA-INFにファイルを作成しAnnotation ProcessorでAbstractProcessorを継承したクラスの呼び出しを行っておもらえるようにします。

AbstractProcessorを継承したクラスを下記のように作成します。

package com.hatenablog.techium.annotation.processor;

import java.io.IOException;
import java.io.Writer;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

@SupportedAnnotationTypes("com.hatenablog.techium.annotation.processor.CustomAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class AnnotationProcessorSample extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        StringBuilder builder = new StringBuilder()
                .append("package com.hatenablog.techium.annotation.processor.generated;\n\n")
                .append("public class GeneratedClass {\n\n")
                .append("\tpublic String getMessage() {\n")
                .append("\t\treturn \"");

        builder.append("use annotation : [");
        for (Element element : roundEnvironment.getElementsAnnotatedWith(CustomAnnotation.class)) {
            String objectType = element.getSimpleName().toString();
            builder.append(objectType).append(",");
        }
        builder.append("]\";\n")
                .append("\t}\n")
                .append("}\n");

        try {
            JavaFileObject source = processingEnv.getFiler().createSourceFile("com.hatenablog.techium.annotation.processor.generated.GeneratedClass");
            try (Writer writer = source.openWriter()){
                writer.write(builder.toString());
                writer.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            // 同じファイルが生成されている場合はFilerExceptionがthrowされるためここでprintMessageでErrorを設定するとビルドが通らなくなる
            processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Not create file : com.hatenablog.techium.annotation.processor.generated.GeneratedClass");
        }

        return true;
    }
}

まずAnnotation Processorを動かすにあたって何のAnnotationに対して処理を動かすのかを@SupportedAnnotationTypesにて定義します。
上記の例では独自定義のAnnotationであるCustomAnnotationを定義しています。
注意する点としてはパッケージをフルで記載しないと正しく動作しないため注意してください。
次に@SupportedSourceVersionでこのAnnotation Processorの要求するJavaのバージョンを記載します。
上記の例ではSourceVersion.RELEASE_7を指定しておりJava7を要求しています。
SourceVersion.RELEASE_8にするとJava8の要求になりbuild.gradleのsourceCompatibilityとtargetCompatibilityを1.8にするとビルドすることができるようになります。
また、このライブラリを利用するアプリもJava8を要求されることになるためJackとcompileOptionsで1.8を指定することになりますので注意してください。

次にAbstractProcessorを継承するとprocessメソッドが必須となります。
そのため22行目でprocessを実装しています。
Annotation Processorが動くタイミングでprocessメソッドが呼び出されます。
そこで生成するファイルの中身を定義してファイルに書き出すことを実施します。
23行目〜36行目まではStringBuilderで自動生成するjavaファイルの中身をappendで追加しています。
今回の例では独自のAnnotationをつけているメソッドやクラス名を取得してgetMessageというメソッドで独自Annotationを利用している名前を列挙して文字列で返却できるようにしています。

そして38行目〜49行目まででファイルの書き込みを行っています。
ここでprocessingEnvというのを使っています。
processingEnvはAbstractProcessorに定義されている変数でgetFilerメソッドを実行することで書き出し先のファイルに関する処理を行うためのインターフェースを取得することができます。
今回はcreateSourceFileを使ってソースファイルを新規作成すること行いました。
これ以外にもリソースファイルやクラスファイルを生成することもできます。
ここでcreateSourceFileの引数に自動生成するファイルのクラスをフルパスで指定することになります。
注意すべきなのはStringBuilder指定しているパッケージ名と吐き出すクラスのパッケージを合わせていないとエラーになるので注意してください。
createSourceFileで取得したJavaFileObjectに対してwriteすることでファイルの吐き出しを行います。
そのwriteを実行する際に渡すデータがStringBuilderで作った文字列を渡すことでその内容通りに自動生成されることになります。

これで自動生成部分の処理は完成です。

最後にMETA-INFに定義を追加します。

プロジェクトの「<プロジェクト名>/lib/src/main/resources/META-INF/services」にフォルダを作成し「javax.annotation.processing.Processor」というファイルを作成し下記のように記述します。

[javax.annotation.processing.Processor]

com.hatenablog.techium.annotation.processor.AnnotationProcessorSample

ここで記述するのはAbstractProcessorを継承したクラスを記載します。

これで準備が整いました。

libのモジュールをビルドすると「lib/build/libs/lib.jar」が作成されます。
このjarファイルの内部構成は次のようになっていればOKです。

f:id:muchiki0226:20170311192303p:plain:w200

コード自動生成のAnnotationを使ってみる

lib.jarを利用したいプロジェクトのlibsにコピーします。 そしてそのプロジェクトのbuild.gradleに下記を記載します。

[build.gradle]

dependencies {
    ...

    annotationProcessor files('libs/lib.jar')

    ...
}

dependenciesにannotationProcessorでコピーして持ってきたJarファイルを指定してあげます。
これで利用できるようになりましたので下記のようにAnnotationをつけてみてビルドしてみましょう。

@CustomAnnotation
public class MainActivity extends AppCompatActivity {

    @CustomAnnotation
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

上記のように@CustomAnnotationをつけてビルドすると「app/build/generated/source/apt/debug/com/hatenablog/techium/annotation/processor/generated」内にGeneratedClassが自動生成されます。

生成された内容は次のようになります。

package com.hatenablog.techium.annotation.processor.generated;

public class GeneratedClass {

    public String getMessage() {
        return "use annotation : [MainActivity,onCreate,]";
    }
}

それでは次のように自動生成したGeneratedClassを使って見ましょう。

@CustomAnnotation
public class MainActivity extends AppCompatActivity {

    @CustomAnnotation
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        GeneratedClass gen = new GeneratedClass();
        Log.d("MainActivity", gen.getMessage());
    }
}

実行すると次のようなログが表示され自動生成したコードがきちんと動いていることが確認できます。

03-11 17:00:56.470 4885-4885/com.hatenablog.techium.annotation.processor.sampe D/MainActivity: use annotation : [MainActivity,onCreate,]

サンプルコード

github.com