Annotation Processorの基本的な所

2013-11-13T00:00:00+00:00 gradle Java

codemodelを使ってアノテーションプロセッサ利用によるJavaコードの生成とかでもやってるけど、色々思う所があったのでやってみた

package sample.annotation;

public @interface Bean {
}

んなアノテーションがあって

package sample;

import sample.annotation.Bean;

@Bean
public class Sample {
}

的な感じで使っていると

package sample;


public class SampleBean extends Sample {
}

的な感じでソースが生成される的な事をやるアノテーションプロセッサーを作ると

package sample.processor;

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

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.SourceVersion;
import javax.tools.FileObject;
import javax.tools.StandardLocation;

import com.sun.codemodel.CodeWriter;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JPackage;

import org.apache.commons.lang3.ClassUtils;

import sample.annotation.Bean;

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("sample.annotation.*")
public class BeanAnnotationProcessor extends AbstractProcessor {

    private Messager messager;
    private Filer filer;
    private JCodeModel codeModel;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        filer = processingEnv.getFiler();
        codeModel = new JCodeModel();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Bean.class)) {
            String name = element.toString();

            final JPackage pkg = codeModel._package(
                ClassUtils.getPackageName(name)
            );

            try {
                JDefinedClass clazz = pkg._class(
                    ClassUtils.getShortClassName(name) + "Bean"
                );
                clazz._extends(codeModel.directClass(name));

                codeModel.build(new CodeWriter() {
                    @Override
                    public OutputStream openBinary(JPackage pkg, String fileName) throws IOException {
                        FileObject file = filer.createResource(
                            StandardLocation.SOURCE_OUTPUT,
                            pkg.name(),
                            fileName
                        );

                        return file.openOutputStream();
                    }

                    @Override
                    public void close() throws IOException {
                    }
                });
            } catch (JClassAlreadyExistsException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return true;
    }
}

的な感じになる訳だけど。問題はこれをどうやって実行するかっていう所かと。今までとかだとgradleで

apply plugin: "java"

configurations {
    apt
}

sourceSets {
    apt {
        output.resourcesDir = "apt_generated"
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile "org.apache.commons:commons-lang3:3.1"
    compile "com.sun.codemodel:codemodel:2.6"

    apt files("lib/apt.jar")
}

// ※Gradle2.0からCompileタスクタイプが無くなりJavaCompileを使えとの事らしいので
task compileAptJava(type: JavaCompile, group: "build", overwrite: true) {
    source = sourceSets.main.java
    classpath = configurations.compile + configurations.apt
    dependencyCacheDir = compileJava.dependencyCacheDir
    options.compilerArgs = [
        "-proc:only",
        "-processor",
        "sample.processor.BeanAnnotationProcessor"
    ]
    destinationDir = sourceSets.apt.output.resourcesDir
}

compileJava.dependsOn compileAptJava
sourceSets.main.java.srcDirs += sourceSets.apt.output.resourcesDir

っていう感じでアノテーションプロセッサーを指定してcompileJavaが行われる前にcompileAptJavaを処理する事が出来る訳だけど、これをしなくてもアノテーションプロセッサーなライブラリにMETA-INF/services/javax.annotation.processing.Processorを定義する事でコンパイルプロセス上で勝手にやってくれる模様。で中身には

sample.processor.BeanAnnotationProcessor

っていう感じでアノテーションプロセッサーなクラスを指定する。それを行なってjarでパッケージ化した場合には

apply plugin: "java"
apply plugin: "eclipse"

repositories {
    mavenCentral()
}

dependencies {
    compile files("lib/apt.jar")
}

compileJava {
    options.compilerArgs = ["-s", "apt_generated"]
}

な感じで特にcompileAptJavaなあれをしなくても良い。又、この場合デフォルトではクラスをコンパイルした先の所にソースが出力されるようになるが、-sオプションで指定しておけば良い模様

んまぁこんな所かとは思うんだけど、Eclipseとかで利用する場合には注釈処理とファクトリーパスを設定するだけで良いんだが、アノテーションプロセッサーの処理段階だとクラスパスが何かと認識してくれないのでアノテーションプロセッサーなjarに関してはfat jar(依存性を組み込んだ?jar)を作れば良い

gradleでfatjarを作る FuelPHPをやってみる (30) - Model_Nestedset -