3.16 Bean Annotation Metadata

The methods provided by Java’s AnnotatedElement API in general don’t provide the ability to introspect annotations without loading the annotations themselves, nor do they provide any ability to introspect annotation stereotypes (Often called meta-annotations, an annotation stereotype is where an annotation is annotated with another annotation, essentially inheriting its behaviour).

To solve this problem many frameworks produce runtime metadata or perform expensive reflection to analyze the annotations of a class.

Micronaut instead produces this annotation metadata at compile time, avoiding expensive reflection and saving on memory.

The BeanContext API can be used to obtain a reference to a BeanDefinition which implements the AnnotationMetadata interface.

For example the following code will obtain all bean definitions annotated with a particular stereotype:

Lookup Bean Definitions by Stereotype

  1. BeanContext beanContext = ... // obtain the bean context
  2. Collection<BeanDefinition> definitions =
  3. beanContext.getBeanDefinitions(Qualifiers.byStereotype(Controller.class))
  4. for(BeanDefinition definition : definitions) {
  5. AnnotationValue<Controller> controllerAnn = definition.getAnnotation(Controller.class);
  6. // do something with the annotation
  7. }

The above example will find all BeanDefinition ‘s annotated with @Controller regardless whether @Controller is used directly or inherited via an annotation stereotype.

Note that the getAnnotation method and the variations of the method return an AnnotationValue type and not a Java annotation. This is by design, and you should generally try to work with this API when reading annotation values, the reason being that synthesizing a proxy implementation is worse from a performance and memory consumption perspective.

If you absolutely require a reference to an annotation instance you can use the synthesize method, which will create a runtime proxy that implements the annotation interface:

Synthesizing Annotation Instances

  1. Controller controllerAnn = definition.synthesize(Controller.class);

This approach is not recommended however, as it requires reflection and increases memory consumption due to the use of runtime created proxies and should be used as a last resort (for example if you need an instance of the annotation to integrate with a third party library).

Aliasing / Mapping Annotations

There are times when you may want to alias the value of an annotation member to the value of another annotation member. To do this you can use the @AliasFor annotation to alias the value of one member to the value of another.

A common use case is for example when an annotation defines the value() member, but also supports other members. For example the @Client annotation:

The @Client Annotation

  1. public @interface Client {
  2. /**
  3. * @return The URL or service ID of the remote service
  4. */
  5. @AliasFor(member = "id") (1)
  6. String value() default "";
  7. /**
  8. * @return The ID of the client
  9. */
  10. @AliasFor(member = "value") (2)
  11. String id() default "";
  12. }
1The value member also sets the id member
2The id member also sets the value member

With these aliases in place, regardless whether you define @Client("foo") or @Client(id="foo") both the value and id members are always set, making it much easier to parse and deal with the annotation.

If you do not have control over the annotation then another approach is to use an AnnotationMapper. To create an AnnotationMapper you must perform the following steps:

  • Implement the AnnotationMapper interface

  • Define a META-INF/services/io.micronaut.inject.annotation.AnnotationMapper file referencing the implementation class

  • Add the JAR file containing the implementation to the annotationProcessor classpath (kapt for Kotlin)

Because AnnotationMapper implementations need to be on the annotation processor classpath they should generally be in a project that includes few external dependencies to avoid polluting the annotation processor classpath.

The following is an example the AnnotationMapper that improves the introspection capabilities of JPA entities

EntityIntrospectedAnnotationMapper Mapper Example

  1. public class EntityIntrospectedAnnotationMapper implements NamedAnnotationMapper {
  2. @NonNull
  3. @Override
  4. public String getName() {
  5. return "javax.persistence.Entity";
  6. }
  7. @Override
  8. public List<AnnotationValue<?>> map(AnnotationValue<Annotation> annotation, VisitorContext visitorContext) { (1)
  9. final AnnotationValueBuilder<Introspected> builder = AnnotationValue.builder(Introspected.class)
  10. // don't bother with transients properties
  11. .member("excludedAnnotations", "javax.persistence.Transient"); (2)
  12. return Collections.singletonList(
  13. builder.build()
  14. );
  15. }
  16. }
1The map method receives a AnnotationValue with the values for the annotation.
2One or more annotations can be returned, in this case @Transient.
The example above implements the NamedAnnotationMapper interface which allows for annotations to be mixed with runtime code. If you want to operate against a concrete annotation type then you should use TypedAnnotationMapper instead, though note it requires the annotation class itself to be on the annotation processor classpath.