私はJavaコードを生成することになっているAnnotationProcessorを書いています。特定の既存のインターフェイスから派生インターフェイスを生成する必要があります。
この目的のために、元の入力コードのインポートステートメントを見つけて、生成されたJavaファイルに出力できるようにする必要があります。
これはどのように行うことができますか?
私はJavaコードを生成することになっているAnnotationProcessorを書いています。特定の既存のインターフェイスから派生インターフェイスを生成する必要があります。
この目的のために、元の入力コードのインポートステートメントを見つけて、生成されたJavaファイルに出力できるようにする必要があります。
これはどのように行うことができますか?
注釈プロセッサを使用してインポートステートメントを取得することはできません。ただし、取得できるのは、そのクラスで使用される型です。これはさらに優れています。
使用されているすべての型に import ステートメントがあるわけではないため、ソース コードからの import ステートメントは、クラスで使用されている型を分析するのに十分ではありません。実際のステートメントのみが本当に必要な場合は、ソース ファイルを直接読み取ることができます。
ステートメントだけを見ると、いくつかの問題があります。
java.util.Date date;
アノテーション プロセッサとMirror APIを使用すると、プロパティの型、メソッド パラメータ、メソッドの戻り値の型などを取得できます。基本的には、メソッドまたはブロックに含まれていないすべての宣言の型です。これで十分です。
クラスのすべての要素を分析し、その型を Set に格納する必要があります。このタスクに役立つユーティリティ クラスがいくつかあります。java.lang
パッケージ内のタイプは常に暗黙的にインポートされるため、無視できます。
最小限の注釈プロセッサは次のようになります。
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.TypeElement;
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("*")
public class Processor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
ImportScanner scanner = new ImportScanner();
scanner.scan(roundEnv.getRootElements(), null);
Set<String> importedTypes = scanner.getImportedTypes();
// do something with the types
return false;
}
}
ここでのスキャナーElementScanner7
は、訪問者のパターンに基づいて拡張されています。すべての要素が実際にインポート可能な型を含むことができるわけではないため、いくつかのビジター メソッドのみを実装し、種類によって要素をフィルター処理します。
import java.util.HashSet;
import java.util.Set;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.ElementScanner7;
public class ImportScanner extends ElementScanner7<Void, Void> {
private Set<String> types = new HashSet<>();
public Set<String> getImportedTypes() {
return types;
}
@Override
public Void visitType(TypeElement e, Void p) {
for(TypeMirror interfaceType : e.getInterfaces()) {
types.add(interfaceType.toString());
}
types.add(e.getSuperclass().toString());
return super.visitType(e, p);
}
@Override
public Void visitExecutable(ExecutableElement e, Void p) {
if(e.getReturnType().getKind() == TypeKind.DECLARED) {
types.add(e.getReturnType().toString());
}
return super.visitExecutable(e, p);
}
@Override
public Void visitTypeParameter(TypeParameterElement e, Void p) {
if(e.asType().getKind() == TypeKind.DECLARED) {
types.add(e.asType().toString());
}
return super.visitTypeParameter(e, p);
}
@Override
public Void visitVariable(VariableElement e, Void p) {
if(e.asType().getKind() == TypeKind.DECLARED) {
types.add(e.asType().toString());
}
return super.visitVariable(e, p);
}
}
このスキャナーは、一連の型を完全修飾パスとして返します。まだ考慮すべき点と実装すべき点がいくつかあります。
java.lang
セットには、同じパッケージの要素と型が含まれていますjava.util.List<String>
TypeKind.DECLARED
インポート可能な型である唯一の種類の要素ではありません。またTypeKind.ARRAY
、実際に宣言された型を確認して取得します。else if(e.asType().getKind() == TypeKind.ARRAY) // ...
クラスに別のものを追加する代わりに、代わりTypeKindVisitor7
に使用できますElementScanner6
のTypeKindVisitor6
などの実装を使用します。標準の SDK クラスから import ステートメントを取得する方法がないようです (少なくとも SDK 5-6-7 では)。
ただし、SUN/Oracle の tools.jar 内のいくつかのクラスを使用できます。
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
public class MyProcessor extends AbstractProcessor {
@Override
public void init(ProcessingEnvironment env) {
tree = Trees.instance(env);
}
@Override
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnvironment) {
for( Element rootElement : roundEnvironment.getRootElements() ) {
TreePath path = tree.getPath(rootElement);
System.out.println( "root element "+rootElement.toString() +" "+path.getCompilationUnit().getImports().size() );
}
....
Maven 経由で Java ツールの jar を取得するには、このスレッドを参照してください。
TreePathScanner (tools.jar からも) を使用する代替手段があるはずですが、visitImport メソッドがトリガーされることはありませんでした。
<dependency>
<groupId>org.jvnet.sorcerer</groupId>
<artifactId>sorcerer-javac</artifactId>
<version>0.8</version>
</dependency>
この依存関係を使用すると問題は解決しますが、残念ながら今回は Java 1.7 までサポートされており、Java 1.8 ソースを正しくコンパイルできません。私の解決策は少しハックですが、この依存関係を使用せずにJava 1.8ソースを使用しても機能します
public final class SorcererJavacUtils {
private static final Pattern IMPORT = Pattern.compile("import\\s+(?<path>[\\w\\\\.]+\\*?)\\s*;");
// com.sun.tools.javac.model.JavacElements
public static Set<String> getImports(Element element, ProcessingEnvironment processingEnv) {
Elements elements = processingEnv.getElementUtils();
Class<?> cls = elements.getClass();
try {
Method getTreeAndTopLevel = cls.getDeclaredMethod("getTreeAndTopLevel", Element.class);
getTreeAndTopLevel.setAccessible(true);
// Pair<JCTree, JCCompilationUnit>
Object treeTop = getTreeAndTopLevel.invoke(elements, element);
if (treeTop == null)
return Collections.emptySet();
// JCCompilationUnit
Object toplevel = getFieldValue("snd", treeTop);
return SorcererJavacUtils.<List<Object>>getFieldValue("defs", toplevel).stream()
.map(Object::toString)
.map(IMPORT::matcher)
.filter(Matcher::find)
.map(matcher -> matcher.group("path"))
.collect(Collectors.toSet());
} catch(Exception ignored) {
return Collections.emptySet();
}
}
private static <T> T getFieldValue(String name, Object obj) throws IllegalAccessException, NoSuchFieldException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
return (T)field.get(obj);
}
private SorcererJavacUtils() {
}
}