5

I'm writing an annotation processor which needs to collect all the classes with a certain annotation in the current module and write a class referencing each of them.

To simplify a bit, given these source files:

src/main/java/org/example/A.java
@Annotation
class A {}
src/main/java/org/example/B.java
@Annotation
class B {}

I want to generate a class:

target/generated-sources/org/example/Module.java
class Module {
  String getModuleClasses() {
    return Arrays.asList(
      "org.example.A",
      "org.example.B"
    );
  }
}

This works from Maven, but when I modify class A, IntelliJ gives my annotation processor a RoundEnvironment with A as the single root element.

I understand Gradle supports incremental compilation with aggregating annotation processors by passing a fake RoundEnvironment with all the sources matching the annotations to the annotation processor, but IntelliJ doesn't seem to have anything similar. (Except maybe for Gradle projects?)

What would be the best way to find both classes when IntelliJ compiles only class A?

Maybe the annotator could keep a list of annotated classes: read the list from a resource file in the first round, in each round remove from the list root elements and add to the list elements which are annotated, and write the list back to the resource file in the final round?

Dan Berindei
  • 7,054
  • 3
  • 41
  • 48
  • It's not IntelliJ IDEA specific question. If you want your annotation processor to work anywhere (javac, Maven, Gradle, IntelliJ IDEA, Eclipse, etc), you have to use the standard annotation processor APIs for javac. IntelliJ IDEA allows you to code instrumentation builder for JPS, it has more features, but will work only in the IDE. If you are looking for the portable solution, edit your question and remove `intellij-idea` tag. – CrazyCoder May 14 '19 at 21:19
  • @CrazyCoder I am using "the standard annotation processor APIs". But incremental compilation is an IntelliJ non-standard feature, so the `javax.annotation.processing` javadoc doesn't say anything about it. I was hoping to hear that IntelliJ makes you jump through some hoops to get access to all the classes while still using the standard APIs, like gradle does. – Dan Berindei May 14 '19 at 21:50
  • I'm not sure how incremental compilation is is related to the question. All that IntelliJ IDEA JPS does is determines the minimal required set of classes needed to be recompiled. javac is invoked via the API to perform the compilation, javac calls the annotation processors on all the compiled classes and you can do whatever you want with the annotation processor API. Classes that are not recompiled at this specific invocation are still on the disk and are available to the javac via classpath. – CrazyCoder May 15 '19 at 08:11
  • Regarding the Gradle support for incremental compilation, you should not rely on that for the portable solution. What if the project using your annotation processor is compiled from the command line javac or from Ant? Even if IntelliJ IDEA had something similar, you would not be able to use that outside of the IDE which would make your AP use very limited. – CrazyCoder May 15 '19 at 08:14
  • > "What would be the best way to find both classes when IntelliJ compiles only class A?" — You should support the scenario with the partial compilation. You can search for the missing files the same way, javac resolves them, either via the classpath or in the sourcepath. – CrazyCoder May 15 '19 at 08:16
  • Your AP can also support incremental compilation internally and update the code incrementally by keeping the intermediate state and using it in case of the partial project compilation to get the missing information from this state. – CrazyCoder May 15 '19 at 08:20
  • > "All that IntelliJ IDEA JPS does is determines the minimal required set of classes needed to be recompiled." - is that standard, or is it specific to IntelliJ's incremental compilation? – Dan Berindei May 15 '19 at 08:30
  • 1
    > "Classes that are not recompiled at this specific invocation are still on the disk and are available to the javac via classpath" - I don't need to find a specific class, I thought it was clear from the question that I need to find all the classes with a certain annotation @CrazyCoder – Dan Berindei May 15 '19 at 08:35
  • > "is that standard, or is it specific to IntelliJ's incremental compilation?" — It's IntelliJ IDEA specific implementation. > "I don't need to find a specific class, I thought it was clear from the question that I need to find all the classes with a certain annotation" — I see, in this case the solution with keeping the state between builds like mentioned in my comment and also suggested by @Ahmad is your best option. – CrazyCoder May 15 '19 at 08:38
  • 1
    > 'keeping the state between builds' This is not right solution, because it it doesn't provide a way to clean up the state when classes are removed. IntelliJ is wrong to only include 'minimal required set of classes'. When an annotation processor is present, it is an invalid optimization. – Carl Mastrangelo Apr 18 '20 at 04:58

1 Answers1

2

One way to solve this issue is use some sort of a registry in between builds, you can for example store the types annotated in a service like style in meta-inf

So in your processor you defer the code generation until the processing last round, and after generating your code you store the types in a file into a file under the META-INF

FileObject resource = processingEnv.getFiler()
                    .createResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/annotatedtypes/"+fileName);
PrintWriter out = new PrintWriter(new OutputStreamWriter(resource.openOutputStream()));
            classes.forEach(out::println);

You need to check for duplicate entries of course.

At some point before generating the code, read the types and generate your code based on that

FileObject resource = processingEnv.getFiler()
                        .getResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/annotatedtypes/"+fileName);
new BufferedReader(new InputStreamReader(resource.openInputStream())).lines().forEach(classes::add);

the file content could look like something like this

org.foo.bar.A
org.foo.bar.B

The problem with this is that when you defer the code generation to the last round your generated code will not be picked by any other processor for example dagger2, and also sometime the file might end with records for classes that does not exist anymore.

In summary inside your processor you do the following

  • Read the registered types from the file at META-INF
  • Get elements annotated with your annotation.
  • If it is the last round you just update the file with unique records set and generate code.

You read the file every round, but writes only once at the last round.

Dan Berindei
  • 7,054
  • 3
  • 41
  • 48
Ahmad Bawaneh
  • 1,014
  • 8
  • 23
  • I ended up doing something similar, but I didn't approve the comment because it won't work as is: `Filer` doesn't allow you to write and then read the same resource, or write to the same resource twice: https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Filer.html#createResource-javax.tools.JavaFileManager.Location-java.lang.CharSequence-java.lang.CharSequence-javax.lang.model.element.Element...- – Dan Berindei May 15 '19 at 08:28
  • Yes right, and you need to work that out, for example you only update the file when processing is over, so for next compilation it will have the records, where your only read for all rounds except for the last one. – Ahmad Bawaneh May 15 '19 at 08:36
  • Commenting to confirm that this approach does not solve the problem because the temp file can't be reopened. – chrylis -cautiouslyoptimistic- Sep 02 '22 at 16:57
  • @chrylis-cautiouslyoptimistic- I have it working for sometime now here is an example I wrote some long time ago that is working with this approach https://github.com/DominoKit/domino-mvp/blob/master/domino-mvp-processors/domino-mvp-apt-client/src/main/java/org/dominokit/domino/apt/client/processors/module/client/ClientModuleAnnotationProcessor.java you might need to trace the code a little to know how things happens though – Ahmad Bawaneh Sep 02 '22 at 20:22