Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Annotation processing, RoundEnvironment.processingOver()

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.

like image 273
Mohammed Aouf Zouag Avatar asked Dec 12 '17 18:12

Mohammed Aouf Zouag


1 Answers

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.

like image 196
user1643723 Avatar answered Oct 28 '22 11:10

user1643723