1

I have a VariableElement field that is annotated with a generated Annotation (which is why I can't use field.getAnnotation(annotationClass)). I need to get all parameters passed to this annotation.

Note that by "a generated Annotation" I mean that literally the Annotation class itself (not the annotated one) has been generated by an Annotation Processor. The field/class that is being annotated is in the handwritten source code.

It didn't look like it'd be that hard, so far I've come up with this:

for (AnnotationMirror annotation : field.getAnnotationMirrors()) {
    Map<? extends ExecutableElement, ? extends AnnotationValue> annotationValueMap = annotation.getElementValues();

    messager.printMessage(Diagnostic.Kind.WARNING, annotation.toString() + ":" + annotationValueMap.toString());
}

I thought this would do it, but the output for the field is the following:

@MyAnnotation:{}

So, the processor does recognize that the field is annotated, but I'm unable to access the passed parameters. Even though the field is definetely annotated and does pass parameters with the annotation (it has to, since the annotation defines required parameters and no defaults):

@MyAnnotation(max = 387, min = 66876, ...)
private Integer myField;

Here's the generated annotation code:

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface MyAnnotation {
  int max();

  boolean allowAuto();

  int min();
}

I've clean-compiled the project multiple times, the processor never sees the values. What am I overlooking here? The processor can obviously see the annotation itself, yet the parameters passed to it are hidden.

Namnodorel
  • 385
  • 5
  • 17
  • To further confirm in the question - the annotation is generated but the annotated type isn't? So, prior to being compiled, the source existed with the non-existent annotation, and the annotation is then created _in the same pass_ that you read the annotation's properties? Or are they different passes, or different processors? – Colin Alworth Mar 16 '19 at 19:11
  • And, are you creating the annotation _before_ reading it as a mirror? Not just are these expected to happen in the same pass, but actually in that correct order? I'm certain that would never work in reverse order, and think it is pretty unlikely that it could be even done within the same pass (since the annotation doesnt even have "fields" yet, at least not of a static type, so the map's values can't have a type, even if they are used in sources). – Colin Alworth Mar 16 '19 at 19:12
  • @ColinAlworth Your first comment is correct - the annotation class is created in the same round/pass within the same processor. I am creating the annotation classes before reading it as a mirror, and that order is certain. It currently happens within the same round, since the class that uses the annotation isn't generated and thus not present in subsequent rounds. – Namnodorel Mar 16 '19 at 19:20
  • We're out of my depth to be 100% certain here, but my understanding is that within a round generated sources will not be compiled but instead you need to wait for the round to end, and until then the fields on those newly emitted types will not be available, even in cases like this. Waiting until the next round does not mean that the type won't be present in the round, just that it won't be offered directly, you can still read it from the Elements object, etc. See auto-common's `BasicAnnotationProcessor` as an example, deliberately avoiding processing incomplete classes until they are ready. – Colin Alworth Mar 16 '19 at 19:27
  • @ColinAlworth You should probably post that as an answer :) `annotation.toString()` just outputs the literal annotation, and doesn't care whether the type exists or where it is. But the Annotation type (and its methods) are apparently required to determine the passed parameters (though I don't understand why). Using `BasicAnnotationProcessor` to defer processing the annotations solved the issue. – Namnodorel Mar 17 '19 at 14:38

3 Answers3

2

Recall that annotation processors run as part of the compiler, in steps called "rounds". This process runs iteratively until there is no new code to compile, and then processors get one last chance to run (not necessary for this answer, but helpful for more context). Each round only the newly created types are directly given to the processor to examine.

What seems to be happening here is that during a round you are emitting a new annotation type, which should allow the processor to observe certain features about some code submitted to be compiled. However, any types created during a given round are not yet compiled until the next round begins.

For this question, we run into a conflict here - some Java sources are compiled which use an annotation that doesn't exist yet. The processor first creates the annotation, and then tries to read the newly-created annotation out of those partly-compiled sources. Unfortunately, until the annotation has been compiled, we can't actually read the annotation. Instead, we need to wait until the subsequent round (once the annotation itself has compiled), then go back to that class which has finished being compiled and examine it.

This can be implemented yourself without too much trouble, but the easiest way is often to rely on the google/auto project (specifically the auto-common library, see https://github.com/google/auto/tree/master/common), and extend their BasicAnnotationProcessor class. One of the nice features it supports is to automatically examine types and check if there are any compilation issues - if so, they are deferred until a later round so you can handle them without any type resolution issues.

Colin Alworth
  • 17,801
  • 2
  • 26
  • 39
1

Use getAnnotation(MyAnnotation.class) available from VariableElement

in your example code you can do this to get the min and max parameters

MyAnnotation myAnnotation= field.getAnnotation(MyAnnotation.class);
int max = myAnnotation.max();
int min = myAnnotation.min();

this will work unless the annotation members returns class/class[] value, in which you will get an exception if you try to get the value using this method.

more about how to get class literal values can be found on this answer

How to read a Class[] values from a nested annotation in an annotation processor

Or using annotation mirrors

for (AnnotationMirror annotation : field.getAnnotationMirrors()) {
    Map<? extends ExecutableElement, ? extends AnnotationValue> annotationValueMap = annotation.getElementValues();
    annotationValueMap.forEach((element, annotationValue) -> {
        messager.printMessage(Diagnostic.Kind.WARNING, element.getSimpleName().toString() + ":" + annotationValue.getValue());
    });
}

In case you have more than one annotation on the field then you can iterate over the annotation mirrors and use the check types.isSameType(annotationMirror.getAnnotationType(), elements.getTypeElement(MyAnnotation.class.getName()).asType()) to find the annotation you are interested in

Ahmad Bawaneh
  • 1,014
  • 8
  • 23
  • I can't use `field.getAnnotation(annotationClass)` because the annotation in question is generated. Thus I have no access to the Class object at processor-compile-time. Your second example doesn't output anything for me (because the map is empty for some reason). – Namnodorel Mar 15 '19 at 16:02
  • @Namnodorel this also works for me on a generated class, you might want to share the annotation implementation with us. – Ahmad Bawaneh Mar 15 '19 at 16:09
  • @Namnodorel did you try with `@Retention(RetentionPolicy.RUNTIME)` instead of `SOURCE`? – Ahmad Bawaneh Mar 15 '19 at 16:21
  • I now have, and there is no difference. Though it'd be really strange if there was, given that the entire point of SOURCE is to be used by annotation processors. – Namnodorel Mar 15 '19 at 16:28
  • 1
    Interestingly, retention=SOURCE means as you'd expect, the annotation is only present in .java and not .class file. This still affects annotation processors in two ways: first, if your compiler (eclipse, gradle, etc) is incremental then it might not re-parse all sources, but use bytecode instead, and second if you end up referencing an annotation in a separate module, then that is already compiled so the annotation will be of course missing. SOURCE only makes sense when you are 100% that the processor should only see it when that exact file gets changed, which is true less than you'd think. – Colin Alworth Mar 15 '19 at 16:40
  • @ColinAlworth Interesting... Though, if that were the problem, the annotation would not be recognized at all, instead of just having no elements, or would it? – Namnodorel Mar 16 '19 at 08:42
  • Good point. I think we need to see more of your code if this answer doesnt solve your problem, because I've also successfully used both of the strategies in this answer. – Colin Alworth Mar 16 '19 at 13:09
  • @ColinAlworth When you were using them, did you generate the Annotation? I think that that's got something to do with the problem - even though AnnotationMirror is supposed to work on source code, not bytecode. – Namnodorel Mar 16 '19 at 18:44
  • What do you mean by generating the annotation? Is another processor creating the annotation on the fly, then it is being read as you compile it? Or just generating the usage of the annotation (I've definitely done that). Do note that until the code can successfully be compiled though, there will be pieces missing - the "ErrorType" and others are ways to detect that. I think you should add more detail to your question so we can better help. – Colin Alworth Mar 16 '19 at 18:52
  • @ColinAlworth The Annotation class itself is being generated (and written), within the same processor but before any annotated field is accessed. I included that in the question, but apparently in a too ambiguous way. I'll attempt to rephrase. – Namnodorel Mar 16 '19 at 19:04
  • Thanks, I'm sorry I didn't get it before - I think that is probably the most important part of the question, and that you might want to go so far as to give a brief example of how this is done, as it seems pretty far into the weeds otherwise. I suspect that with even the annotation itself not yet compiled, there is no way to read its properties, so you either will need to split this into multiple passes of the same processor, or multiple processors... – Colin Alworth Mar 16 '19 at 19:08
0

Yes, you will not be able to instantiate a Class object for a type which is not available in your annotation processor's classloader, and may not even have been compiled into a class file yet at all. A similar problem exists for retrieving enum constants.

There are a few wrinkles to dealing with this sort of thing:

  1. Any annotation value that is declared as an array might come to you at compile time either as a single value, or a list of values - so any code needs a path to handle both the list and non-list case - like this
  2. What you get may be a generic type, and if you are generating Java code or similar that wants to insert a reference to Foo.class you need to get the erasure of that type, so you don't generate Foo<Bar>.class into your generated sources.
  3. One of the places your annotation processor is going to get run is in an IDE, on broken code still being typed, so it is important to fail gracefully in the case that code elements that, you would think, can't possibly be missing or broken or unlikely values, are. In an IDE, your annotation processor may also be kept alive for a long time and reused, so it's important not to pile up objects modeling stuff that has already been generated and emitted.

FWIW, I wrote a library to solve this and related problems, which can be found on Maven central at the coordinates com.mastfrog:annotations-tools:2.8.3.4 (check for newer versions). The usage pattern is simple:

  • Instantiate an instance of AnnotationUtils in an override of the init() method of your annotation processor and store it in a field
  • Use it to, for example, resolve a Class<?>[] into a list of string class names that you can work with inside javac, and similar

It makes it pretty straightforward to write annotation processors that do not directly depending on the classes they processes at all - which means the annotation processors (and their dependency graphs!) be completely independent of what they process, and can depend on whatever libraries they like without forcing those dependencies into the dependency graph of any project that uses them - the most common pattern is someone writes some annotations and then puts the annotation processor in the same project, or even package, and so anything the annotation processor uses becomes a dependency of every consumer of the annotations, even though those dependencies will probably never be used at runtime at all. That, it seems to me, is an antipattern worth avoiding.

Tim Boudreau
  • 1,741
  • 11
  • 13