今回はJava6から追加されたAnnotation Processingを使ったソースコード生成をプログラムを作るまでの過程を記載していきます。
Annotation Processingについてはネットで検索していけばどういうものかはおぼろげながら理解できると思います。
1. Processorを触ってみる。
##Processor提供側
1.1. Processor提供用のプロジェクトを作成。
1.2. プロジェクト内のsrcフォルダ内に
・TargetClass.java <- Annotation定義用クラスファイル
・SampleProcessor.java <- Processorの実装
1.3. TargetClass.javaを以下のように編集。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface TargetClass{
}
1.4. AbstractProcessorを継承したSimpleProcessorを実装。
(processメソッドはProcessorを使う側をコンパイルした時点で実行されます)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"*"})
public class Processor extends AbstractProcessor{
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
processingEnv.getMessager().printMessage(Kind.NOTE,
"サンプル");
return true;
}
}
1.5. srcフォルダ内にMETA-INF/services フォルダを作成。その中にjavax.annotation.processing.Processorファイルを作成。そのファイル内に実行したいProcessorのクラス名を記載。
1.6. srcフォルダを含んだjarファイルを生成。
##Processor使用側
1.7. Processor使用側のプロジェクトを生成。
1.8. 1.6.で生成したjarファイルをプロジェクトにインポートし、Java Build Pathに設定する。
1.9. プロジェクトを選択→プロパティを選択。Java Compiler-Annotation Processing-Factory Pathを選択し、1.6でインポートしたjarファイルを設定。
そして、Java Compiler-Annotation Processingを選択し、下のキャプチャの通りにチェックを入れる。
[Java Compiler-Annotation Processing-Factory Path]
[Java Compiler-Annotation Processing]
1.10. 一度コンパイルが通るのでこれでoK.メッセージはここにでる!
Processメソッドは2回実行されます。(なぜProcessorが2回実行されるのかは…すいません、調査不足でわかっていません…)
EclipseでError Logを開くとProcessorからのメッセージが表示されます。
2. javapoetを使ったソース生成
いよいよソースコード生成に入りますが…下手に書こうとすると死にます。なので、Square社のjavapoetというライブラリを使って楽に実装していきます。(というか最近はjavapoetを使うやり方がメジャーらしい。)
Square/javapoet
https://github.com/square/javapoet
##Processor提供側
2.1. javapoet.jarを上記ページからダウンロード(README.mdの下部にリンクあり)
2.2. Processor提供側プロジェクトにダウンロードしたjavapoet.jarをインポートし、Java Build Pathに設定する。
2.3. SampleProcessor.javaにコード生成のロジックを書く。
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"*"})
public class Processor extends AbstractProcessor{
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
processingEnv.getMessager().printMessage(Kind.NOTE,
"サンプル");
try{
makeSource();
processingEnv.getMessager().printMessage(Kind.NOTE, "クラスファイル生成");
}catch(IOException ex){
ex.printStackTrace();
}
return true;
}
private void makeSource() throws IOException{
MethodSpec main = MethodSpec.methodBuilder("create")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("Hello World!!")
.build();
TypeSpec sampleMain = TypeSpec.classBuilder("SampleMain")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.lyricaloriginal"
, sampleMain)
.build();
javaFile.writeTo(processingEnv.getFiler());
}
}
ここに記載しているソースコードはjavapoetのGithubに提示されているサンプルソースの原型をそのまま利用しています。
JavaFile#writeToメソッドの引数には出力先フォルダを指定します。ProcessingEnv#getFiler()はProcessor使用側プロジェクト内のbinフォルダをさすので、生成したコードはbinフォルダ内に出力されます。
2.4. もう一度jarファイルを出力します。
Processor使用側
2.5. 2.4.で出力したjarとjavapoet.jarをProcessor使用側プロジェクトにインポート。
2.6. 1.9.のように、Java Compiler-Annotation Processing-Factory Pathのところでjavapoet.jarを指定します。
2.7. リビルドをするとbinフォルダ内にcom/lyricaloriginal/SampleMain.classフォルダが生成されています。
3. 指定したAnnotationに関連するFactoryクラスを生成する。
##Processor提供側
3.1. TargetClass.java, TargetField.javaを作成。(以下のようなコードを書く。)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface TargetClass {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface TargetField {
}
3.2. MakeFactoryProcessor.javaを作成。
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"*"})
public class MakeFactoryProcessor extends AbstractProcessor{
private Filer _filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
processingEnv.getMessager().printMessage(Kind.NOTE, "プロセッサー初期化");
_filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
try{
for (TypeElement typeElement : annotations) {
for (Element element : roundEnv.getElementsAnnotatedWith(typeElement)) {
TargetClass classAnno = element.getAnnotation(TargetClass.class);
if(classAnno !=null){
createFactoryClass(element);
}
}
}
processingEnv.getMessager().printMessage(Kind.NOTE, "クラスファイル生成");
return true;
}catch(IOException ex){
ex.printStackTrace();
}
return false;
}
private void createFactoryClass(Element element) throws IOException{
ArgumentInfo argInfo = getArgumentInfo(element);
MethodSpec.Builder builder = MethodSpec.methodBuilder("create")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(TypeName.get(element.asType()))
.addStatement("return new $T(" + argInfo.join() + ")",
TypeName.get(element.asType()));
for(String arg : argInfo.getArgNames()){
builder.addParameter(String.class, arg);
}
MethodSpec create = builder.build();
String className = element.getSimpleName() + "Factory";
TypeSpec factory = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(create)
.build();
JavaFile javaFile = JavaFile.builder(
getPackageName(element), factory).
build();
javaFile.writeTo(_filer);
}
private String getPackageName(Element element){
List<String> packNames = new ArrayList<String>();
Element packageElem = element.getEnclosingElement();
while(packageElem != null){
String packName = packageElem.
getSimpleName().toString();
packNames.add(packName);
packageElem = packageElem.getEnclosingElement();
}
StringBuilder sb = new StringBuilder();
for(int i = packNames.size() - 1; i >= 0; i--){
if(sb.length() > 0){
sb.append(".");
}
sb.append(packNames.get(i));
}
return sb.toString();
}
private ArgumentInfo getArgumentInfo(Element element){
ArgumentInfo argInfo = new ArgumentInfo();
for(Element e : element.getEnclosedElements()){
if(e.getAnnotation(TargetField.class) != null){
argInfo.add(e.getSimpleName().toString());
}
}
return argInfo;
}
private class ArgumentInfo{
private List<String> _argNames = new ArrayList<String>();
public void add(String argName){
_argNames.add(argName);
}
public String[] getArgNames(){
return _argNames.toArray(new String[0]);
}
public String join(){
StringBuilder sb = new StringBuilder();
for(String argName : _argNames){
if(sb.length() > 0){
sb.append(",");
}
sb.append(argName);
}
return sb.toString();
}
}
}
@TargetClassが指定されているクラスにぶら下がるフィールドに@TargetFieldがついているものだけ引数として認識するための処理が入っています。
また、@TargetClassが付いているクラス名+Factoryというクラスを作りたいのでElement#getSimpleName()でクラス名情報を取得しているのがポイントです。
3.3. srcフォルダ内にMETA-INF/services フォルダを作成。その中にjavax.annotation.processing.Processorファイルを作成。そのファイル内に実行したいProcessorのクラス名を記載。
3.4. Processor提供側プロジェクトのsrcファイルを含んだjarファイルを出力。
##Processor使用側
3.5. 3.4.で出力したjarファイルをProcessor使用側プロジェクトにインポート。
3.6. 1.9.のように、Java Compiler-Annotation Processing-Factory Pathのところでインポートしたjarを指定します。
3.7. Item.java, Main.javaのコードを以下のように実装する。
public class Main {
public static void main(String[] args) {
Item item = ItemFactory.create("taro", "これは猫です。");
System.out.println(item.getName());
System.out.println(item.getDesc());
}
}
@TargetClass
public class Item {
@TargetField
private String _name;
@TargetField
private String _desc;
public Item(String name, String desc){
_name = name;
_desc = desc;
}
public String getName(){
return _name;
}
public String getDesc(){
return _desc;
}
}
3.8. 一度ビルドを試みてください。Main.javaでビルドエラーが発生しますが、ItemFactory.classをインポートするように指定すればビルドエラーが解消されます。
3.9. ビルドエラーが完全になくなったら、Main.javaを実行してください。以下のような実行結果が出ればOKです。
追伸
手前味噌ですが、ざっくり自分がやってきた方法を記載してきました。
今回作ったファクトリークラス生成のサンプルコードはGithubにあげていますので参考にしてみてください。
Processor提供側プロジェクト
https://github.com/LyricalMaestro/ProcessorProvider
Processor使用側プロジェクト
https://github.com/LyricalMaestro/ProcessorUser
実は、3.のProcessor提供側クラスを作るときに何度もトライアンドエラーをしました。そのときはいちいちjarファイルを作っては使用側にインポート…というのをやっててすごい手間でした。Processor用単体テストツールAptinaTestというのがあるみたいですが、それを使えばもっと楽にできたかもしれません。