第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