AnnotationProcessorでプログラムの自動生成
本記事はあまりにも情報が少ないため書いた記事です。本なら情報があると思うのですが、ネットでは見つけられませんでした。
そして、書いてる人は使うことをあきらめました。
プログラムを一から生成する場合こちらで紹介する方法ではなく、テンプレートなどからJavaFileObject
を生成したほうが楽です。
代替手段
- リフレクションで実行時に何とかする - 動作速度は遅いですが情報が多いです。
- 部分的に
JavacParser
で作成して差し替え - 圧倒的に楽になると思います。
環境
- JDK corretto-11
- Gradle gradle 7.4
- Intellij IDEA 2022.1
gradleの設定
AnnotationProcessor
を作成するプロジェクトにいかを追加します。
dependencies {
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'
compileOnly 'com.google.auto.service:auto-service:1.0.1'
if (targetCompatibility.toString().startsWith("1."))
implementation files("${System.properties['java.home']}/../lib/tools.jar")
}
compileJava {
if (!targetCompatibility.startsWith("1.")) {
doFirst {
options.compilerArgs = ["--module-path", classpath.asPath,
"--add-modules", "com.google.auto.service",
"--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED"]
classpath = files()
}
}
}
具体的に何をしているかというと、使用するクラスがJDKのバージョンが8以下の場合tools.jar
に、それ以外の場合モジュールシステムで隠蔽されているので強制的にexportしています。
また、本来なら自分でMETA-INF/service/javax.annotation.processing.Processor
にAnnotationProcessorの情報を記述しないといけないのですが、googleの auto.service
の@AutoService
アノテーションをつけると自動で記述してくれます。
クラス,メソッドを取得する。
アノテーションがつけられているクラスやメソッドを取得します。
package com.example
import com.google.auto.service.AutoService;
import com.sun.tools.javac.code.*;
import com.sun.tools.javac.model.*;
import com.sun.tools.javac.tree.*;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.util.*;
import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
@AutoService("Processor.class")
@SupportedAnnotationTypes("*")
public class AnnotationProcessor extends AbstractProcessor{
JavacElements elementUtils;
Context context;
TreeMaker maker;
Names names;
@Override
pulbic synchronized void init(ProcessingEnvironment processingEnv){
elementUtils = ((JavacElements)processingEnv.getElementUtils());
context = ((JavacElements)processingEnv.getContext());
maker = TreeMaker.instance(context);
names = Names.instance(context);
}
@Override
public boolean process(Set<? extends TypeElement> annotations,RoundEnvironment roundEnv){
for (Element element : roundEnv.getElementsAnnotatedWith(Override.class){
JCTree tree elementUtils.getTree(element);
JCMethodDecl jcMethodDecl = (JCMethodDecl)tree;
// ↑これ
}
}
}
なお以下からprocessの中身または一部のみだけ書きます。
一つ上のTreeを取得する。
メソッドならメソッドが所属するクラスを取得します。
for (Element element : roundEnv.getElementsAnnotatedWith(Override.class){
Element enclosingElement = element.getEnclosingElement();
// ↑これ
}
クラスにメソッドを作成する。
classDecl.defs = classDecl.defs.prepend(maker.MethodDef(maker.Modifiers(Flags.PUBLIC),
names.fromString("methodName"), //メソッド名
Maker.Ident(names.fromString("String")), //返り値の型
List.nil(),//ジェネリクスの<T,K>←これ
List.nil(),// パラメータ(引数)
List.nil(),// throwされる例外
null, //Block(後述)
null //初期化(用途不明)
));
メソッドを実行する
maker.exec(
maker.Apply(
null, //ジェネリクス
maker.Select( //実行するメソッドのインスタンス?
maker.Ident(
names.fromString("Integer") //インスタンス等を名前で指定
),
names.fromString("parseInt") //メソッド名を指定
),
args //メソッドに渡す引数 例えば List.of(maker.Literal("100"))
)
);
変数の作成
maker.VarDef(
maker.Modifiers(Flags.PUBLIC), // PublicやParameterなど
names.fromString("varName"), // 変数名
maker.Ident(names.fromString("int")), //型
exec.getExpression() //初期化 この場合↑の結果を代入している
)
その他
- いろいろな変数がPublicなのをいいことに直接編集するとエラーが多発するので普通に作成したほうがいいです。