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
BeanContext beanContext = ... // obtain the bean context
Collection<BeanDefinition> definitions =
beanContext.getBeanDefinitions(Qualifiers.byStereotype(Controller.class))
for (BeanDefinition definition : definitions) {
AnnotationValue<Controller> controllerAnn = definition.getAnnotation(Controller.class);
// do something with the annotation
}
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
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 thegetDeclaredAnnotation*
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:
Any annotations annotated with Inherited and present on a method of interface or super class
A
that is overridden by child interface or classB
will be inherited into the AnnotationMetadata retrievable via the ExecutableMethod API from a BeanDefinition or an AOP interceptor.Any annotations annotated with Inherited and present on a method parameter of interface or super class
A
that is overridden by child interface or classB
will be inherited into the AnnotationMetadata retrievable via the Argument interface from thegetArguments
method of the ExecutableMethod API.
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
import io.micronaut.context.annotation.AliasFor;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.AnnotationMetadata;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Inherited (1)
@Retention(RetentionPolicy.RUNTIME)
@Requires(property = "datasource.url") (2)
@Named (3)
@Singleton (4)
public @interface SqlRepository {
@AliasFor(annotation = Named.class, member = AnnotationMetadata.VALUE_MEMBER) (5)
String value() default "";
}
Defining Inherited Meta Annotations
import io.micronaut.context.annotation.AliasFor
import io.micronaut.context.annotation.Requires
import io.micronaut.core.annotation.AnnotationMetadata
import jakarta.inject.Named
import jakarta.inject.Singleton
import java.lang.annotation.Inherited
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
@Inherited (1)
@Retention(RetentionPolicy.RUNTIME)
@Requires(property = "datasource.url") (2)
@Named (3)
@Singleton (4)
@interface SqlRepository {
@AliasFor(annotation = Named.class, member = AnnotationMetadata.VALUE_MEMBER) (5)
String value() default "";
}
Defining Inherited Meta Annotations
import io.micronaut.context.annotation.Requires
import jakarta.inject.Named
import jakarta.inject.Singleton
import java.lang.annotation.Inherited
@Inherited (1)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Requires(property = "datasource.url") (2)
@Named (3)
@Singleton (4)
annotation class SqlRepository(
val value: String = ""
)
1 | The annotation is declared as @Inherited |
2 | Bean Conditions will be inherited by child classes |
3 | Bean Qualifiers will be inherited by child classes |
4 | Bean Scopes will be inherited by child classes |
5 | You 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
@SqlRepository
public abstract class BaseSqlRepository {
}
Using Inherited Meta Annotations on a Super Class
@SqlRepository
abstract class BaseSqlRepository {
}
Using Inherited Meta Annotations on a Super Class
@SqlRepository
abstract class BaseSqlRepository
And then a subclass will inherit all the annotations:
Inherting Annotations in a Child Class
import jakarta.inject.Named;
import javax.sql.DataSource;
@Named("bookRepository")
public class BookRepository extends BaseSqlRepository {
private final DataSource dataSource;
public BookRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
}
Inherting Annotations in a Child Class
import jakarta.inject.Named
import javax.sql.DataSource
@Named("bookRepository")
class BookRepository extends BaseSqlRepository {
private final DataSource dataSource
BookRepository(DataSource dataSource) {
this.dataSource = dataSource
}
}
Inherting Annotations in a Child Class
import jakarta.inject.Named
import javax.sql.DataSource
@Named("bookRepository")
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
public @interface Client {
/**
* @return The URL or service ID of the remote service
*/
@AliasFor(member = "id") (1)
String value() default "";
/**
* @return The ID of the client
*/
@AliasFor(member = "value") (2)
String id() default "";
}
1 | The value member also sets the id member |
2 | The 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 classAdd 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
public class EntityIntrospectedAnnotationMapper implements NamedAnnotationMapper {
@NonNull
@Override
public String getName() {
return "javax.persistence.Entity";
}
@Override
public List<AnnotationValue<?>> map(AnnotationValue<Annotation> annotation, VisitorContext visitorContext) { (1)
final AnnotationValueBuilder<Introspected> builder = AnnotationValue.builder(Introspected.class)
// don't bother with transients properties
.member("excludedAnnotations", "javax.persistence.Transient"); (2)
return Arrays.asList(
builder.build(),
AnnotationValue.builder(ReflectiveAccess.class).build()
);
}
}
1 | The map method receives a AnnotationValue with the values for the annotation. |
2 | One 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. |