CUBは子供の白熊

Java SE 8 実践プログラミングの練習問題を解く

第8章 その他の Java 8 機能を理解する : 問題 13 : ソースレベルのアノテーション処理

問題

問題 12 で定義したTestCaseアノテーションを処理するソースレベルのアノテーションプロセッサを構築せよ。

このプロセッサーは実行されると、テストを実行するmainメソッドを持つソースを出力する。

解答

問題 12 で定義したTestCaseアノテーションを、もう一度見てみよう。
今回はパッケージ名が関係してくるので、パッケージ名を省略しないでコードを記述する。

TestCase

package cub;

// 本アノテーションは繰り返し宣言できる
@Repeatable(TestCases.class)
public @interface TestCase {
    /** 引数 */
    int param();
    /** 想定する戻り値 */
    int expected();
}

TestCaseアノテーションの繰り返し

package cub;

// 実行時にアノテーションを取得できなくてもよい
@Target(ElementType.METHOD)
public @interface TestCases {
    TestCase[] value();
}

このTestCaseは、テスト対象のオブジェクトの生成方法が指定されていない。
そこで、テストするメソッドは static に限定する。

TestCaseアノテーションでテストする対象

package cub;

public class TestTarget {
    @TestCase(param=4, expected=24)
    @TestCase(param=1, expected= 1)
    public static int factorial(int n) {
        return n <= 1 ? 1 : n * factorial(n - 1);
    }
}

TestTargetクラスをコンパイルしたときに、以下のようなテストコードを出力する。

TestTargetクラスのテストコード

package cub;

public class Test {
    public static void main(String[] args) {
        System.out.print("テストケース : 引数 = 4, 戻り値 = 24 : ");
        try {
            int ret = cub.TestTarget.factorial(4);
            if (ret == 24)
                System.out.println("OK");
            else
                System.out.println("NG, 戻り値 = " + ret);
        } catch (Exception ex) {
            System.out.println("NG, 例外 : " + ex);
        }
        // "param=1, expected= 1" も同様
    }
}

では、上のテストコードを出力するプロセッサーを書いてみよう。

  • ソースレベルのアノテーション処理はjavax.annotation.processing.Processorインターフェースを実装しなければならない
    • そこでjavax.annotation.processing.AbstractProcessorの派生クラスを定義する
    • Override するメソッドはprocessだけで十分
  • ソースファイルの出力は自前でやるのではなくjavax.annotation.processing.Filerを使う
    • Filerオブジェクトはメンバーjavax.annotation.processing.ProcessingEnvironmentから取得
    • ソースファイルの出力はcreateSourceFile(CharSequence name, Element...)メソッドで
    • createSourceFileメソッドは、クラス名から所定のフォルダにファイルを作成するだけなので、パッケージ宣言やクラス宣言などは自前で出力する

■ テストコードを出力するプロセッサー

package cub;

@SupportedAnnotationTypes({"cub.TestCase","cub.TestCases"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class TestCaseProcessor extends AbstractProcessor {
    public boolean process(
        Set<? extends TypeElement> annotations,
        RoundEnvironment roundEnv
    ) {
        for (TypeElement type : annotations) {
            //
            // type は実質 TestCases アノテーションだが TestCases にキャストできないので
            // アノテーションを付けたメソッドから TestCases アノテーションを取得する
            //
            Filer filer = processingEnv.getFiler();
            try (Writer writer = filer.createSourceFile("cub.Test").openWriter()) {
                writer.write("package cub;\n\n");       // パッケージ宣言
                writer.write("public class Test {\n");  // クラス宣言
                                                        // main メソッド宣言
                writer.write("\tpublic static void main(String[] args) {\n");
                for (Element element : roundEnv.getElementsAnnotatedWith(type)) {
                    //
                    // element.toString() = "factorial(int)"
                    // element.getSimpleName() = "factorial"
                    // element.getKind() = MEHOD
                    // element.getEnclosingElement() = "cub.TestTarget"
                    // element.getEnclosedElements() はなし
                    //
                    String method = element.getEnclosingElement() + "."
                                  + element.getSimpleName();
                    TestCases testCases = element.getAnnotation(TestCases.class);
                    for (TestCase testCase : testCases.value()) {
                        String text = "テストケース : 引数 = " + testCase.param()
                                    + ", 戻り値 = " + testCase.expected() + " : ";
                        writer.write("\t\tSystem.out.print(\"" + text + "\");\n");
                        writer.write("\t\ttry {\n");
                        writer.write("\t\t\tint ret = " + method + "(" + testCase.param() + ");\n");
                        writer.write("\t\t\tif (ret == " + testCase.expected() + ")\n");
                        writer.write("\t\t\t\tSystem.out.println(\"OK\");\n");
                        writer.write("\t\t\telse\n");
                        writer.write("\t\t\t\tSystem.out.println(\"NG, 戻り値 = \" + ret);\n");
                        writer.write("\t\t} catch (Exception ex) {\n");
                        writer.write("\t\t\tSystem.out.println(\"NG, 例外 : \" + ex);\n");
                        writer.write("\t\t}\n");
                        writer.write("\n");
                    }
                }
                writer.write("\t}\n");                  // main メソッドを閉じる
                writer.write("}\n");                    // クラスを閉じる
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }
    }
}

このTestCaseProcessorコンパイルするのは普通に

javac cub/TestCaseProcessor.java cub/TestCases.java cub/TestCase.java

でよい。 TestCaseProcessorでソースを出力するには、javacコマンドで以下のオプションを指定する。

TestCaseProcessorによるアノテーション処理(実際には1行で)

javac -processorpath . -processor cub.TestCaseProcessor cub/TestTarget.java
       cub/TestCases.java cub/TestCase.java