注解处理,RoundEnvironment.processingOver()

13

阅读Java中一个自定义注解处理器的代码时,我在处理器的process方法中注意到了这段代码:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
  if (!roundEnv.errorRaised() && !roundEnv.processingOver()) {
    processRound(annotations, roundEnv);
  }
  return false;
}

恰好我也在开发一个自定义的注解处理器,想要在我的注解处理器中使用上面的代码片段。

我尝试了上面的代码:

if (!roundEnv.errorRaised() && !roundEnv.processingOver()) {
    processRound(annotations, roundEnv);
}
return false;

& 这样做:

if (!roundEnv.errorRaised()) {
    processRound(annotations, roundEnv);
}
return false;

但我没有注意到处理器的行为有任何变化。 我理解!roundEnv.errorRaised()的检查,但我看不出!roundEnv.processingOver()有什么用。

我想知道在处理某个轮次时使用roundEnv.processingOver()有什么用例。

1个回答

26

这两项检查都很重要,但只有在同一项目中同时运行多个注解处理器时才会注意到它们的效果。让我解释一下。

Javac在任何原因(例如缺少类型声明或解析错误)导致编译失败时,不会立即终止。相反,它会收集尽可能多有关错误的信息,并尝试以有意义的方式向用户显示该信息。此外,如果存在注解处理器,并且错误是由于缺少类型或方法声明引起的,则Javac将尝试运行这些处理器并重试编译,以期望它们生成缺少的代码。这称为“多轮编译”。

编译序列如下:

  1. 主要轮次(可能包括代码生成);
  2. 若干可选代码生成轮次;新的轮次将一直发生,直到注解处理器不生成任何东西为止;
  3. 最后一个轮次;在此轮期间生成的代码不会被注解处理器处理。

每个轮次都是一次全面尝试来编译代码。除了最后一轮之外的每个轮次都会重新运行先前由注解处理器生成的代码上的每个注解处理器。

这个精彩的序列允许使用像Dagger2和Android-Annotated-SQL之类的库所推广的方法:在源代码中引用“尚不存在”的类,并允许注解处理器在编译期间生成它:

// this would fail with compilation error in absence of Dagger2
// but annotation processor will generate the Dagger_DependencyFactory
// class during compilation
Dagger_DependencyFactory.inject(this);

有些人认为这种技术不可靠,因为它依赖于在源代码中使用不存在的类,并将源代码紧密地绑定到注解处理(并且与IDE代码完成不太兼容)。但是该实践本身是合法的,并且按照Javac开发人员的意图运作。


那么,所有这些与Spring的注解处理器有什么关系呢?

TL;DR:您问题中的代码存在错误。

正确使用这些方法的方式如下:

对于 errorRaised

  1. 如果您的处理器生成新的公开可见类(可能会像上面描述的那样在用户代码中“提前”使用),则必须非常弹性:保持生成,尽可能忽略缺失的部分和不一致性,并忽略 errorRaised。这可以确保在 Javac 进行错误报告时留下尽可能少的遗漏内容。
  2. 如果您的代码不生成新的公共可见类(例如,因为它只创建包私有类,而其他代码将在运行时反射地查找它们,请参见 ButterKnife),则应尽快检查 errorRaised,并在其返回 true 时立即退出。这将简化您的代码并加速出错编译。

对于 processingOver

  1. 如果当前轮不是最后一轮(processingOver 返回 false),请尝试尽可能多地生成输出;在用户代码中忽略缺失的类型和方法(假定其他注解处理器可能会在后续轮次中生成它们)。但仍然尽可能地生成,以防其他注解处理器需要使用。例如,如果您在每个带有 @Entity 注释的类上触发代码生成,则应遍历这些类并尝试为每个类生成代码,即使之前的类存在错误或缺少的方法也是如此。个人而言,我只需将每个单独的生成单元都包装在 try-catch 中,并检查 processingOver:如果它为 false,则忽略错误并继续迭代注解和生成代码。这允许 Javac 在运行它们直到满意以打破由不同注解处理器生成的代码之间的循环依赖。
  2. 如果当前轮不是最后一轮(processingOver 返回 false),并且某些上一轮的注解未被处理(每当处理失败时,我都会记住它们因为异常而未被处理),请在这些注解上重试处理。
  3. 如果当前轮是最后一轮(processingOver 返回 true),请查看是否仍有未处理的注解。如果是,则失败编译(仅在 last 轮中!)

上述顺序是使用 processingOver预期 方式。

有些注解处理器在使用processingOver时有些不同:它们会在每个轮次缓冲生成的代码,并在最后一轮时将其实际写入Filer中。这样允许解决对其他处理器的依赖,但防止“小心”处理器生成的代码被“其他”处理器找到。虽然这是有点卑鄙的策略,但如果生成的代码不打算在其他地方引用,我想这也还好。

同时还有像上述第三方Spring配置验证器那样的注解处理器:它们会误解一些事情,并以猴子和扳手的方式使用API。

为了更好地理解整个过程,请安装Dagger2,并尝试在另一个注解处理器使用的类中引用Dagger生成的类(最好以使该处理器解析它们的方式)。这将很快向您展示这些处理器如何处理多轮编译。大多数处理器只会在Javac中崩溃并抛出异常。有些会输出成千上万的错误,填满IDE错误报告缓冲区并混淆编译结果。极少数会正确参与多轮编译,但如果失败仍会产生大量错误。

“即使存在错误也继续生成代码”的部分特别是为了在编译失败时减少编译错误的数量。更少的丢失类=更少的缺少声明错误(希望如此)。或者,不要创建激励用户引用由它们生成的代码的注解处理器。但是,您仍然必须应对这样的情况:某些注解处理器会生成使用您的注解注释的代码-不像“提前声明”,用户将期望它能够直接工作。


回到原始问题上:由于Spring配置验证处理器不应该生成任何代码(希望我没有深入研究),但应始终报告扫描配置中的所有错误,因此它理想情况下应该像这样工作:忽略errorRaised并推迟配置扫描直到processingOver返回true:这将避免在多次编译轮次期间多次报告相同的错误,并允许注解处理器生成新的配置片段。

不幸的是,所涉及的处理器似乎已经被抛弃(自2015年以来没有提交),但作者在Github上很活跃,因此您可能可以向他们报告此问题。

同时,我建议您学习思路清晰的注解处理器,例如Google Auto,Dagger2或我的小研究项目


这个救了我的一天,我都快疯了,点赞! - Niton

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