如何通过注解处理器访问TypeUse注解

11

问题:

  1. 通过注解处理器是否可以访问带有@Target(ElementType.TYPE_USE)注解的元素?
  2. 通过注解处理器是否可以访问带注解的类型限定边界?

如果有相关文档链接,欢迎提供。

背景信息:

注解:

@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.SOURCE)
public @interface TypeUseAnno {}

一个示例类:

public class SomeClass extends HashMap<@TypeUseAnno String, String> {}
处理器:
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("base.annotations.TypeUseAnno")
public class Processor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Initialized.");
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Invoked.");
        for (TypeElement annotation : annotations) {
            this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "" + roundEnv.getElementsAnnotatedWith(annotation));
        }
        return true;
    }
}

在类路径上使用Processor编译上述的SomeClass将显示"Intialized"消息,但是process(...)方法从未被调用。 通过为处理器添加另一个注释@Target(ElementType.PARAMETER),当注释存在于方法参数上时正常工作。如果方法参数带有@TypeUseAnno注释,则处理过程将再次忽略该元素。

1个回答

7
TYPE_USE 注解有些棘手,因为编译器对它们的处理方式与“旧用法”注解不同。
正如您正确观察到的那样,它们不会传递给注解处理器,因此您的 process() 方法将永远不会接收到它们。
那么如何在编译时使用它们呢?
在引入这些注解的 Java 8 中,还引入了一种新的连接到 Java 编译的方式。现在,您可以将侦听器附加到编译任务,并触发自己对源代码的遍历。因此,访问注解的任务分为两个步骤:
1. 连接到编译器。 2. 实现您的分析器。
第一步: 在 Java 8 中,有两种挂钩编译器的选项:
1. 使用 new compiler plugin API。 2. 使用注解处理器。
我没有经常使用选项 #1,因为它需要明确指定为 javac 参数。因此,我将描述选项 #2:
你需要将TaskListener附加到适当的编译阶段。有各种不同的阶段。下面是唯一一个阶段,在此期间您可以访问表示完整源代码(包括方法体)的语法树(请记住,即使在局部变量声明上也可以使用TYPE_USE注释)。
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EndProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        Trees trees = Trees.instance(env);
        JavacTask.instance(env).addTaskListener(new TaskListener() {

            @Override
            public void started(TaskEvent taskEvent) {
                // Nothing to do on task started event.
            }

            @Override
            public void finished(TaskEvent taskEvent) {
                if(taskEvent.getKind() == ANALYZE) {
                    new MyTreeScanner(trees).scan(taskEvent.getCompilationUnit(), null);
                }
            }
            
        });
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // We don't care about this method, as it will never be invoked for our annotation.
        return false;
    }
}

广告2. 现在MyTreeScanner可以扫描完整的源代码并找到注释。无论您使用Plugin还是AnnotationProcessor方法,都适用。这仍然很棘手。您必须实现TreeScanner,或者通常扩展TreePathScanner。 这代表了一个访问者模式,您必须正确分析哪些元素是您感兴趣的要访问的。

让我们举一个简单的例子,可以在本地变量声明时做出反应(给我5分钟):

class MyTreeScanner extends TreePathScanner<Void, Void> {
    private final Trees trees;

    public MyTreeScanner(Trees trees) {
        this.trees = trees;
    }

    @Override
    public Void visitVariable(VariableTree tree, Void aVoid) {
        super.visitVariable(variableTree, aVoid);
        // This method might be invoked in case of
        //  1. method field definition
        //  2. method parameter
        //  3. local variable declaration
        // Therefore you have to filter out somehow what you don't need.
        if(tree.getKind() == Tree.Kind.VARIABLE) {
            Element variable = trees.getElement(trees.getPath(getCurrentPath().getCompilationUnit(), tree));
            MyUseAnnotation annotation = variable.getAnnotation(MyUseAnnotation.class);
            // Here you have your annotation.
            // You can process it now.
        }
        return aVoid;
    }
}

这是一个非常简短的介绍。如果您想查看真实的示例,可以查看以下项目源代码:https://github.com/c0stra/fluent-api-end-check/tree/master/src/main/java/fluent/api/processors 在开发此类功能时,拥有良好的测试非常重要,这样您就可以调试、反向工程和解决在此领域中遇到的所有棘手问题;为此,您还可以从这里获得灵感:https://github.com/c0stra/fluent-api-end-check/blob/master/src/test/java/fluent/api/EndProcessorTest.java 也许我的最后一条评论是,由于注释在javac中的使用方式确实不同,因此存在一些限制。例如,它不适用于触发Java代码生成,因为编译器不会选择在此阶段创建的文件进行进一步编译。

1
Java 8的注意事项:确保tools.jar在类路径中,因为它位于其他库(jdk/lib vs jdk/jre/lib)的不同位置。 尽管这似乎更加繁琐,因为com.sun.source API似乎比javax.lang.model API更细粒度,但它为我提供了所有我需要的工具。 - Trinova
是的,感谢您添加这个要点。另请注意,自Java 9以来,不再需要tools.jar,而且javax.lang.model(公共) API可以直接使用。另一方面,在Java 9中内部的com.sun.sourceAPI根本不可用。后者肯定更灵活,但如果您计划迁移到Java 9或更高版本,则最好使用公共API。 - Ondřej Fischer
这个答案可能会让一些人误以为你不能通过注解处理器访问TYPE_USE注解。但实际上,你可以通过TypeMirror(因为它被注释了类型)来访问它们,因此你不能像普通注解那样进行全局搜索和声明。但是,你可以通过TypeMirror.getAnnotationMirrors()检查变量、字段等是否有TYPE_USE注解 - Adam Gent

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接