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
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 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
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
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, 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 classAdd 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
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 Collections.singletonList(
builder.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. 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. |