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()在处理某个回合时有用的用例。

4

1 回答 1

25

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

当 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尽快检查,如果它立即退出返回真。这将简化您的代码并加速错误的编译。

对于processingOver

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

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

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

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

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

“尽管存在错误仍继续生成代码”部分专门用于减少编译失败期间报告的编译错误的数量。更少的缺失类 = 更少的缺失声明错误(希望如此)。或者,不要创建会煽动用户参考他们生成的代码的注释处理器。但是你仍然需要处理这样的情况,当一些注释处理器生成代码,用你的注释进行注释时——与“提前”声明不同,用户会期望它可以开箱即用。


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

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

同时,我建议您向深思熟虑的注释处理器学习,例如 Google Auto、Dagger2 或我的小型研究项目

于 2017-12-12T22:26:11.607 回答