codemodelを使ってアノテーションプロセッサ利用によるJavaコードの生成

2013-08-10T00:00:00+00:00 gradle Java

とあるチャットにて、「クラスについたアノテーションからJavaソース生成させるとかどうやるの」っていう相談を受けたんだが、まぁ出来るんだろうけどやった事が無いっていう事でやってみた

でJavaコード(.java)を生成させる方法としてまず思いついたのが、codemodelっていうのを使う。多分これSun Microsystems時代にそこが公式的に作ったのじゃないのかなーって。まぁパッケージ名もそうなってとるしね、そんなことはおいといてアノテーションプロセッサから得られたクラスに対するアノテートを処理させ、その情報を元にcodemodelを使ってコードを生成させる事は出来るのではっていう事

んまぁグダグダ言ってないでコード書けよハゲっておこられそうなので(ry

※いろいろとイケてないのでネタとして

package sample;

public class Sample {
    @Attribute
    protected String name;
}

的な事やれば

package sample;

public class SampleBean extends Sample {
    public String getName() {
        return name;
    }
}

的なJavaコードを出力できるように。んじゃアノテーションプロセッサーを書く

簡単に言うと@Attributeのアノテーションがついてるフィールドを走査してcodemodelを使ってメソッドを作り、ソースを生成させる

package sample;

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

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
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.TypeElement;
import javax.tools.FileObject;

import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;

import com.sun.codemodel.CodeWriter;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JType;

import static javax.tools.StandardLocation.SOURCE_OUTPUT;
import static com.sun.codemodel.JMod.*;

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("sample.Attribute")
public class SampleAnnotationProcessor extends AbstractProcessor {

    private static final String GENERATE_CLASS_SUFFIX = "Bean";

    private Filer filer;
    private CodeWriter writer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
        writer = new CodeWriter() {
            @Override
            public OutputStream openBinary(JPackage pkg, String fileName) throws IOException {
                FileObject file = filer.createResource(SOURCE_OUTPUT, pkg.name(), fileName);

                return file.openOutputStream();
            }

            @Override
            public void close() throws IOException {}
        };
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // roundEnv.getRootElementsだと、アノテーションがついてるついてない以前に走査されるソースが取得されるのかも。今回はクラスアイデンティファイにつくアノテーションが無いので走査してから@Attibuteなアノテーションがついてるクラスに処理を行う

        for (Element root : roundEnv.getRootElements()) {
            JCodeModel cm = new JCodeModel();
            String rootClassName = root.toString();
            List<Element> methods = new ArrayList<Element>();

            for (Element element : roundEnv.getElementsAnnotatedWith(Attribute.class)) {
                Element encloser = element.getEnclosingElement();
                String presentAnnotatedClassName = encloser.toString();

                if (rootClassName.equals(presentAnnotatedClassName)) {
                    methods.add(element);
                }
            }

            if (methods.size() <= 0) {
                continue;
            }

            JDefinedClass clazz = createClass(cm, rootClassName);

            if (clazz == null) {
                continue;
            }

            for (Element element : methods) {
                createMethod(cm, clazz, element);
            }

            build(cm, clazz, rootClassName);
        }

        return false;
    }

    private JDefinedClass createClass(JCodeModel cm, String className) {
        JDefinedClass clazz = null;

        try {
            // 元クラスのパッケージ名をJPackageとして取得
            JPackage pkg = cm._package(ClassUtils.getPackageName(className));

            // クラスを生成
            clazz = pkg._class(ClassUtils.getShortClassName(className) + GENERATE_CLASS_SUFFIX);
        } catch (JClassAlreadyExistsException e) {
            e.printStackTrace();
        }

        return clazz;
    }

    private void createMethod(JCodeModel cm, JDefinedClass clazz, Element element) {
        String name = element.toString();
        String getterName = "get" + StringUtils.capitalize(name);
        String typeStr = element.asType().toString();

        try {
            // FIELDの型を取る
            JType type = cm.parseType(typeStr);

            // FIELDの型と「get + FIELD名をucfirstした名称」なメソッドを作る
            JMethod method = clazz.method(PUBLIC, type, getterName);

            // 内容は単純にフィールド値を返すだけ
            method.body()._return(JExpr.ref(name));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    private void build(JCodeModel cm, JDefinedClass clazz, String rootClassName) {
        try {
            // インターフェースな場合
            // clazz._implements(Class.forName("sample.Sample"));

            clazz._extends(cm.directClass(rootClassName));
            cm.build(writer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

ていう感じで、ビルドしてjarに突っ込んでおく。で別プロジェクトでこのjarを参照して利用するんだけど、Eclipseとか使わずに単純にgradleでbuildタスクでAPT処理をお願いする形で使ってみる

apply plugin: "java"

repositories {
    mavenCentral()
}

dependencies {
    compile files("lib/apt.jar")
    compile "org.apache.commons:commons-lang3:3.1"
    compile "com.sun.codemodel:codemodel:2.6"
}

sourceSets {
    apt
}

task compileAptJava(type: Compile, group: "build", overwrite: true) {
    source = sourceSets.main.java
    classpath = configurations.compile
    dependencyCacheDir = compileJava.dependencyCacheDir
    options.compilerArgs = [
        "-proc:only",
        "-processor", "sample.SampleAnnotationProcessor"
    ]
    destinationDir = sourceSets.apt.output.resourcesDir
}

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

んな感じ。でやってみて思ったのが、通常アノテーションを処理する際にはアノテーション自体がどこに付与出来るかは@Target等で指定出来る。でもそのチェック処理等の前にアノテーションプロセッサが働いてしまってるので、そのバリデーションを行う前に処理される為にフィールドに付与出来るのをクラスに付与したりしてもソースが生成されちゃう。そこら辺のチェックやる方法がちょっと微妙に分かってない

んまぁとりあえずはアノテーションプロセッサとcodemodelを使ってソースを生成しちゃったりする的な事は出来なくも無いって模様。ただ、それをやるにはアノテーションプロセッサのかなりな知識が必要になりそう。んまぁこういうの普通にJavaでWebシステム開発とかやってるだけならまず出会わないだろうなぁっと

んまぁ、良い体験できた気がする

Chrome Extension+Google App Engine(+OAuth) Laravel使ってみた (20) - Mail -