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

16

我有两个带有方法的类,我想将这两个类的方法合并到一个类中。

@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就只包含被编辑的类,因此结果如下所示(将newMethod()添加到接口ITest1中)

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

现在缺少method2。我不知道如何解决我的问题。有没有一种方法(环境),可以访问项目中的所有类?或者还有其他解决方法吗?

生成类的代码非常长,所以这里是一个简短的描述:我使用env.getElementsAnnotatedWith(Service.class)迭代元素并提取方法,然后将它们写入新文件中。

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

你在Eclipse中运行这个注解处理器吗? - John Ericksen
2
@johncarl的观点非常重要且必须正确。标准的Java编译器不允许增量编译。RoundEnvironment无法仅包含单个文件。Eclipse编译器是增量的,只编译已更改的文件。看起来这种逻辑对您不起作用,您需要找到一种方法告诉Eclipse始终重新编译给定的文件。我们可能可以尝试一些方法,但首先为了避免浪费精力,我们应该确信这只影响Eclipse编译。 - Pace
1
@Pace:有没有明确规定javac永远不使用增量编译?我认为ant和maven也有增量编译模式,所以我猜它们也不能正确地使用这样的注释处理器。 - Jörn Horstmann
1
javac不支持增量编译(https://dev59.com/eHE85IYBdhLWcg3w1HKF)。可以在javac之上实现增量编译,这就是Ant和Maven所做的。Eclipse实际上并不使用javac,而是使用ecj,即Eclipse Java Compiler。每个编译器都会以不同的方式实现处理增量编译的规则。可以创建一个别名到javac或自定义CompilerAdapter来欺骗Ant,但这不会欺骗Eclipse。Eclipse可能允许控制增量构建,但这并不能欺骗Ant。 - Pace
2个回答

9

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

我尝试做你想要的事情,即访问来自处理器的所有类,正如评论中所说,javac总是编译所有类,并且从RoundEnvironment中,我可以访问正在编译的所有类,每次(即使没有文件更改),只要所有类都显示在要编译的类列表中。

我已经对两个接口进行了几次测试,其中一个(A)依赖于另一个(B)(extends),我有以下场景:

  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: ...]
...

重要的细节在于编译器在第一种情况下将依赖项作为隐式对象加载进行编译。在第二种情况下,它将作为待编译对象的一部分加载(您可以看到在解析提供的类之后开始搜索其他路径的文件)。而且似乎隐式对象不包含在注释处理列表中。
有关编译过程的更多详细信息,请查看Compilation Overview。它没有明确说明选择哪些文件进行处理。
在这种情况下的解决方案是始终将所有类添加到编译器命令中。
--- 选项2 - 从Eclipse编译 ---
如果您从Eclipse编译,则增量构建会使处理器失败(未经测试)。但我认为您可以通过请求进行干净构建(项目>清理...,也未经测试)或编写一个Ant构建来绕过它,该构建始终清除类目录并设置Eclipse的Ant Builder
--- 选项3 - 使用构建工具 ---
如果您使用其他构建工具,如Ant、Maven或Gradle,则最好的解决方案是将源代码生成与编译分开。您还需要在先前的步骤中(或者在Maven / Gradle中使用多项目构建时的单独子项目中)编译处理器。这将是最佳方案,因为:
  1. 对于处理步骤,您始终可以进行完整的清理“编译”,而不实际编译代码(使用javac选项-proc:only仅处理文件)
  2. 有了生成的源代码,如果您使用Gradle,它将足够聪明,如果未更改生成的源文件,则不重新编译它们。Ant和Maven只会重新编译所需的文件(生成的文件及其依赖项)。

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


很好的回答,如果你能找到一些官方链接,证明javac不支持增量编译,那么你就赢得了悬赏。 - Jörn Horstmann
我认为规范并没有要求编译器必须是增量的或非增量的。这似乎是编译器实现者的决定。在Filer javadoc(http://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/Filer.html)中,甚至引用了关于增量或非增量的说明,虽然没有明确表示,但暗示着这真的取决于实现。 “这些信息可以在增量环境中用于确定重新运行处理器或删除生成的文件的需要。非增量环境可以忽略原始元素信息。” - visola
@JörnHorstmann 在阅读了很多相关资料后,我的结论是Java只涉及到语言和JVM,编译器没有规范。规范只解释了字节码的格式。如果你想手动生成字节码并使用笔记本电脑和笔处理注释,那也没问题!只要你遵守注释处理的API并在最后生成正确的字节码格式即可。因此,是否增量取决于实现。一个简单的例子是Eclipse编译器,它也生成标准字节码但是是增量式的。 - visola
值得一提的是,增量编译通常不是编译器的职责。其他主要的编译器(如G++/gcc等)也不支持自己的增量编译,需要像make这样的工具进行支持。 - Pace

3

NetBeans的@Messages注释会为同一包中的所有类生成单个Bundle.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();
  }
}

通过这样做,即使编译只在包中的一个源文件上调用,也可以确保所有(顶级)元素在包中一起处理。
如果您知道要查找注释的位置(包中的顶级元素甚至是包中的任何元素),您应该能够始终获取所有这些元素的列表。

这对我不起作用,第一个循环pkg.getEnclosedElements()返回元素,但注释仅由正在编译的元素返回,其他getAnnotation返回null,尽管它们在类上定义了它。(我检查了上面链接中的注释处理器,并添加了if(roundEnv.processingOver()) return false;) 编辑:我通过javac运行此代码。 - RookieGuy

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