本文介绍了注释处理,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()有用的用例.

解决方案

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

当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中.这样可以解决对其他处理器的依赖性,但可以防止 other 处理器查找小心"处理器生成的代码.这是一个讨厌的策略,但是如果生成的代码不打算在其他地方引用,那么我想它还可以.

还有诸如上述第三方Spring配置验证器之类的注释处理器:它们误解了某些东西,并以猴子和扳手的方式使用API​​.

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

尽管存在错误仍保留生成代码"部分专门用于减少编译失败时报告的编译错误数.减少类丢失=减少声明错误(希望).或者,不要创建注释处理器,它会诱使用户引用由它们生成的代码.但是,您仍然必须应对这种情况,当某些注释处理器生成的代码带有注释时,与提前"声明不同,用户将期望它们即开即用.


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

可悲的是,有问题的处理器看起来已经废弃了(自2015年以来没有提交),但是作者活跃在Github上,所以也许您可以向他们报告问题.

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

While reading the code of a custom annotation processor in Java, I noticed this piece of code in the processor's process method:

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

It happened that I'm working on a custom Annotation processor too, & I wanted to use the snippet above in my annotation processor.

I tried the code above this way:

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

& this way:

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

but I couldn't notice any change in the processor's behavior.I get the !roundEnv.errorRaised() check, but I can't see how is !roundEnv.processingOver() any useful.

I would like to know the use cases where it is useful to use roundEnv.processingOver() when processing a certain round.

解决方案

Both of these checks are important, but you won't notice their effects until you run multiple annotation processors at once in the same project. Let me explain.

When Javac fails the compilation for any reason (for example because of missing type declaration or parsing errors), it does not immediately terminate. Instead it will gather as much information about the error as possible and attempt to display that information to user in meaningful way. In addition, if there are annotation processors, and the error was caused by missing type or method declaration, Javac will try to run those processors and retry the compilation in hopes, that they generate the missing code. This is called "multi-round compilation".

The compilation sequences will look like this:

  1. Primary round (possibly with code generation);
  2. Several optional code generation rounds; new rounds will happen until nothing is generated by annotation processors;
  3. The final round; code generated during this round will not by subjected to annotation processing.

Each round is a full-blown attempt to compile the code. Each round except the last one will re-run every annotation processors on the code, previously generated by annotation processors.

This wonderful sequence allows using approach, popularized by libraries like Dagger2 and Android-Annotated-SQL: reference a not yet existing class in your sources code, and let annotation processor generate it during a compilation:

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

Some people consider that technique iffy, because it relies on using nonexisting classes in source code, and closely ties source code to annotation processing (and does not work very well with IDE code completion). But the practice itself is legitimate and works as intended by Javac developers.


So, how does all of this relate to Spring's annotation processor in your question?

TL;DR: the code in your question is buggy.

The correct way to use those methods is like this:

for errorRaised:

  1. If your processor generates new publicly visibly classes (which may be used in user code "ahead of time" like described above), you have to be super-resilient: keep generating, ignore missing bits and inconsistencies when possible, and ignore errorRaised. This ensures, that you leave as little missing stuff as possible by the time the Javac goes on it's error reporting spree.
  2. If your code does not generate new publicly visibly classes (for example, because it only creates package-private classes, and other code will reflectively look them up at runtime, see ButterKnife), then you should check for errorRaised ASAP, and exit immediately if it returns true. This will simplify your code and speed-up erroneous compilations.

for processingOver:

  1. If the current round is not last (processingOver returns false), try to generate as much of your output as possible; ignore missing types and methods in user code (assume, that some other annotation processor might generate them in following rounds). But still try to generate as much as possible, in case it may be needed for other annotation processors. For example if you trigger code generation on each class, annotated with @Entity, you should iterate over those classes and attempt code generation for each, even if previous classes have errors or missing methods. Personally, I just wrap every separate unit of generation in try-catch, and check for processingOver: if it is false, ignore errors and keep iterating over annotations and generating code. This allows Javac to breake circular dependencies between code, generated by different annotation processors by running them until full satisfaction.
  2. If the current round is not last (processingOver returns false), and some of previous round's annotations weren't processed (I remember them whenever a processing fails due to exception), retry processing on those.
  3. If the current round is last (processingOver returns true), look if there are annotations still unprocessed. If so, fail a compilation (only during last round!)

The sequence above is the intended way to use processingOver.

Some annotation processors use processingOver a bit differently: they buffer the code generated during each round and actually write it to Filer during the last round. This allows dependencies on other processors to be resolved, but prevents other processors from finding the code generated by "careful" processors. This is a bit nasty tactic, but if the generated code is not meant to be referenced elsewhere, I guess it is alright.

And there are annotation processors like the above-mentioned third-party Spring configuration validator: they misunderstand some things and use the API in monkey-and-wrench style.

To get a better gist of whole thing, install Dagger2, and try to reference the Dagger-generated classes in classes, used by another annotation processor (preferably in a way, that would make that processor resolve them). This will quickly show you, how well those processors cope with multi-round compilation. Most would just crash Javac with exception. Some would spit out thousands of errors, filling IDE error reporting buffers and obfuscating compilation results. Very few will properly participate in multi-round compilation but still spit lots of errors if it fails.

The "keep generating code despite existing errors" part is specifically meant to reduce number of reported compilation errors during failed compilation. Less missing classes = less missing declaration errors (hopefully). Alternatively, do not create annotation processors, that incite user to reference code generated by them. But you still have to cope with situation, when some annotation processor generated code, annotated with your annotations — unlike the "ahead of time" declarations, users will expect that to just work out of box.


Going back to original matter: since the Spring configuration validation processor is not expected to generate any code (hopefully, I didn't look deeply into it), but should always report all errors in scanned configuration, it should ideally work like this: ignore errorRaised and postpone configuration scanning until the processingOver returns true: this will avoid reporting same error multiple times during multiple compilation rounds, and allow annotation processors to generate new configuration pieces.

Sadly, the processor in question looks abandoned (no commits since 2015), but the author is active on Github, so maybe you may be able to report the issue to them.

In meantime, I suggest that you learn from well thought-out annotation processors, such as Google Auto, Dagger2 or my tiny research project.

这篇关于注释处理,RoundEnvironment.processingOver()的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

07-23 01:59
查看更多