Repository navigation
AnnotationDescriptor.findAllLocalMergedAnnotations fails to find interface annotations #36975
Description
Activity
- addedstatus: waiting-for-triageAn issue we've not yet triaged or decided onAn issue we've not yet triaged or decided on
on Jun 28, 2026 - addedtype: bugA general bugA general bugand removedstatus: waiting-for-triageAn issue we've not yet triaged or decided onAn issue we've not yet triaged or decided on
on Jun 28, 2026 Hi @nosan,
Good catch, and thanks for reporting this! 👍
My understanding is that the method should return annotations declared directly on the class together with those from its implemented interfaces, while excluding annotations from superclasses.
Indeed, that was the original intent.
However,
AnnotationDescriptor.findAllLocalMergedAnnotationsappears to ignore annotations declared on implemented interfaces.That's partially true. Annotations are found on interfaces if there are none found on the root declaring class. However, you're right: interface annotations are currently silently excluded whenever the root declaring class itself declares the annotation.
Since this is a long standing bug whose fix technically introduces a regression in behavior, I will only apply the fix to 7.1.
Regards,
Sam
- added a commit that references this issue
on Jun 28, 2026 Hi @sbrannen,
Thank you very much for the quick fix!
I have a question regarding this change, since it appears to alter the existing behavior. For example:
static class First implements Second { } interface Second extends Third { } @TypeExcludeFilters(ThirdSliceExclude.class) interface Third { }
Before this fix, the following test passed:
AnnotationDescriptor<TypeExcludeFilters> descriptor = TestContextAnnotationUtils.findAnnotationDescriptor(First.class, TypeExcludeFilters.class); assertThat(descriptor).isNotNull(); assertThat(descriptor.findAllLocalMergedAnnotations()).hasSize(1);
With the new changes, this is no longer the case.
Based on my understanding of
AnnotationDescriptor.findAnnotationDescriptor()and the implementation ofAnnotationDescriptor.next(), I thought the original intention was to locate the first class in the hierarchy that declares the annotation. Once that "root declaring class" is found,findAllLocalMergedAnnotations()would return all matching annotations declared on that class as well as on its implemented interfaces, while intentionally excluding annotations from superclasses. The superclass would then become the root declaring class for the nextAnnotationDescriptorreturned bynext().In other words, the flow would be:
Find the first class in the hierarchy that declares the annotation.
- Use
findAllLocalMergedAnnotations()to retrieve all relevant annotations associated with that root declaring class (including its interfaces). - Call
next()to move to the next root declaring class higher in the hierarchy, if needed.
If that understanding is correct, perhaps the original implementation was intended to behave more like this:
private Set<T> findAllLocalMergedAnnotations() { return Stream.concat(getDirectAnnotations(), getInterfaceAnnotations()) .collect(MergedAnnotationCollectors.toAnnotationSet()); } private Stream<MergedAnnotation<T>> getDirectAnnotations() { return MergedAnnotations.search(SearchStrategy.DIRECT) .from(getRootDeclaringClass()) .stream(getAnnotationType()); } private Stream<MergedAnnotation<T>> getInterfaceAnnotations() { return Arrays.stream(getRootDeclaringClass().getInterfaces()) .flatMap(ifc -> MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY) .from(ifc) .stream(getAnnotationType())); }
Or am I misunderstanding the original design of
AnnotationDescriptor?- Use
Hi @nosan,
Thanks a lot for reviewing the changes and discovering a regression.
I had indeed overlooked the extended interfaces scenario, and I am reopening this issue to address that similar to the approach you have outlined.
Cheers
Sam
- added a commit that references this issue
on Jun 30, 2026 Thank you very much, @sbrannen 👍
The Javadoc states:
However,
AnnotationDescriptor.findAllLocalMergedAnnotationsappears to ignore annotations declared on implemented interfaces. My understanding is that the method should return annotations declared directly on the class together with those from its implemented interfaces, while excluding annotations from superclasses.Instead, the current implementation only returns the annotations declared directly on the class.
sample.zip