diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java index b840edfb45..8c3d8056b1 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java @@ -20,6 +20,7 @@ import java.lang.annotation.Annotation; import java.lang.annotation.Inherited; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.Collection; import java.util.function.Predicate; import java.util.stream.Stream; @@ -368,6 +369,26 @@ public interface MergedAnnotations extends Iterable return TypeMappedAnnotations.from(source, annotations, repeatableContainers, annotationFilter); } + /** + * Create a new {@link MergedAnnotations} instance from the specified + * collection of directly present annotations. This method allows a + * {@link MergedAnnotations} instance to be created from annotations that + * are not necessarily loaded using reflection. The provided annotations + * must all be {@link MergedAnnotation#isDirectlyPresent() directly present} + * and must have a {@link MergedAnnotation#getAggregateIndex() aggregate + * index} of {@code 0}. + *

+ * The resulting {@link MergedAnnotations} instance will contain both the + * specified annotations, and any meta-annotations that can be read using + * reflection. + * @param annotations the annotations to include + * @return a {@link MergedAnnotations} instance containing the annotations + * @see MergedAnnotation#of(ClassLoader, Object, Class, java.util.Map) + */ + static MergedAnnotations of(Collection> annotations) { + return MergedAnnotationsCollection.of(annotations); + } + /** * Search strategies supported by diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java new file mode 100644 index 0000000000..48308cdafe --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java @@ -0,0 +1,315 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link MergedAnnotations} implementation backed by a {@link Collection} + * {@link MergedAnnotation} instances that represent direct annotations. + * + * @author Phillip Webb + * @since 5.2 + * @see MergedAnnotations#of(Collection) + */ +final class MergedAnnotationsCollection implements MergedAnnotations { + + private final MergedAnnotation[] annotations; + + private final AnnotationTypeMappings[] mappings; + + private MergedAnnotationsCollection(Collection> annotations) { + Assert.notNull(annotations, "Annotations must not be null"); + this.annotations = annotations.toArray(new MergedAnnotation[0]); + this.mappings = new AnnotationTypeMappings[this.annotations.length]; + for (int i = 0; i < this.annotations.length; i++) { + MergedAnnotation annotation = this.annotations[i]; + Assert.notNull(annotation, "Annotation must not be null"); + Assert.isTrue(annotation.isDirectlyPresent(), "Annotation must be directly present"); + Assert.isTrue(annotation.getAggregateIndex() == 0, "Annotation must have aggregate index of zero"); + this.mappings[i] = AnnotationTypeMappings.forAnnotationType( + annotation.getType()); + } + } + + @Override + public Iterator> iterator() { + return Spliterators.iterator(spliterator()); + } + + @Override + public Spliterator> spliterator() { + return spliterator(null); + } + + private Spliterator> spliterator( + @Nullable Object annotationType) { + return new AnnotationsSpliterator<>(annotationType); + } + + @Override + public boolean isPresent(Class annotationType) { + return isPresent(annotationType, false); + } + + @Override + public boolean isPresent(String annotationType) { + return isPresent(annotationType, false); + } + + @Override + public boolean isDirectlyPresent(Class annotationType) { + return isPresent(annotationType, true); + } + + @Override + public boolean isDirectlyPresent(String annotationType) { + return isPresent(annotationType, true); + } + + private boolean isPresent(Object requiredType, boolean directOnly) { + for (MergedAnnotation annotation : this.annotations) { + Class type = annotation.getType(); + if (type == requiredType || type.getName().equals(requiredType)) { + return true; + } + } + if (!directOnly) { + for (AnnotationTypeMappings mappings : this.mappings) { + for (int i = 1; i < mappings.size(); i++) { + AnnotationTypeMapping mapping = mappings.get(i); + if (isMappingForType(mapping, requiredType)) { + return true; + } + } + } + } + return false; + } + + @Override + public MergedAnnotation get(Class annotationType) { + return get(annotationType, null, null); + } + + @Override + public MergedAnnotation get(Class annotationType, + @Nullable Predicate> predicate) { + + return get(annotationType, predicate, null); + } + + @Override + public MergedAnnotation get(Class annotationType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector) { + MergedAnnotation result = find(annotationType, predicate, selector); + return (result != null ? result : MergedAnnotation.missing()); + } + + @Override + public MergedAnnotation get(String annotationType) { + return get(annotationType, null, null); + } + + @Override + public MergedAnnotation get(String annotationType, + @Nullable Predicate> predicate) { + + return get(annotationType, predicate, null); + } + + @Override + public MergedAnnotation get(String annotationType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector) { + + MergedAnnotation result = find(annotationType, predicate, selector); + return (result != null ? result : MergedAnnotation.missing()); + } + + @SuppressWarnings("unchecked") + private MergedAnnotation find(Object requiredType, + Predicate> predicate, + MergedAnnotationSelector selector) { + if (selector == null) { + selector = MergedAnnotationSelectors.nearest(); + } + MergedAnnotation result = null; + for (int i = 0; i < this.annotations.length; i++) { + MergedAnnotation root = this.annotations[i]; + AnnotationTypeMappings mappings = this.mappings[i]; + for (int mappingIndex = 0; mappingIndex < mappings.size(); mappingIndex++) { + AnnotationTypeMapping mapping = mappings.get(mappingIndex); + if (!isMappingForType(mapping, requiredType)) { + continue; + } + MergedAnnotation candidate = (mappingIndex == 0 + ? (MergedAnnotation) root + : TypeMappedAnnotation.createIfPossible(mapping, root, IntrospectionFailureLogger.INFO)); + if (candidate != null && (predicate == null || predicate.test(candidate))) { + if (selector.isBestCandidate(candidate)) { + return candidate; + } + result = (result != null ? selector.select(result, candidate) : candidate); + } + } + } + return result; + } + + @Override + public Stream> stream(Class annotationType) { + return StreamSupport.stream(spliterator(annotationType), false); + } + + @Override + public Stream> stream(String annotationType) { + return StreamSupport.stream(spliterator(annotationType), false); + } + + @Override + public Stream> stream() { + return StreamSupport.stream(spliterator(), false); + } + + private static boolean isMappingForType(AnnotationTypeMapping mapping, @Nullable Object requiredType) { + if (requiredType == null) { + return true; + } + Class actualType = mapping.getAnnotationType(); + return (actualType == requiredType || actualType.getName().equals(requiredType)); + } + + static MergedAnnotations of(Collection> annotations) { + Assert.notNull(annotations, "Annotations must not be null"); + if(annotations.isEmpty()) { + return TypeMappedAnnotations.NONE; + } + return new MergedAnnotationsCollection(annotations); + } + + + private class AnnotationsSpliterator implements Spliterator> { + + @Nullable + private Object requiredType; + + private final int[] mappingCursors; + + public AnnotationsSpliterator(@Nullable Object requiredType) { + this.mappingCursors = new int[annotations.length]; + this.requiredType = requiredType; + } + + @Override + public boolean tryAdvance(Consumer> action) { + int lowestDepth = Integer.MAX_VALUE; + int annotationResult = -1; + for (int annotationIndex = 0; annotationIndex < annotations.length; annotationIndex++) { + AnnotationTypeMapping mapping = getNextSuitableMapping(annotationIndex); + if (mapping != null && mapping.getDepth() < lowestDepth) { + annotationResult = annotationIndex; + lowestDepth = mapping.getDepth(); + } + if (lowestDepth == 0) { + break; + } + } + if (annotationResult != -1) { + MergedAnnotation mergedAnnotation = createMergedAnnotationIfPossible(annotationResult, this.mappingCursors[annotationResult]); + this.mappingCursors[annotationResult]++; + if (mergedAnnotation == null) { + return tryAdvance(action); + } + action.accept(mergedAnnotation); + return true; + } + return false; + } + + @Nullable + private AnnotationTypeMapping getNextSuitableMapping(int annotationIndex) { + AnnotationTypeMapping mapping; + do { + mapping = getMapping(annotationIndex, this.mappingCursors[annotationIndex]); + if (mapping != null && isMappingForType(mapping, this.requiredType)) { + return mapping; + } + this.mappingCursors[annotationIndex]++; + } + while (mapping != null); + return null; + } + + @Nullable + private AnnotationTypeMapping getMapping(int annotationIndex, int mappingIndex) { + AnnotationTypeMappings mappings = MergedAnnotationsCollection.this.mappings[annotationIndex]; + return (mappingIndex < mappings.size() ? mappings.get(mappingIndex) : null); + } + + @Nullable + @SuppressWarnings("unchecked") + private MergedAnnotation createMergedAnnotationIfPossible( + int annotationIndex, int mappingIndex) { + MergedAnnotation root = annotations[annotationIndex]; + if(mappingIndex == 0) { + return (MergedAnnotation) root; + } + IntrospectionFailureLogger logger = (this.requiredType != null + ? IntrospectionFailureLogger.INFO + : IntrospectionFailureLogger.DEBUG); + return TypeMappedAnnotation.createIfPossible( + mappings[annotationIndex].get(mappingIndex), root, logger); + } + + @Override + public Spliterator> trySplit() { + return null; + } + + @Override + public long estimateSize() { + int size = 0; + for (int i = 0; i < annotations.length; i++) { + AnnotationTypeMappings mappings = MergedAnnotationsCollection.this.mappings[i]; + int numberOfMappings = mappings.size(); + numberOfMappings -= Math.min(this.mappingCursors[i], mappings.size()); + size += numberOfMappings; + } + return size; + } + + @Override + public int characteristics() { + return NONNULL | IMMUTABLE; + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java index ed0ab5a6f6..6408c6b2f0 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java @@ -42,7 +42,10 @@ final class TypeMappedAnnotations implements MergedAnnotations { private static final AnnotationFilter FILTER_ALL = (annotationType -> true); - private static final MergedAnnotations NONE = new TypeMappedAnnotations( + /** + * Shared instance that can be used when there are no annotations. + */ + static final MergedAnnotations NONE = new TypeMappedAnnotations( null, new Annotation[0], RepeatableContainers.none(), FILTER_ALL); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsCollectionTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsCollectionTests.java new file mode 100644 index 0000000000..db1be5b3f4 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsCollectionTests.java @@ -0,0 +1,312 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Spliterator; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +/** + * Tests for {@link MergedAnnotationsCollection}. + * + * @author Phillip Webb + */ +public class MergedAnnotationsCollectionTests { + + @Test + public void ofWhenDirectAnnotationsIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> MergedAnnotationsCollection.of(null)).withMessage( + "Annotations must not be null"); + } + + @Test + public void ofWhenEmptyReturnsSharedNoneInstance() { + MergedAnnotations annotations = MergedAnnotationsCollection.of(new ArrayList<>()); + assertThat(annotations).isSameAs(TypeMappedAnnotations.NONE); + } + + @Test + public void createWhenAnnotationIsNotDirectlyPresentThrowsException() { + MergedAnnotation annotation = mock(MergedAnnotation.class); + given(annotation.isDirectlyPresent()).willReturn(false); + assertThatIllegalArgumentException().isThrownBy(() -> { + MergedAnnotationsCollection.of(Collections.singleton(annotation)); + }).withMessage("Annotation must be directly present"); + } + + @Test + public void createWhenAnnotationAggregateIndexIsNotZeroThrowsException() { + MergedAnnotation annotation = mock(MergedAnnotation.class); + given(annotation.isDirectlyPresent()).willReturn(true); + given(annotation.getAggregateIndex()).willReturn(1); + assertThatIllegalArgumentException().isThrownBy(() -> { + MergedAnnotationsCollection.of(Collections.singleton(annotation)); + }).withMessage("Annotation must have aggregate index of zero"); + } + + @Test + public void interateIteratesInCorrectOrder() { + MergedAnnotations annotations = getDirectAndSimple(); + List> types = new ArrayList<>(); + for (MergedAnnotation annotation : annotations) { + types.add(annotation.getType()); + } + assertThat(types).containsExactly(Direct.class, Simple.class, Meta1.class, + Meta2.class, Meta11.class); + } + + @Test + public void spliteratorIteratesInCorrectOrder() { + MergedAnnotations annotations = getDirectAndSimple(); + Spliterator> spliterator = annotations.spliterator(); + List> types = new ArrayList<>(); + spliterator.forEachRemaining(annotation -> types.add(annotation.getType())); + assertThat(types).containsExactly(Direct.class, Simple.class, Meta1.class, + Meta2.class, Meta11.class); + } + + @Test + public void spliteratorEstimatesSize() { + MergedAnnotations annotations = getDirectAndSimple(); + Spliterator> spliterator = annotations.spliterator(); + assertThat(spliterator.estimateSize()).isEqualTo(5); + spliterator.tryAdvance( + annotation -> assertThat(annotation.getType()).isEqualTo(Direct.class)); + assertThat(spliterator.estimateSize()).isEqualTo(4); + } + + @Test + public void isPresentWhenDirectlyPresentReturnsTrue() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.isPresent(Direct.class)).isTrue(); + assertThat(annotations.isPresent(Direct.class.getName())).isTrue(); + } + + @Test + public void isPresentWhenMetaPresentReturnsTrue() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.isPresent(Meta11.class)).isTrue(); + assertThat(annotations.isPresent(Meta11.class.getName())).isTrue(); + } + + @Test + public void isPresentWhenNotPresentReturnsFalse() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.isPresent(Missing.class)).isFalse(); + assertThat(annotations.isPresent(Missing.class.getName())).isFalse(); + + } + + @Test + public void isDirectlyPresentWhenDirectlyPresentReturnsTrue() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.isDirectlyPresent(Direct.class)).isTrue(); + assertThat(annotations.isDirectlyPresent(Direct.class.getName())).isTrue(); + } + + @Test + public void isDirectlyPresentWhenMetaPresentReturnsFalse() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.isDirectlyPresent(Meta11.class)).isFalse(); + assertThat(annotations.isDirectlyPresent(Meta11.class.getName())).isFalse(); + } + + @Test + public void isDirectlyPresentWhenNotPresentReturnsFalse() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.isDirectlyPresent(Missing.class)).isFalse(); + assertThat(annotations.isDirectlyPresent(Missing.class.getName())).isFalse(); + } + + @Test + public void getReturnsAppropriateAnnotation() { + MergedAnnotations annotations = getMutiRoute1(); + assertThat(annotations.get(MutiRouteTarget.class).getString( + MergedAnnotation.VALUE)).isEqualTo("12"); + assertThat(annotations.get(MutiRouteTarget.class.getName()).getString( + MergedAnnotation.VALUE)).isEqualTo("12"); + } + + @Test + public void getWhenNotPresentReturnsMissing() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.get(Missing.class)).isEqualTo(MergedAnnotation.missing()); + } + + @Test + public void getWithPredicateReturnsOnlyMatching() { + MergedAnnotations annotations = getMutiRoute1(); + assertThat(annotations.get(MutiRouteTarget.class, + annotation -> annotation.getDepth() >= 3).getString( + MergedAnnotation.VALUE)).isEqualTo("111"); + } + + @Test + public void getWithSelectorReturnsSelected() { + MergedAnnotations annotations = getMutiRoute1(); + MergedAnnotationSelector deepest = (existing, + candidate) -> candidate.getDepth() > existing.getDepth() ? candidate + : existing; + assertThat(annotations.get(MutiRouteTarget.class, null, deepest).getString( + MergedAnnotation.VALUE)).isEqualTo("111"); + } + + @Test + public void streamStreamsInCorrectOrder() { + MergedAnnotations annotations = getDirectAndSimple(); + List> types = new ArrayList<>(); + annotations.stream().forEach(annotation -> types.add(annotation.getType())); + assertThat(types).containsExactly(Direct.class, Simple.class, Meta1.class, + Meta2.class, Meta11.class); + } + + @Test + public void streamWithTypeStreamsInCorrectOrder() { + MergedAnnotations annotations = getMutiRoute1(); + List values = new ArrayList<>(); + annotations.stream(MutiRouteTarget.class).forEach( + annotation -> values.add(annotation.getString(MergedAnnotation.VALUE))); + assertThat(values).containsExactly("12", "111"); + } + + @Test + public void getMetaWhenRootHasAttributeValuesShouldAlaisAttributes() { + MergedAnnotation root = MergedAnnotation.of(null, null, Alaised.class, + Collections.singletonMap("testAlias", "test")); + MergedAnnotations annotations = MergedAnnotationsCollection.of( + Collections.singleton(root)); + MergedAnnotation metaAnnotation = annotations.get(AlaisTarget.class); + assertThat(metaAnnotation.getString("test")).isEqualTo("test"); + } + + @Test + public void getMetaWhenRootHasNoAttributeValuesShouldAlaisAttributes() { + MergedAnnotation root = MergedAnnotation.of(null, null, Alaised.class, + Collections.emptyMap()); + MergedAnnotations annotations = MergedAnnotationsCollection.of( + Collections.singleton(root)); + MergedAnnotation metaAnnotation = annotations.get(AlaisTarget.class); + assertThat(root.getString("testAlias")).isEqualTo("newdefault"); + assertThat(metaAnnotation.getString("test")).isEqualTo("newdefault"); + } + + private MergedAnnotations getDirectAndSimple() { + List> list = new ArrayList<>(); + list.add(MergedAnnotation.of(null, null, Direct.class, Collections.emptyMap())); + list.add(MergedAnnotation.of(null, null, Simple.class, Collections.emptyMap())); + return MergedAnnotationsCollection.of(list); + } + + private MergedAnnotations getMutiRoute1() { + List> list = new ArrayList<>(); + list.add(MergedAnnotation.of(null, null, MutiRoute1.class, + Collections.emptyMap())); + return MergedAnnotationsCollection.of(list); + } + + @Meta1 + @Meta2 + @Retention(RetentionPolicy.RUNTIME) + @interface Direct { + + } + + @Meta11 + @Retention(RetentionPolicy.RUNTIME) + @interface Meta1 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Meta2 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Meta11 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Simple { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Missing { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface MutiRouteTarget { + + String value(); + + } + + @MutiRoute11 + @MutiRoute12 + @Retention(RetentionPolicy.RUNTIME) + @interface MutiRoute1 { + + } + + @MutiRoute111 + @Retention(RetentionPolicy.RUNTIME) + @interface MutiRoute11 { + + } + + @MutiRouteTarget("12") + @Retention(RetentionPolicy.RUNTIME) + @interface MutiRoute12 { + + } + + @MutiRouteTarget("111") + @Retention(RetentionPolicy.RUNTIME) + @interface MutiRoute111 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AlaisTarget { + + String test() default "default"; + + } + + @Retention(RetentionPolicy.RUNTIME) + @AlaisTarget + @interface Alaised { + + @AliasFor(annotation = AlaisTarget.class, attribute = "test") + String testAlias() default "newdefault"; + + } + +}