techium

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

独自Annotationのコード生成をjavapoetを使ってやってみる

独自Annotationを使ってコードの自動生成(Annotation Processor)をこちらの記事で行いました。

自動生成を行うにあたって文字列をStringBuilderを使って生成するとインデントや似たコードの生成などを作る際にコードを生成してみないとわからないためかなり手間取ります。

そのためテンプレートエンジンを利用してコードを生成することが良いと思われますが今回はsquare公開しているオープンソースのjavapoetを使って自動生成を行ってみましょう。

準備

まずjavapoetをAnnotation Processor用のライブラリプロジェクト側のbuild.gradleを下記のように変更しjavapoetをコンパイルするようにします。

[lib/build.gradle]

apply plugin: 'java'

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

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

これでライブラリ側でjavapoetを使う準備が整いました。
それではAnnotation Processorでの自動生成の実装は前回使ったものを改造してみましょう。

前回使ったコードが下記になります。

    @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;
    }

3行目から17行目までが自動生成するコードの内容をStringBuilderで文字列を追加して作成しています。
このコードでは自動生成のコードが複数のファイルで利用する際などでは再利用性が低い。
また\tとかが散見しており非常に読みづらい問題があります。

そこでjavapoetに置き換えて見ると次のようになります。

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        StringBuilder builder = new StringBuilder().append("return \"use annotation : [");
        for (Element element : roundEnvironment.getElementsAnnotatedWith(CustomAnnotation.class)) {
            String objectType = element.getSimpleName().toString();
            builder.append(objectType).append(",");
        }
        builder.append("]\";\n");
        MethodSpec methodSpec = MethodSpec
                .methodBuilder("getMessage")
                .addModifiers(Modifier.PUBLIC)
                .returns(String.class)
                .addCode(builder.toString())
                .build();

        TypeSpec typeSpec = TypeSpec
                .classBuilder("GeneratedClass")
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodSpec)
                .build();

        JavaFile javaFile = JavaFile
                .builder("com.hatenablog.techium.annotation.processor.generated", typeSpec)
                .build();

        try {
            javaFile.writeTo(processingEnv.getFiler());
        } 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;
    }

MethodSpecを使ってクラス内に定義したいメソッドの記述を行います。
そしてMethodSpecをTypeSpec生成時に渡し、指定したクラスにMethodSpecのメソッドを生成させるようにすることで同様のことができるようになります。
残念ながら3行目から8行目までのメソッド内のコードは文字列生成するため今までと同じ処理をしなければなりません。
このあたりはテンプレートエンジンなどを利用ような工夫をすれば更にきれいになる可能性があります。
さらにJavaFileのインスタンス生成時に生成するパッケージ名を渡しwriteToを使うことでTypeSpecで指定した内容をファイルに書き出してくれます。
ここでありがたいことにFilerを渡すことでそこにファイル吐き出しをしてくれる機能があるのでprocessingEnv.getFiler()を指定することで適切な場所にファイルを書き出してくれます。
このようにjavapoetを使うことで簡単に自動生成ファイルの作成ができるようになります。
これらのメソッドやクラスなどのjavapoetについてはJavaPoet 使い方メモに非常にまとまっていますのでそちらを読んでみることをおすすめします。

使ってみよう

先のAndroidで独自Annotationを使ってコードの自動生成したコードを使うでの手順通りにlib.jarを移し替えて使うのですが利用する側のアプリでもjavapoetのライブラリをコンパイル対象に入れる必要があります。
そのため下記のようにbuild.gradleの記述をします。

[app/build.gradle]

dependencies {
    ...
    compile 'com.squareup:javapoet:1.8.0'
    ...
}

これでサンプルコード通りにビルドすると次のようなクラスファイルが自動生成されます。

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

import java.lang.String;

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

改行やインデントなどがきれいに入っていることがわかるため可読性が高いことが見てて取りれます。