注释使用多个源文件创建一个文件的处理器

2022-09-03 07:31:31

我有两个带有方法的类,我想将两个类的方法组合成一个类。

@Service("ITestService")
public interface ITest1
{
   @Export
   void method1();
}

@Service("ITestService")
public interface ITest2
{
   @Export
   void method2();
}

结果应该是:

public interface ITestService extends Remote
{
  void method1();
  void method2();
}

我的 AnnotationProcessor 的第一次运行会生成正确的输出(因为 RoundEnvironment 包含这两个类)。

但是,如果我编辑其中一个类(例如添加一个新方法),RoundEnviroment只包含编辑过的类,因此结果是followwing(将newMethod()添加到接口ITest1)

public interface ITestService extends Remote
{
  void method1();
  void newMethod();
}

现在缺少方法 2。我不知道如何解决我的问题。有没有办法(环境)来访问项目中的所有类?还是有另一种方法可以解决这个问题?

生成类的代码很长,所以这里简要描述一下我如何生成类。我循环访问了 Elements with 并提取了这些方法,并将它们写入新文件中:env.getElementsAnnotatedWith(Service.class)

FileObject file = null;
file = filer.createSourceFile("com/test/" + serviceName);
file.openWriter().append(serviceContent).close();

答案 1

-- 选项 1 - 从命令行手动编译---

我试图做你想做的事,即从处理器访问所有类,正如人们所评论的那样,javac总是编译所有类,并且从RoundEnvironment,我确实可以访问所有正在编译的类,每次(即使没有文件更改),只有一个小细节:只要所有类都显示在要编译的类列表中。

我已经用两个接口做了一些测试,其中一个(A)依赖于(B)另一个(扩展),我有以下场景:

  1. 如果我要求编译器仅显式编译具有依赖项 (A) 的接口,将 java 文件的完整路径传递到命令行中,然后将输出文件夹添加到类路径中,则仅处理我传递到命令行中的接口。
  2. 如果我只显式编译 (A) 而不将输出文件夹添加到类路径,编译器仍然只处理接口 (A)。但它也给了我警告:Implicitly compiled files were not subject to annotation processing.
  3. 如果我使用*或将两个类都传递给编译器到命令行中,那么我得到预期的结果,两个接口都被处理。

如果将编译器设置为详细,则会收到一条显式消息,显示每轮将处理哪些类。这是我在显式传递接口(A)时得到的:

Round 1:
input files: {com.bearprogrammer.test.TestInterface}
annotations: [com.bearprogrammer.annotation.Service]
last round: false

这就是我添加这两个类时得到的:

Round 1:
input files: {com.bearprogrammer.test.AnotherInterface, com.bearprogrammer.test.TestInterface}
annotations: [com.bearprogrammer.annotation.Service]
last round: false

在这两种情况下,我都看到编译器解析这两个类,但顺序不同。对于第一种情况(仅添加一个接口):

[parsing started RegularFileObject[src\main\java\com\bearprogrammer\test\TestInterface.java]]
[parsing completed 15ms]
[search path for source files: src\main\java]
[search path for class files: ...]
[loading ZipFileIndexFileObject[lib\processor.jar(com/bearprogrammer/annotation/Service.class)]]
[loading RegularFileObject[src\main\java\com\bearprogrammer\test\AnotherInterface.java]]
[parsing started RegularFileObject[src\main\java\com\bearprogrammer\test\AnotherInterface.java]]

对于第二种情况(添加了所有接口):

[parsing started RegularFileObject[src\main\java\com\bearprogrammer\test\AnotherInterface.java]]
...
[parsing started RegularFileObject[src\main\java\com\bearprogrammer\test\TestInterface.java]]
[search path for source files: src\main\java]
[search path for class files: ...]
...

这里的重要细节是,在第一种情况下,编译器将依赖项作为编译的隐式对象加载。在第二种情况下,它将将其作为待编译对象的一部分加载(您可以看到这一点,因为它在解析提供的类后开始搜索其他路径中的文件)。而且隐式对象似乎不包括在注释处理列表中。

有关编译过程的更多详细信息,请查看此编译概述。这并没有明确说明哪些文件被拾取进行处理。

在这种情况下,解决方案是始终将所有类添加到编译器的命令中。

--- 选项 2 - 从 Eclipse ---编译

如果您从 Eclipse 编译,增量构建将使处理器失败(尚未对其进行测试)。但我认为你可以绕过这个要求一个干净的构建(Project > Clean...,也没有测试过它),或者编写一个总是清理类目录的Ant构建,并从Eclipse设置一个Ant Builder。

--- 选项 3 - 使用生成工具---

如果您使用的是其他构建工具,如Ant,Maven或Gradle,最好的解决方案是将源代码生成放在与编译分开的步骤中。您还需要在单独的上一步中编译处理器(如果在 Maven/Gradle 中使用多项目构建,则编译单独的子项目)。这将是最好的方案,因为:

  1. 对于处理步骤,您始终可以执行完全干净的“编译”,而无需实际编译代码(使用javac中的选项仅处理文件)-proc:only
  2. 有了生成的源代码,如果您使用的是Gradle,那么如果生成的源文件没有更改,它将足够聪明,不会重新编译它们。Ant和Maven只会重新编译所需的文件(生成的文件及其依赖项)。

对于第三个选项,您还可以设置一个 Ant 构建脚本,以从 Eclipse 生成这些文件,作为在 Java 构建器之前运行的构建器。在某个特殊文件夹中生成源文件,并将其添加到 Eclipse 中的类路径/构建路径中。


答案 2

NetBeans @Messages注释为每个包中的所有类生成单个捆绑.java文件。由于注释处理器中的以下技巧,它可以与增量编译一起正常工作:

Set<Element> toProcess = new HashSet<Element>();
for (Element e : roundEnv.getElementsAnnotatedWith(Messages.class)) {
  PackageElement pkg = findPkg(e);
  for (Element elem : pkg.getEnclosingElements()) {
    if (elem.getAnnotation(Message.class) != null) {
      toProcess.add(elem);
    }
  }
}
// now process all package elements in toProcess 
// rather just those provided by the roundEnv

PackageElement findPkg(Element e) {
  for (;;) {
    if (e instanceof PackageElement) {
      return (PackageElement)e;
    }
    e = e.getEnclosingElement();
  }
}

通过这样做,可以确保包中的所有(顶级)元素一起处理,即使编译仅在包中的单个源文件上调用。

如果您知道在哪里可以找到注释(包中的顶级元素,甚至是包中的任何元素),您应该能够始终获得所有此类元素的列表。


推荐