Ensure direct @PropertySource annotations override meta-annotations
Prior to this commit, there was an issue with the semantics of property source overrides. Specifically, a @PropertySource annotation present as a meta-annotation on a @Configuration class was registered with higher precedence than a @PropertySource annotation declared closer to (or directly on) the @Configuration class. Consequently, there was no way for a "local" @PropertySource annotation to override properties registered via @PropertySource as a meta-annotation. This commit addresses this issue by introducing a new overloaded getMergedRepeatableAnnotationAttributes() variant in AnnotatedTypeMetadata that allows the caller to supply a sortByReversedMetaDistance flag. When set to `true`, the annotation search results will be sorted in reversed order based on each annotation's meta distance, which effectively orders meta-annotations before annotations that are declared directly on the underlying element. ConfigurationClassParser and AnnotationConfigUtils have been updated to use this new repeatable annotation search method for @PropertySource. Closes gh-31074
This commit is contained in:
parent
285c92bb03
commit
74130d007b
|
@ -286,4 +286,11 @@ public abstract class AnnotationConfigUtils {
|
||||||
return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, false);
|
return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
|
||||||
|
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
|
||||||
|
boolean sortByReversedMetaDistance) {
|
||||||
|
|
||||||
|
return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, false, sortByReversedMetaDistance);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -268,7 +268,7 @@ class ConfigurationClassParser {
|
||||||
// Process any @PropertySource annotations
|
// Process any @PropertySource annotations
|
||||||
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
|
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
|
||||||
sourceClass.getMetadata(), org.springframework.context.annotation.PropertySource.class,
|
sourceClass.getMetadata(), org.springframework.context.annotation.PropertySource.class,
|
||||||
PropertySources.class)) {
|
PropertySources.class, true)) {
|
||||||
if (this.propertySourceRegistry != null) {
|
if (this.propertySourceRegistry != null) {
|
||||||
this.propertySourceRegistry.processPropertySource(propertySource);
|
this.propertySourceRegistry.processPropertySource(propertySource);
|
||||||
}
|
}
|
||||||
|
|
|
@ -231,6 +231,8 @@ class PropertySourceAnnotationTests {
|
||||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MultipleComposedAnnotationsConfig.class);
|
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MultipleComposedAnnotationsConfig.class);
|
||||||
ctx.getBean(MultipleComposedAnnotationsConfig.class);
|
ctx.getBean(MultipleComposedAnnotationsConfig.class);
|
||||||
assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2", "from.p3", "from.p4", "from.p5");
|
assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2", "from.p3", "from.p4", "from.p5");
|
||||||
|
// p5 should 'win' as it is registered via the last "locally declared" direct annotation
|
||||||
|
assertEnvironmentProperty(ctx, "testbean.name", "p5TestBean");
|
||||||
ctx.close();
|
ctx.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.core.type;
|
package org.springframework.core.type;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -179,14 +180,56 @@ public interface AnnotatedTypeMetadata {
|
||||||
* @return the set of all merged repeatable {@code AnnotationAttributes} found,
|
* @return the set of all merged repeatable {@code AnnotationAttributes} found,
|
||||||
* or an empty set if none were found
|
* or an empty set if none were found
|
||||||
* @since 6.1
|
* @since 6.1
|
||||||
|
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean, boolean)
|
||||||
*/
|
*/
|
||||||
default Set<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
|
default Set<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
|
||||||
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
|
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
|
||||||
boolean classValuesAsString) {
|
boolean classValuesAsString) {
|
||||||
|
|
||||||
Adapt[] adaptations = Adapt.values(classValuesAsString, true);
|
return getMergedRepeatableAnnotationAttributes(annotationType, containerType, classValuesAsString, false);
|
||||||
return getAnnotations().stream()
|
}
|
||||||
.filter(MergedAnnotationPredicates.typeIn(containerType, annotationType))
|
|
||||||
|
/**
|
||||||
|
* Retrieve all <em>repeatable annotations</em> of the given type within the
|
||||||
|
* annotation hierarchy <em>above</em> the underlying element (as direct
|
||||||
|
* annotation or meta-annotation); and for each annotation found, merge that
|
||||||
|
* annotation's attributes with <em>matching</em> attributes from annotations
|
||||||
|
* in lower levels of the annotation hierarchy and store the results in an
|
||||||
|
* instance of {@link AnnotationAttributes}.
|
||||||
|
* <p>{@link org.springframework.core.annotation.AliasFor @AliasFor} semantics
|
||||||
|
* are fully supported, both within a single annotation and within annotation
|
||||||
|
* hierarchies.
|
||||||
|
* <p>If the {@code sortByReversedMetaDistance} flag is set to {@code true},
|
||||||
|
* the results will be sorted in {@link Comparator#reversed() reversed} order
|
||||||
|
* based on each annotation's {@linkplain MergedAnnotation#getDistance()
|
||||||
|
* meta distance}, which effectively orders meta-annotations before annotations
|
||||||
|
* that are declared directly on the underlying element.
|
||||||
|
* @param annotationType the annotation type to find
|
||||||
|
* @param containerType the type of the container that holds the annotations
|
||||||
|
* @param classValuesAsString whether to convert class references to {@code String}
|
||||||
|
* class names for exposure as values in the returned {@code AnnotationAttributes},
|
||||||
|
* instead of {@code Class} references which might potentially have to be loaded
|
||||||
|
* first
|
||||||
|
* @param sortByReversedMetaDistance {@code true} if the results should be
|
||||||
|
* sorted in reversed order based on each annotation's meta distance
|
||||||
|
* @return the set of all merged repeatable {@code AnnotationAttributes} found,
|
||||||
|
* or an empty set if none were found
|
||||||
|
* @since 6.1
|
||||||
|
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean)
|
||||||
|
*/
|
||||||
|
default Set<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
|
||||||
|
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
|
||||||
|
boolean classValuesAsString, boolean sortByReversedMetaDistance) {
|
||||||
|
|
||||||
|
Stream<MergedAnnotation<Annotation>> stream = getAnnotations().stream()
|
||||||
|
.filter(MergedAnnotationPredicates.typeIn(containerType, annotationType));
|
||||||
|
|
||||||
|
if (sortByReversedMetaDistance) {
|
||||||
|
stream = stream.sorted(reversedMetaDistance());
|
||||||
|
}
|
||||||
|
|
||||||
|
Adapt[] adaptations = Adapt.values(false, true);
|
||||||
|
return stream
|
||||||
.map(annotation -> annotation.asAnnotationAttributes(adaptations))
|
.map(annotation -> annotation.asAnnotationAttributes(adaptations))
|
||||||
.flatMap(attributes -> {
|
.flatMap(attributes -> {
|
||||||
if (containerType.equals(attributes.annotationType())) {
|
if (containerType.equals(attributes.annotationType())) {
|
||||||
|
@ -197,4 +240,9 @@ public interface AnnotatedTypeMetadata {
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Comparator<MergedAnnotation<Annotation>> reversedMetaDistance() {
|
||||||
|
return Comparator.<MergedAnnotation<Annotation>> comparingInt(MergedAnnotation::getDistance).reversed();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -263,6 +263,20 @@ class AnnotationMetadataTests {
|
||||||
assertRepeatableAnnotations(metadata);
|
assertRepeatableAnnotations(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test // gh-31074
|
||||||
|
void multipleComposedRepeatableAnnotationsSortedByReversedMetaDistanceUsingStandardAnnotationMetadata() {
|
||||||
|
AnnotationMetadata metadata = AnnotationMetadata.introspect(MultipleComposedRepeatableAnnotationsClass.class);
|
||||||
|
assertRepeatableAnnotationsSortedByReversedMetaDistance(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // gh-31074
|
||||||
|
void multipleComposedRepeatableAnnotationsSortedByReversedMetaDistanceUsingSimpleAnnotationMetadata() throws Exception {
|
||||||
|
MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
|
||||||
|
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(MultipleComposedRepeatableAnnotationsClass.class.getName());
|
||||||
|
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
|
||||||
|
assertRepeatableAnnotationsSortedByReversedMetaDistance(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
@Test // gh-31041
|
@Test // gh-31041
|
||||||
void multipleRepeatableAnnotationsInContainersUsingStandardAnnotationMetadata() {
|
void multipleRepeatableAnnotationsInContainersUsingStandardAnnotationMetadata() {
|
||||||
AnnotationMetadata metadata = AnnotationMetadata.introspect(MultipleRepeatableAnnotationsInContainersClass.class);
|
AnnotationMetadata metadata = AnnotationMetadata.introspect(MultipleRepeatableAnnotationsInContainersClass.class);
|
||||||
|
@ -318,6 +332,18 @@ class AnnotationMetadataTests {
|
||||||
.containsExactly("A", "B", "C", "D");
|
.containsExactly("A", "B", "C", "D");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void assertRepeatableAnnotationsSortedByReversedMetaDistance(AnnotationMetadata metadata) {
|
||||||
|
// Note: although the real @ComponentScan annotation is not looked up using
|
||||||
|
// "sortByReversedMetaDistance" semantics, we can still use @TestComponentScan
|
||||||
|
// to verify the expected behavior.
|
||||||
|
Set<AnnotationAttributes> attributesSet =
|
||||||
|
metadata.getMergedRepeatableAnnotationAttributes(TestComponentScan.class, TestComponentScans.class, false, true);
|
||||||
|
assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("value")).flatMap(Arrays::stream))
|
||||||
|
.containsExactly("C", "D", "A", "B");
|
||||||
|
assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("basePackages")).flatMap(Arrays::stream))
|
||||||
|
.containsExactly("C", "D", "A", "B");
|
||||||
|
}
|
||||||
|
|
||||||
private static void assertRepeatableAnnotations(Set<TestComponentScan> annotations) {
|
private static void assertRepeatableAnnotations(Set<TestComponentScan> annotations) {
|
||||||
assertThat(annotations.stream().map(TestComponentScan::value).flatMap(Arrays::stream))
|
assertThat(annotations.stream().map(TestComponentScan::value).flatMap(Arrays::stream))
|
||||||
.containsExactly("A", "B", "C", "D");
|
.containsExactly("A", "B", "C", "D");
|
||||||
|
|
Loading…
Reference in New Issue