From 3b145a5a73ae4418d7d5ad2b0f10b0dd48ab3fcf Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 29 Apr 2019 10:44:52 -0700 Subject: [PATCH] Add MergedAnnotation.getTypeHierarchy method Add a `getTypeHierarchy()` method to `MergedAnnotation` that can be used to return the full type hierarchy information. This method is specifically designed to be used in combination with `MergedAnnotationPredicates.unique`. This update also allows us to delete the `parentAndType` method from `AnnotatedElementUtils`. Closes gh-22908 --- .../annotation/AnnotatedElementUtils.java | 9 +-------- .../annotation/AnnotationTypeMapping.java | 19 +++++++++++++++++++ .../core/annotation/MergedAnnotation.java | 10 ++++++++++ .../annotation/MissingMergedAnnotation.java | 6 ++++++ .../core/annotation/TypeMappedAnnotation.java | 6 ++++++ .../AnnotationTypeMappingsTests.java | 9 +++++++++ .../annotation/MergedAnnotationsTests.java | 9 +++++++++ .../MissingMergedAnnotationTests.java | 5 +++++ 8 files changed, 65 insertions(+), 8 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java index 1321fbbef0..65803ca33f 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java @@ -502,7 +502,7 @@ public abstract class AnnotatedElementUtils { Adapt[] adaptations = Adapt.values(classValuesAsString, nestedAnnotationsAsMap); return getAnnotations(element).stream(annotationName) - .filter(MergedAnnotationPredicates.unique(AnnotatedElementUtils::parentAndType)) + .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getTypeHierarchy)) .map(MergedAnnotation::withNonMergedAttributes) .collect(MergedAnnotationCollectors.toMultiValueMap(AnnotatedElementUtils::nullIfEmpty, adaptations)); } @@ -775,13 +775,6 @@ public abstract class AnnotatedElementUtils { repeatableContainers, AnnotationFilter.PLAIN); } - private static Object parentAndType(MergedAnnotation annotation) { - if (annotation.getParent() == null) { - return annotation.getType().getName(); - } - return annotation.getParent().getType().getName() + ":" + annotation.getParent().getType().getName(); - } - @Nullable private static MultiValueMap nullIfEmpty(MultiValueMap map) { return (map.isEmpty() ? null : map); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java index 8a69a28cb6..a48b85c3dc 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java @@ -57,6 +57,8 @@ final class AnnotationTypeMapping { private final Class annotationType; + private final List> annotationTypeHierarchy; + @Nullable private final Annotation annotation; @@ -84,6 +86,9 @@ final class AnnotationTypeMapping { this.root = (parent != null ? parent.getRoot() : this); this.depth = (parent == null ? 0 : parent.getDepth() + 1); this.annotationType = annotationType; + this.annotationTypeHierarchy = merge( + parent != null ? parent.getAnnotationTypeHierarchy() : null, + annotationType); this.annotation = annotation; this.attributes = AttributeMethods.forAnnotationType(annotationType); this.mirrorSets = new MirrorSets(); @@ -98,6 +103,16 @@ final class AnnotationTypeMapping { } + private static List merge(@Nullable List existing, T element) { + if (existing == null) { + return Collections.singletonList(element); + } + List merged = new ArrayList<>(existing.size() + 1); + merged.addAll(existing); + merged.add(element); + return Collections.unmodifiableList(merged); + } + private Map> resolveAliasedForTargets() { Map> aliasedBy = new HashMap<>(); for (int i = 0; i < this.attributes.size(); i++) { @@ -372,6 +387,10 @@ final class AnnotationTypeMapping { return this.annotationType; } + List> getAnnotationTypeHierarchy() { + return this.annotationTypeHierarchy; + } + /** * Get the source annotation for this mapping. This will be the * meta-annotation, or {@code null} if this is the root mapping. diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java index 3f47417c6b..ecefd18099 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java @@ -21,6 +21,7 @@ import java.lang.annotation.Inherited; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Proxy; import java.util.EnumSet; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; @@ -72,6 +73,15 @@ public interface MergedAnnotation { */ Class getType(); + /** + * Return a complete type hierarchy from this annotation to the + * {@link #getRoot() root}. Provides a useful way to uniquely identify a + * merged annotation instance. + * @return the type heirarchy for the annotation + * @see MergedAnnotationPredicates#unique(Function) + */ + List> getTypeHierarchy(); + /** * Determine if the annotation is present on the source. Considers * {@linkplain #isDirectlyPresent() directly present} and diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java index 70360c2db5..58b3b9f013 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java @@ -18,6 +18,7 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; @@ -49,6 +50,11 @@ final class MissingMergedAnnotation extends AbstractMerged throw new NoSuchElementException("Unable to get type for missing annotation"); } + @Override + public List> getTypeHierarchy() { + return Collections.emptyList(); + } + @Override public boolean isPresent() { return false; diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java index 124816a50d..22bc15c6a4 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java @@ -21,6 +21,7 @@ import java.lang.reflect.Array; import java.lang.reflect.Method; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; @@ -139,6 +140,11 @@ final class TypeMappedAnnotation extends AbstractMergedAnn return (Class) this.mapping.getAnnotationType(); } + @Override + public List> getTypeHierarchy() { + return this.mapping.getAnnotationTypeHierarchy(); + } + @Override public boolean isPresent() { return true; diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java index bcea921024..772f4a3e74 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java @@ -245,6 +245,15 @@ public class AnnotationTypeMappingsTests { assertThat(mappings.get(1).getAnnotationType()).isEqualTo(MappedTarget.class); } + @Test + public void getAnnotationTypeHierarchyReturnsTypeHierarchy() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + ThreeDeepA.class); + AnnotationTypeMapping mappingC = mappings.get(2); + assertThat(mappingC.getAnnotationTypeHierarchy()).containsExactly( + ThreeDeepA.class, ThreeDeepB.class, ThreeDeepC.class); + } + @Test public void getAnnotationWhenRootReturnsNull() { AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index 6ae3a02f15..9c1d374491 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -170,6 +170,15 @@ public class MergedAnnotationsTests { assertThat(annotation.getRoot()).isSameAs(annotation); } + @Test + public void getTypeHierarchy() { + MergedAnnotation annotation = MergedAnnotations.from( + ComposedTransactionalComponentClass.class).get( + TransactionalComponent.class); + assertThat(annotation.getTypeHierarchy()).containsExactly( + ComposedTransactionalComponent.class, TransactionalComponent.class); + } + @Test public void collectMultiValueMapFromNonAnnotatedClass() { MultiValueMap map = MergedAnnotations.from( diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MissingMergedAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MissingMergedAnnotationTests.java index 045483cedd..8b5ac6fbca 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MissingMergedAnnotationTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MissingMergedAnnotationTests.java @@ -44,6 +44,11 @@ public class MissingMergedAnnotationTests { assertThatNoSuchElementException().isThrownBy(this.missing::getType); } + @Test + public void getTypeHierarchyReturnsEmptyList() { + assertThat(this.missing.getTypeHierarchy()).isEmpty(); + } + @Test public void isPresentReturnsFalse() { assertThat(this.missing.isPresent()).isFalse();