-
Notifications
You must be signed in to change notification settings - Fork 38.2k
MergedAnnotation API internals
This page attempts to document some of the design decisions taken for the internals of the MergedAnnotations
API.
See also: Spring Annotation Programming Model
The MergedAnnotations
interface is designed to work with both Java reflection types and ASM bytecode reading.
In the future, it's also possible that additional sources may be supported.
The boundary of ASM support extends only as far as the annotation usage.
The actual Annotation
class must be available at runtime and is always inspected using reflection.
In other words, the annotation attributes can be read using ASM, but the actual annotation class is not.
As much as possible the user-facing API for MergedAnnotations
is limited to just interfaces.
The implementations are intentionally kept package-private and are not available to the user.
Access is always via static methods on the interfaces.
Calculating the way that annotation attributes are merged can be expensive, so as much as possible cache friendly structures are used.
The two main caches used are in AttributeMethods
and AnnotationTypeMappings
.
The AttributeMethods
class provides a consistent view of the attribute methods of an Annotation
class.
It primarily provides caching for the sorted Method
instances.
The AnnotationTypeMappings
class is responsible for crawling the meta-annotations on an Annotation
and pre-computing as much information as possible.
From AnnotationTypeMappings
you can get an AnnotationTypeMapping
and very quickly map attributes back to the root annotation.
It's important to understand that AnnotationTypeMapping
represents a view of an annotation within the context of a root annotation.
For example, an instance of AnnotationTypeMappings
for @GetMapping
provides
access to an AnnotationTypeMapping
for @RequestMapping
(i.e., the root type).
However, this is different than the AnnotationTypeMapping
for @RequestMapping
accessed via @PostMapping
or @PutMapping
. You need to consider the entire chain
of annotations back to the root type.
The internal AnnotationTypeMapping.aliasMappings
array tracks how attributes are mapped via @AliasFor
to the root annotation.
Given an AnnotationTypeMapping
it's possible to very quickly tell which of the root annotation attributes actually provides its value.
Remember that an AnnotationTypeMapping
only knows information about the Annotation
type.
It doesn't directly know what attribute values will be used when it is actually declared.
In other words, AnnotationTypeMapping
tracks static metadata about an annotation type.
For example, suppose we have the following:
@interface Bar {
String name() default "";
}
@Bar
@interface Foo {
@AliasFor(annotation=Bar.class, attribute="name")
String barName() default "";
}
The AnnotationTypeMapping
for Bar
(in the context of Foo
) knows that the "name" attribute is aliased to "barName" on the root annotation. When we actually declare an annotation, for example @Foo(barName="Spring")
, we can very quickly tell that calling name()
on the @Bar
meta-annotation (index 0
) just goes to barName
on the root annotation (index 0
).
This mapping logic also supports multi-level meta-annotation hierarchies. For example, a @ComposedFoo
annotation would also maintain mappings.
For backward compatibility, convention-based mappings are also tracked.
These are implicit mappings used when the @AliasFor
annotation is not present.
They work for all attributes, except value
.
For example, given the following, there is an implicit, convention based mapping from Foo.name
to Bar.name
:
@interface Bar {
String name() default "";
}
@Bar
@Interface Foo {
String name() default "";
}
The internal AnnotationTypeMapping.conventionMappings
array works in exactly the same way as AnnotationTypeMapping.aliasMappings
but for convention based mappings.
Some meta-annotation values are actually resolvable from the type information.
For example, the @RequestMapping.method()
attribute on a @PostMapping
will always return RequestMethod.POST
.
For these elements the AnnotationTypeMapping.annotationValueMappings
and AnnotationTypeMapping.annotationValueSource
arrays are used.
In the example above, the AnnotationTypeMapping
for @RequestMapping
would point to the @PostMapping
source and the method
attribute. The value would be directly read from the declared meta-annotation:
@RequestMapping(method = RequestMethod.POST)
@interface PostMapping {
// ...
}
The @AliasFor
annotation can be used to declare that multiple attributes represent the same underlying value.
In its simplest form, it looks like this:
@interface Foo {
@AliasFor("value")
String name() default "";
@AliasFor("name")
String value() default "";
}
The AnnotationTypeMapping
class refers to these as "mirror" attributes.
Mirror attributes can also be declared when two or more attributes declare an @AliasFor
on the same meta-annotation attribute.
Although it's possible to detect mirror attributes from the type mapping, we can't actually tell which one to use until we have attribute values. We also can't fully verify that the user has not made an error.
For example, @Foo("spring")
and @Foo(name = "spring")
are valid, but @Foo(value = "framework", name = "spring")
is not.
The getMirrorSets().resolve(...)
method is used to work out which actual mirror attribute has been used on the declared annotation.
It returns an array that maps to a real attribute, or it throws an exception if the user has misconfigured something.
Give the examples above, calling resolve
for @Foo("spring")
would return [1,1]
. For @Foo(name = "spring")
it would return [0,0]
.
The index into the array is the attribute being requested; the value is the attribute to use.
For @Foo("spring")
calling Foo.name()
is a lookup at index 0
, returning 1
and Foo.value()
is a lookup at index 1
, returning 1
.
In other words, the value
attribute is always the source of truth in this case.
As with the other elements, the design goal is to provide as much pre-computation as possible. The actual resolution process once we have declared attribute values should be quick.
The final piece of the puzzle is to provide actual implementations of the MergedAnnotations
and MergedAnnotation
interfaces for the user to work with. The main classes here are TypeMappedAnnotations
and TypeMappedAnnotation
.
The TypeMappedAnnotations
class is responsible for exposing declared annotations from an AnnotatedElement
in a uniform way.
The class has a scan
method which is used when operating on a single annotation, and stream
methods for working with all annotations.
Both annotations and meta-annotations need to be exposed. Declared annotations are discovered using the AnnotationScanner
, then AnnotationTypeMappings
are used to add meta-annotations.
There are some fairly complicated ordering rules that are contained in the inner AggregatesSpliterator
class.
The AnnotationsScanner
class is used to find declared annotations from an AnnotatedElement
.
It supports various search strategies and deals with the complexities of searching for Inherited methods.
There are a few unusual aspects to the way the scanner has been implemented that can catch you out.
One specifically worth mentioning is that the array passed to the AnnotationsProcessor
callback can contain null
elements.
This was done as a performance optimization so that we don't need to copy the array simply to remove null
elements.
Another interesting aspect of the Scanner
is that an AnnotationsProcessor
can trigger an early exit.
This is another performance tweak that prevents the need to scan a full class hierarchy if a result has already been found.
Since the AnnotationsScanner
is a package-private class, neither of these quirks are exposed to the end user.
The TypeMappedAnnotation
class provides a MergedAnnotation
implementation by combining an AnnotationTypeMapping
with an actual annotation source.
The source can be an actual declared Java annotation or a value read using ASM.
The valueExtractor
function provides the level of indirection needed to support both.
For a real annotation, ReflectionUtils::invokeMethod
can be used.
Most of the TypeMappedAnnotation
implementation is fairly straightforward, and there's nothing too unusual with the class.
One aspect that is worth noting is the set of adapt...
methods.
These are used when an extracted value needs to be adapted to a different type.
This might be because a String
value read from ASM now needs to be accessed as a class.
Or it might be because a nested Annotation
needs to be read (for example @ComponentScan.includeFilters
)
It's quite common for MergedAnnotations
to be called on a class that has no annotations.
For these situations, we return a single shared instance of TypeMappedAnnotations
.
This saves us from needing to create a new empty instance each time and helps to reduce garbage collection.
The MergedAnnotations
API has been designed not to return null
whenever possible.
Instead MergedAnnotation.missing()
is returned when an annotation is not present.
The package-private MissingMergedAnnotation
class provides the implementation for this "null object" pattern.