3.17 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 memory.

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

For example the following code obtains 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 finds all BeanDefinition instances annotated with @Controller 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, since synthesizing a proxy implementation is worse from a performance and memory consumption perspective.

If you require a reference to an annotation instance you can use the synthesize method, which creates 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 generated 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.

Annotation Inheritance

Micronaut will respect the rules defined in Java’s AnnotatedElement API with regards to annotation inheritance:

  • Annotations meta-annotated with Inherited will be available via the getAnnotation* methods of the AnnotationMetadata API whilst those directly declared are available via the getDeclaredAnnotation* methods.

  • Annotations not meta-annotated with Inherited will not be included in the metadata

Micronaut differs from the AnnotatedElement API in that it extends these rules to methods and method parameters such that:

In general behaviour which you may wish to override is not inherited by default including Bean Scopes, Bean Qualifiers, Bean Conditions, Validation Rules and so on.

If you wish a particular scope, qualifier, or set of requirements to be inherited when subclassing then you can define a meta-annotation that is annotated with @Inherited. For example:

Defining Inherited Meta Annotations

  1. import io.micronaut.context.annotation.AliasFor;
  2. import io.micronaut.context.annotation.Requires;
  3. import io.micronaut.core.annotation.AnnotationMetadata;
  4. import jakarta.inject.Named;
  5. import jakarta.inject.Singleton;
  6. import java.lang.annotation.Inherited;
  7. import java.lang.annotation.Retention;
  8. import java.lang.annotation.RetentionPolicy;
  9. @Inherited (1)
  10. @Retention(RetentionPolicy.RUNTIME)
  11. @Requires(property = "datasource.url") (2)
  12. @Named (3)
  13. @Singleton (4)
  14. public @interface SqlRepository {
  15. @AliasFor(annotation = Named.class, member = AnnotationMetadata.VALUE_MEMBER) (5)
  16. String value() default "";
  17. }

Defining Inherited Meta Annotations

  1. import io.micronaut.context.annotation.AliasFor
  2. import io.micronaut.context.annotation.Requires
  3. import io.micronaut.core.annotation.AnnotationMetadata
  4. import jakarta.inject.Named
  5. import jakarta.inject.Singleton
  6. import java.lang.annotation.Inherited
  7. import java.lang.annotation.Retention
  8. import java.lang.annotation.RetentionPolicy
  9. @Inherited (1)
  10. @Retention(RetentionPolicy.RUNTIME)
  11. @Requires(property = "datasource.url") (2)
  12. @Named (3)
  13. @Singleton (4)
  14. @interface SqlRepository {
  15. @AliasFor(annotation = Named.class, member = AnnotationMetadata.VALUE_MEMBER) (5)
  16. String value() default "";
  17. }

Defining Inherited Meta Annotations

  1. import io.micronaut.context.annotation.Requires
  2. import jakarta.inject.Named
  3. import jakarta.inject.Singleton
  4. import java.lang.annotation.Inherited
  5. @Inherited (1)
  6. @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
  7. @Requires(property = "datasource.url") (2)
  8. @Named (3)
  9. @Singleton (4)
  10. annotation class SqlRepository(
  11. val value: String = ""
  12. )
1The annotation is declared as @Inherited
2Bean Conditions will be inherited by child classes
3Bean Qualifiers will be inherited by child classes
4Bean Scopes will be inherited by child classes
5You can also alias annotations and they will be inherited

With this meta-annotation in place you can add the annotation to a super class:

Using Inherited Meta Annotations on a Super Class

  1. @SqlRepository
  2. public abstract class BaseSqlRepository {
  3. }

Using Inherited Meta Annotations on a Super Class

  1. @SqlRepository
  2. abstract class BaseSqlRepository {
  3. }

Using Inherited Meta Annotations on a Super Class

  1. @SqlRepository
  2. abstract class BaseSqlRepository

And then a subclass will inherit all the annotations:

Inherting Annotations in a Child Class

  1. import jakarta.inject.Named;
  2. import javax.sql.DataSource;
  3. @Named("bookRepository")
  4. public class BookRepository extends BaseSqlRepository {
  5. private final DataSource dataSource;
  6. public BookRepository(DataSource dataSource) {
  7. this.dataSource = dataSource;
  8. }
  9. }

Inherting Annotations in a Child Class

  1. import jakarta.inject.Named
  2. import javax.sql.DataSource
  3. @Named("bookRepository")
  4. class BookRepository extends BaseSqlRepository {
  5. private final DataSource dataSource
  6. BookRepository(DataSource dataSource) {
  7. this.dataSource = dataSource
  8. }
  9. }

Inherting Annotations in a Child Class

  1. import jakarta.inject.Named
  2. import javax.sql.DataSource
  3. @Named("bookRepository")
  4. class BookRepository(private val dataSource: DataSource) : BaseSqlRepository()
A child class must at least have one bean definition annotation such as a scope or qualifier.

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, use the @AliasFor annotation.

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, whether you define @Client("foo") or @Client(id="foo"), both the value and id members will be set, making it easier to parse and work with the annotation.

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

  • 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 must 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 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 Arrays.asList(
  13. builder.build(),
  14. AnnotationValue.builder(ReflectiveAccess.class).build()
  15. );
  16. }
  17. }
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. To operate against a concrete annotation type, use TypedAnnotationMapper instead, although note it requires the annotation class itself to be on the annotation processor classpath.