注释处理,RoundEnvironment.processingOver()

问题描述 投票:10回答:1

在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;
}

碰巧我正在使用自定义Annotation处理器,我想在我的注释处理器中使用上面的代码片段。

我用这种方式尝试了上面的代码:

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

& 这条路:

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

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

我想知道在处理某一轮时使用roundEnv.processingOver()很有用的用例。

java annotation-processing java-annotations
1个回答
16
投票

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

当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),查看是否仍有未处理的注释。如果是这样,编译失败(仅在上一轮!)

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

一些注释处理器使用processingOver有点不同:它们缓冲每轮中生成的代码,并在上一轮实际上将其写入Filer。这允许解析对其他处理器的依赖性,但是阻止其他处理器找到由“仔细”处理器生成的代码。这是一个有点讨厌的策略,但如果生成的代码不打算在其他地方引用,我想这没关系。

并且有一些注释处理器,如上面提到的第三方Spring配置验证器:他们误解了一些东西,并以猴子和扳手的方式使用API​​。

为了更好地了解整个事物,请安装Dagger2,并尝试在类中引用Dagger生成的类,由另一个注释处理器使用(最好以某种方式使该处理器解析它们)。这将很快向您展示这些处理器如何应对多轮编译。大多数人只会让Javac崩溃。有些人会吐出数千个错误,填充IDE错误报告缓冲区并混淆编译结果。很少有人能够正确参与多轮编译,但如果失败则仍会发出大量错误。

“尽管存在错误,仍保持生成代码”部分专门用于减少编译失败期间报告的编译错误的数量。减少缺少类=减少缺少声明错误(希望如此)。或者,不要创建注释处理器,以煽动用户引用它们生成的代码。但是,当一些注释处理器生成代码时,您还需要处理情况,并使用您的注释进行注释 - 与“提前”声明不同,用户可能希望这只是开箱即用。


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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.