tokuhirom's Blog

[java] モダンな Annocation Processor の開発手順まとめ

APT とは Annotation Processing Tool のことで、Java でコードの自動生成を行う際に利用される。 APT を利用すると、Java クラスやリソースの自動生成が可能となる。

インターネットに情報は結構あるのだが、昔のものが多くて、Eclipse に JAR を追加して云々とかそういう感じのものが多くて辛いので調べたことをまとめておく。

アノテーションを作る

適当なアノテーションを作る。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target({
        ElementType.TYPE
})
public @interface Hello {
}

アノテーションプロセッサを実装する

AbstractProcessor を implement する。@SupportedAnnotationTypes で、処理対象のアノテーションを指定。@SupportedSourceVersion で、ソースのバージョンを指定。

ソースの生成には、テキスト処理用の template engine を利用してもよいが、いろいろやるなら javapoet などの Java コード生成用 DSL を利用したほうが、インデントや空行なども綺麗に揃うし、良い。

process() の引数にある RoundEnvironment を扱うための便利メソッドが google auto-common に入っているから、参考にするとよい。

package me.geso.sample.hello;

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

import javax.annotation.Generated;
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.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileObject;

import com.google.auto.common.MoreElements;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("me.geso.sample.hello.*")
public class MyProcessor extends AbstractProcessor {
	@Override
	public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
		if (annotations.isEmpty()) {
			System.out.println("no annotations");
			return true;
		}
		log("HUAAAAAAAAAAAAAAAAAAAAA");

		final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Hello.class);
		log(elements.toString());
		for (final Element element : elements) {
			final PackageElement aPackage = MoreElements.getPackage(element);
			final TypeSpec blah = TypeSpec.classBuilder("Blah")
				.addAnnotation(AnnotationSpec.builder(Generated.class)
					.addMember("value", "{$S}", getClass().getCanonicalName())
					.build())
				.addModifiers(Modifier.PUBLIC)
				.addMethod(MethodSpec.methodBuilder("hello")
					.addModifiers(Modifier.PUBLIC)
					.addCode(
						CodeBlock.builder()
							.add("return \"hello\";\n")
							.build()
					)
					.returns(TypeName.get(String.class))
					.build()
				).build();
			JavaFile javaFile = JavaFile.builder(aPackage.getQualifiedName().toString(), blah)
				.build();

			try {
				JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(aPackage.toString() + ".Blah");
				Writer writer = sourceFile.openWriter();
				javaFile.writeTo(writer);
				writer.close();
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}

		return true;
	}

	private void log(String msg) {
		if (processingEnv.getOptions().containsKey("debug")) {
			processingEnv.getMessager().printMessage(Kind.NOTE, msg);
		}
	}

}

src/main/resources/META-INF/services/javax.annotation.processing.Processor

src/main/resources/META-INF/services/javax.annotation.processing.Processor というファイルに、アノテーションプロセッサのクラス名を記述する。

me.geso.sample.hello.MyProcessor

maven pom.xml の設定

Annotation Processor 自体のコンパイル時に APT 有効にしていると、Annotation Processor 自体のコンパイルに Annotation Processor 使おうとしてわけわかめになるということがあるので、無効にしたほうがいいかもしれない(IntelliJ だけかも)。

	<build>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
					<!-- disable annotation processing -->
					<compilerArgument>-proc:none</compilerArgument>
				</configuration>
			</plugin>
		</plugins>
	</build>

テストコード書く

検索すると AptinaUnit 使えって書いてあるものが多いのだが、今どきは google compile-testing を使うとよさそう。 APT に限らず、コードコンパイルして云々するもの全般で使える。 開発もアクティブだし。

https://github.com/google/compile-testing

書き方は、javadoc か test code か google auto のコードを参考にする。

コンパイラオプションの指定をしたいところだが、現行バージョンでは指定できない。 実装はされててリリース待なので今後に期待。 https://github.com/google/compile-testing/pull/64

package me.geso.sample.hello;

import static com.google.common.truth.Truth.assert_;
import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource;

import org.junit.Test;

import com.google.common.io.Resources;
import com.google.testing.compile.JavaFileObjects;

public class ProcessorTest {

	@Test
	public void testProcess() throws Exception {
		// Compiler option coming soon.
		// https://github.com/google/compile-testing/pull/64

		assert_().about(javaSource())
			.that(JavaFileObjects.forResource(Resources.getResource("HelloWorld.java")))
			.processedWith(new MyProcessor())
			.compilesWithoutError()
			.and()
			.generatesSources(JavaFileObjects.forSourceString("foo.bar.baz.Blah", "package foo.bar.baz;\n"
					+ "\n"
					+ "import java.lang.String;\n"
					+ "import javax.annotation.Generated;\n"
					+ "\n"
					+ "@Generated({\"me.geso.sample.hello.MyProcessor\"})\n"
					+ "public class Blah {\n"
					+ "  public String hello() {\n"
					+ "    return \"hello\";\n"
					+ "  }\n"
					+ "}"));
	}
}

参考になるコード

google auto

https://github.com/google/auto/

google auto は APT 関連のサンプルとしてよい。

auto commmon は、APT 用のユーティリティクラスとして便利。

APT の実際の使い道

コンパイル時に計算するとか、ボイラープレートコードの生成とかには役立つが、実際、既存のコードを変更できるわけではないので、できることは意外と少ない。

例えば、Dagger2 のようなものにはよい。DI 対象がコンパイル時に確定するので、実行時の速度を抑えることができる。Android 等の始動速度が必要なことが有用なテクニックとなる。

生成されたコードは target/generated-sources/ を見れば確認できるので、実際の動作が想像しやすい。デバッグもしやすくなる。JVM bytecode の知識がなくても修正可能だし、デバッグ等もやりやすい。

lombok のような AST を処理するものよりはよほどやりやすい。

参考資料

http://www.slideshare.net/vvakame/apt-7568357