在注解处理过程中,为什么注解处理器会处理它自己生成的文件,即使该文件没有被任何受支持的注解标注?
我想理解这个注解处理行为。具体来说,我对下面情景中的第二轮有一个问题。(随后还有一个关于编译器行为的小问题。)
假设我的非常简单的处理器 p0 支持一个注解 @X。假设它在第1 轮被调用,带有一个对 @X 注解的 TypeElement。假设因此它生成了一个简单的Java源文件,导致一个编译器警告。比如:
package foo;
public class FooBecauseOfX {} // warning; implicit constructor
没有其他处理器。
我注意到(不包括虚拟的第0轮)注解处理共有三轮。(我天真地以为只有两轮。)
以下是各轮发生的情况,我试图把这一行为理解得透彻:
(Primordial:“虚拟”的第0轮发生,产生了一些根元素。我理解这一点。)
我理解三轮中的第一轮:
- 注解集合按预期为
@X(在被注解的对象上找到了@X) - 根元素集合(来自第0轮)是被注解的类型元素集合的超集,符合预期(还有其他对象正在编译)
roundEnvironment.getElementsAnnotatedWith(x)返回了相应的TypeElement,符合预期- (p0 生成
FooBecauseOfX。)
三轮中的第二轮让我吃惊:在处理结束前,p0被要求再次处理,即使没有其他被注解为 @X 的对象,包括 FooBecauseOfX:
- 注解集合为空
- 根元素集合正好是
FooBecauseOfX(刚写好的文件;注意它完全没有注解) - 显然现在
roundEnvironment.getElementsAnnotatedWith(x)返回一个空集合
我理解三轮中的第三轮:p0被要求再进行某种处理(最后一轮的规则略有不同):
- 注解集合如预期为空
- 根元素集合如预期为空
roundEnvironment.processingOver()返回true,符合预期
恼人地、并且有些神秘地,因在第一轮结束时因处理 FooBecauseOfX 而产生的编译器警告在整个过程被发出三次。
我的问题:
- 为什么在第2轮要让 p0 处理?生成的文件根本没有注解,因此也没有 p0 支持的注解。我理解处理器在空注解集合存在时必须健壮;我只是不能理解为什么 p0 会在这里被卷入。是不是因为“若在给定轮次被请求处理一个处理器,它将被请求在后续轮次继续处理,包括最后一轮,即使没有注解可处理”?
- (只是有点烦人,主要是。)有没有办法避免编译器把警告输出三次?看到警告竟然显示了三次,让我有些意外,因为该文件是由第1轮生成的。
解决方案
第1轮(共3轮)之所以发生,是因为初始输入集合并非空:这里请求javac至少编译一个文件。由于这个文件上关注的注解是 p0,所以将 p0 添加入到参与此次编译过程的注解处理器集合中,并且会因之在所有未来轮次被调用,无论以后是否还有它感兴趣的元素被注解(在这个例子中是 @X),还是没有。为什么?因为规范是这么规定的:来自 javax.annotation.processing.Processor 的Javadoc:
如果在给定轮次被请求处理一个处理器,它将在后续轮次中继续被请求处理,包括最后一轮,即使没有注解可处理。
第2轮(共3轮)之所以发生,是因为在第1轮中生成了源文件。
停止把思路局限在你的处理器上,而是要从整个编译过程来考虑。
如果任何注解处理器(无论是 p0 还是真正参与过程的其他处理器)生成了新的源文件,这就意味着必须发生新的轮次。就此而言,编译器本身也是轮次系统的一部分。
最后,第3轮(共3轮)之所以发生,是因为总会有一个最终轮。该最终轮没有源文件,processingOver = true。如果在这一轮中某个AP生成了新的源文件,filer将抛出异常,因为这在逻辑上已经不再可能(那样最终轮就不能作为最终轮,至少还需要再发生2 轮)。
这必须与第2轮分开发生,因为javac不会把这些轮次合并:
有些注解处理过程可能在第2轮中生成更多源文件。事后看来可以说:哦,但不会发生,但 javac在它真正开始第2轮时并不知道这一点。因此第2轮不是最终轮。即使在第2轮完成后,javac 也可能得出结论:哎呀,我本来可以把它设为最终轮——也无所谓。javac 没有时光机,不能回头。相反,它只是把最终轮在第2轮之后开始。
之所以能这样,是因为第2轮没有生成任何东西。如果第2轮生成了更多源文件,第3轮就不会是最终轮(而且总会有一个最终轮,因此至少还会有第4轮)。
总结
不确定到底让你感到困惑的是什么,但:
如果你的问题是:为什么在第2轮被调用,而第2轮中没有“有趣的”(被 @X 注解的)事项对 p0 有影响?
答案是:因为规范就是这么规定的。一旦被调用,你就在每一轮中继续被调用,只是传入的 Set<? extends TypeElement> annotations 参数变成了空集合。
如果你的问题是:为什么会有第三轮,而这一步本可以在两轮内完成?
答案是:因为需要对所有处理器以 processingOver() = true进行调用,javac需要在轮次开始时就知道它是否是最终轮。它在第2轮开始时不可能知道(也许你的AP会在接收零个 annotations 的情况下生成一个文件)。你可以按照规范这样做。Javac不知道你不会这样做,也不会试图通过分析你的代码来解决停机问题并推断出结论。因此,尽管第2轮本来可以是最终轮,但它不是最终轮,因此会发生第3轮。