注解处理,RoundEnvironment.processingOver()

Annotation processing, RoundEnvironment.processingOver()

在阅读Java中custom annotation processor的代码时, 我在处理器的 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() 有用的用例。

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

当 Javac 由于任何原因编译失败时(例如由于缺少类型声明或解析错误),它不会立即终止。相反,它将尽可能多地收集有关错误的信息,并尝试以有意义的方式向用户显示该信息。此外,如果有注释处理器,并且错误是由缺少类型或方法声明引起的,Javac 将尝试 运行 这些处理器并重试编译,希望它们生成丢失的代码。这叫做"multi-round compilation".

编译序列如下所示:

  1. 第一轮(可能有代码生成);
  2. 几个可选的代码生成轮次;新一轮将发生,直到注释处理器没有生成任何内容;
  3. 最后一轮;本轮生成的代码将不进行注释处理。

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

这个美妙的序列允许使用方法,由 Dagger2 和 Android-Annotated-SQL 等库推广:引用 尚未存在 class 在您的源代码中,并让注释处理器在编译期间生成它:

// 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);

有些人认为该技术有问题,因为它依赖于在源代码中使用不存在的 classes,并将源代码与注释处理紧密相关(并且不能很好地与 IDE 代码一起使用完成)。但这种做法本身是合法的,并且符合 Javac 开发人员的预期。


那么,所有这些与您问题中的 Spring 注释处理器有什么关系?

TL;DR:您问题中的代码有问题。

正确使用这些方法的方法是这样的:

对于 errorRaised:

  1. 如果您的处理器生成新的公开可见的 classes(可以在用户代码中使用 "ahead of time",如上所述),您必须具有超级弹性:继续生成,忽略丢失的位和不一致的地方,忽略 errorRaised。这确保了在 Javac 继续它的错误报告狂欢时,你留下的东西尽可能少。
  2. 如果您的代码没有生成新的公开可见的 classes(例如,因为它只创建包私有的 classes,其他代码会反射性地在 运行时间,参见 ButterKnife),那么你应该尽快检查 errorRaised,如果 returns 为真,则立即退出。这将简化您的代码并加速错误编译。

对于 processingOver:

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

上面的序列是预期使用processingOver的方式。

一些注释处理器使用 processingOver 有点不同:它们缓冲每一轮生成的代码,并在最后一轮实际将其写入 Filer。这允许解决对其他处理器的依赖性,但会阻止 other 处理器找到 "careful" 处理器生成的代码。这是一个有点讨厌的策略,但如果生成的代码不打算在其他地方引用,我想这没问题。

还有像上面提到的第三方Spring配置验证器这样的注解处理器:他们误解了一些东西,用猴子和扳手的方式使用API。

为了更好地了解整个事情的要点,安装 Dagger2,并尝试在 classes 中引用 Dagger 生成的 classes,由另一个注释处理器使用(最好是在某种程度上,这将使该处理器解决它们)。这将快速向您展示这些处理器如何处理多轮编译。大多数只会使 Javac 异常崩溃。有些会吐出数千个错误,填充 IDE 错误报告缓冲区并混淆编译结果。很少有人会正常参与多轮编译失败还是会吐出很多错误

"keep generating code despite existing errors" 部分专门用于减少编译失败期间报告的编译错误的数量。更少的缺失 classes = 更少的缺失声明错误(希望如此)。或者,不要创建注释处理器,这会煽动用户引用它们生成的代码。但是您仍然必须应对这样的情况,当一些注释处理器生成代码时,用您的注释进行注释 — 与 "ahead of time" 声明不同,用户会期望它开箱即用。


回到最初的问题:由于 Spring 配置验证处理器预计不会生成任何代码(希望我没有深入研究它),但应该始终报告扫描配置中的所有错误, 它应该像这样理想地工作:忽略 errorRaised 并推迟配置扫描直到 processingOver returns 为真:这将避免在多轮编译期间多次报告相同的错误,并允许注释处理器生成新的配置文件。

遗憾的是,有问题的处理器看起来已被废弃(自 2015 年以来没有提交),但作者在 Github 上很活跃,所以也许你可以向他们报告这个问题。

与此同时,我建议您向经过深思熟虑的注释处理器学习,例如 Google Auto、Dagger2 或 my tiny research project