Annotation processing, RoundEnvironment.processingOver()

在用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

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

当 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. 如果您的处理器生成了新的公开可见的类(可以像上面描述的那样“提前”在用户代码中使用),则必须具有超强的弹性:继续生成,尽可能忽略缺失的位和不一致,并忽略 。这可以确保,当Javac继续错误报告狂欢时,你尽可能少地留下丢失的东西。errorRaised
  2. 如果您的代码没有生成新的公开可见的类(例如,因为它只创建包私有类,而其他代码将在运行时反射性地查找它们,请参阅 ButterKnife),那么您应该尽快检查,并在返回 true 时立即退出。这将简化您的代码并加快错误编译的速度。errorRaised

为:processingOver

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

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

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

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

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

“尽管有现有错误,仍继续生成代码”部分专门用于减少编译失败期间报告的编译错误数。更少的缺失类 = 更少的缺失声明错误(希望如此)。或者,不要创建注释处理器,这会诱使用户引用由它们生成的代码。但是你仍然需要应对这种情况,当一些注释处理器生成代码,用你的注释进行注释时 - 与“提前”声明不同,用户会期望它只是开箱即用。


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

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

与此同时,我建议你从深思熟虑的注释处理器中学习,比如谷歌汽车、Dagger2或我的小研究项目


答案 2

推荐