Skip to content

Commit eb654dc

Browse files
committed
Allow single element to override array in synthesized annotation
This commit picks up where 8ff9e81 left off. Specifically, this commit introduces support that allows a single element attribute to override an array attribute with a matching component type when synthesizing annotations (e.g., in annotations synthesized from attributes that have been merged from the annotation hierarchy above a composed annotation). Issue: SPR-13972
1 parent e086a5d commit eb654dc

File tree

4 files changed

+259
-74
lines changed

4 files changed

+259
-74
lines changed

spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.AnnotatedElement;
21+
import java.lang.reflect.Array;
2122
import java.lang.reflect.Method;
2223
import java.util.LinkedHashMap;
2324
import java.util.List;
@@ -132,8 +133,16 @@ private static Map<String, Object> enrichAndValidateAttributes(
132133
if (!ClassUtils.isAssignable(requiredReturnType, actualReturnType)) {
133134
boolean converted = false;
134135

136+
// Single element overriding an array of the same type?
137+
if (requiredReturnType.isArray() && requiredReturnType.getComponentType() == actualReturnType) {
138+
Object array = Array.newInstance(requiredReturnType.getComponentType(), 1);
139+
Array.set(array, 0, attributeValue);
140+
attributes.put(attributeName, array);
141+
converted = true;
142+
}
143+
135144
// Nested map representing a single annotation?
136-
if (Annotation.class.isAssignableFrom(requiredReturnType) &&
145+
else if (Annotation.class.isAssignableFrom(requiredReturnType) &&
137146
Map.class.isAssignableFrom(actualReturnType)) {
138147
Class<? extends Annotation> nestedAnnotationType =
139148
(Class<? extends Annotation>) requiredReturnType;

spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java

Lines changed: 133 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@
3333
import org.junit.Ignore;
3434
import org.junit.Rule;
3535
import org.junit.Test;
36+
import org.junit.internal.ArrayComparisonFailure;
3637
import org.junit.rules.ExpectedException;
3738

39+
import org.springframework.core.annotation.AnnotationUtilsTests.WebController;
40+
import org.springframework.core.annotation.AnnotationUtilsTests.WebMapping;
3841
import org.springframework.stereotype.Component;
3942
import org.springframework.util.Assert;
4043
import org.springframework.util.MultiValueMap;
@@ -44,6 +47,7 @@
4447
import static org.hamcrest.Matchers.*;
4548
import static org.junit.Assert.*;
4649
import static org.springframework.core.annotation.AnnotatedElementUtils.*;
50+
import static org.springframework.core.annotation.AnnotationUtilsTests.*;
4751

4852
/**
4953
* Unit tests for {@link AnnotatedElementUtils}.
@@ -379,11 +383,21 @@ public void getMergedAnnotationWithTransitiveImplicitAliases() {
379383
assertGetMergedAnnotation(TransitiveImplicitAliasesContextConfigClass.class, "test.groovy");
380384
}
381385

386+
@Test
387+
public void getMergedAnnotationWithTransitiveImplicitAliasesWithSingleElementOverridingAnArrayViaAliasFor() {
388+
assertGetMergedAnnotation(SingleLocationTransitiveImplicitAliasesContextConfigClass.class, "test.groovy");
389+
}
390+
382391
@Test
383392
public void getMergedAnnotationWithTransitiveImplicitAliasesWithSkippedLevel() {
384393
assertGetMergedAnnotation(TransitiveImplicitAliasesWithSkippedLevelContextConfigClass.class, "test.xml");
385394
}
386395

396+
@Test
397+
public void getMergedAnnotationWithTransitiveImplicitAliasesWithSkippedLevelWithSingleElementOverridingAnArrayViaAliasFor() {
398+
assertGetMergedAnnotation(SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfigClass.class, "test.xml");
399+
}
400+
387401
private void assertGetMergedAnnotation(Class<?> element, String... expected) {
388402
String name = ContextConfig.class.getName();
389403
ContextConfig contextConfig = getMergedAnnotation(element, ContextConfig.class);
@@ -560,6 +574,53 @@ public void findAndSynthesizeAnnotationAttributesOnClassWithAttributeAliasesInTa
560574
assertEquals("TX qualifier via synthesized annotation.", qualifier, annotation.qualifier());
561575
}
562576

577+
@Test
578+
public void findMergedAnnotationAttributesOnClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() {
579+
AnnotationAttributes attributes = assertComponentScanAttributes(TestComponentScanClass.class,
580+
"com.example.app.test");
581+
582+
Filter[] excludeFilters = attributes.getAnnotationArray("excludeFilters", Filter.class);
583+
assertNotNull(excludeFilters);
584+
585+
List<String> patterns = stream(excludeFilters).map(Filter::pattern).collect(toList());
586+
assertEquals(asList("*Test", "*Tests"), patterns);
587+
}
588+
589+
/**
590+
* This test ensures that {@link AnnotationUtils#postProcessAnnotationAttributes}
591+
* uses {@code ObjectUtils.nullSafeEquals()} to check for equality between annotation
592+
* attributes since attributes may be arrays.
593+
*/
594+
@Test
595+
public void findMergedAnnotationAttributesOnClassWithBothAttributesOfAnAliasPairDeclared() {
596+
assertComponentScanAttributes(ComponentScanWithBasePackagesAndValueAliasClass.class, "com.example.app.test");
597+
}
598+
599+
@Test
600+
public void findMergedAnnotationAttributesWithSingleElementOverridingAnArrayViaConvention() {
601+
assertComponentScanAttributes(ConventionBasedSinglePackageComponentScanClass.class, "com.example.app.test");
602+
}
603+
604+
@Test
605+
public void findMergedAnnotationAttributesWithSingleElementOverridingAnArrayViaAliasFor() {
606+
assertComponentScanAttributes(AliasForBasedSinglePackageComponentScanClass.class, "com.example.app.test");
607+
}
608+
609+
private AnnotationAttributes assertComponentScanAttributes(Class<?> element, String... expected) {
610+
AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class);
611+
612+
assertNotNull("Should find @ComponentScan on " + element, attributes);
613+
assertArrayEquals("value: ", expected, attributes.getStringArray("value"));
614+
assertArrayEquals("basePackages: ", expected, attributes.getStringArray("basePackages"));
615+
616+
return attributes;
617+
}
618+
619+
private AnnotationAttributes findMergedAnnotationAttributes(AnnotatedElement element, Class<? extends Annotation> annotationType) {
620+
Assert.notNull(annotationType, "annotationType must not be null");
621+
return AnnotatedElementUtils.findMergedAnnotationAttributes(element, annotationType.getName(), false, false);
622+
}
623+
563624
@Test
564625
public void findMergedAnnotationWithAttributeAliasesInTargetAnnotation() {
565626
Class<?> element = AliasedTransactionalComponentClass.class;
@@ -593,38 +654,6 @@ public void findMergedAnnotationForMultipleMetaAnnotationsWithClashingAttributeN
593654
assertArrayEquals("value", propFiles, testPropSource.value());
594655
}
595656

596-
@Test
597-
public void findMergedAnnotationAttributesOnClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() {
598-
String[] expected = asArray("com.example.app.test");
599-
Class<?> element = TestComponentScanClass.class;
600-
AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class);
601-
602-
assertNotNull("Should find @ComponentScan on " + element, attributes);
603-
assertArrayEquals("basePackages for " + element, expected, attributes.getStringArray("basePackages"));
604-
605-
Filter[] excludeFilters = attributes.getAnnotationArray("excludeFilters", Filter.class);
606-
assertNotNull(excludeFilters);
607-
608-
List<String> patterns = stream(excludeFilters).map(Filter::pattern).collect(toList());
609-
assertEquals(asList("*Test", "*Tests"), patterns);
610-
}
611-
612-
/**
613-
* This test ensures that {@link AnnotationUtils#postProcessAnnotationAttributes}
614-
* uses {@code ObjectUtils.nullSafeEquals()} to check for equality between annotation
615-
* attributes since attributes may be arrays.
616-
*/
617-
@Test
618-
public void findMergedAnnotationAttributesOnClassWithBothAttributesOfAnAliasPairDeclared() {
619-
String[] expected = asArray("com.example.app.test");
620-
Class<?> element = ComponentScanWithBasePackagesAndValueAliasClass.class;
621-
AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class);
622-
623-
assertNotNull("Should find @ComponentScan on " + element, attributes);
624-
assertArrayEquals("value: ", expected, attributes.getStringArray("value"));
625-
assertArrayEquals("basePackages: ", expected, attributes.getStringArray("basePackages"));
626-
}
627-
628657
@Test
629658
public void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() {
630659
final String[] EMPTY = new String[0];
@@ -638,6 +667,24 @@ public void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMeta
638667
assertArrayEquals("classes for " + element, new Class<?>[] {Number.class}, contextConfig.classes());
639668
}
640669

670+
@Test
671+
public void findMergedAnnotationWithSingleElementOverridingAnArrayViaConvention() throws Exception {
672+
assertWebMapping(WebController.class.getMethod("postMappedWithPathAttribute"));
673+
}
674+
675+
@Test
676+
public void findMergedAnnotationWithSingleElementOverridingAnArrayViaAliasFor() throws Exception {
677+
assertWebMapping(WebController.class.getMethod("getMappedWithValueAttribute"));
678+
assertWebMapping(WebController.class.getMethod("getMappedWithPathAttribute"));
679+
}
680+
681+
private void assertWebMapping(AnnotatedElement element) throws ArrayComparisonFailure {
682+
WebMapping webMapping = findMergedAnnotation(element, WebMapping.class);
683+
assertNotNull(webMapping);
684+
assertArrayEquals("value attribute: ", asArray("/test"), webMapping.value());
685+
assertArrayEquals("path attribute: ", asArray("/test"), webMapping.path());
686+
}
687+
641688
@Test
642689
public void javaLangAnnotationTypeViaFindMergedAnnotation() throws Exception {
643690
Constructor<?> deprecatedCtor = Date.class.getConstructor(String.class);
@@ -651,28 +698,10 @@ public void javaxAnnotationTypeViaFindMergedAnnotation() throws Exception {
651698
assertEquals(SpringAppConfigClass.class.getAnnotation(Resource.class), findMergedAnnotation(SpringAppConfigClass.class, Resource.class));
652699
}
653700

654-
655701
private Set<String> names(Class<?>... classes) {
656702
return stream(classes).map(Class::getName).collect(toSet());
657703
}
658704

659-
@SafeVarargs
660-
// The following "varargs" suppression is necessary for javac from OpenJDK
661-
// (1.8.0_60-b27); however, Eclipse warns that it's unnecessary. See the following
662-
// Eclipse issues for details.
663-
//
664-
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=344783
665-
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=349669#c10
666-
// @SuppressWarnings("varargs")
667-
private static <T> T[] asArray(T... arr) {
668-
return arr;
669-
}
670-
671-
private static AnnotationAttributes findMergedAnnotationAttributes(AnnotatedElement element, Class<? extends Annotation> annotationType) {
672-
Assert.notNull(annotationType, "annotationType must not be null");
673-
return AnnotatedElementUtils.findMergedAnnotationAttributes(element, annotationType.getName(), false, false);
674-
}
675-
676705

677706
// -------------------------------------------------------------------------
678707

@@ -881,6 +910,17 @@ static class MetaCycleAnnotatedClass {
881910
String[] groovy() default {};
882911
}
883912

913+
@ImplicitAliasesContextConfig
914+
@Retention(RetentionPolicy.RUNTIME)
915+
@interface SingleLocationTransitiveImplicitAliasesContextConfig {
916+
917+
@AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "xmlFiles")
918+
String xml() default "";
919+
920+
@AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "groovyScripts")
921+
String groovy() default "";
922+
}
923+
884924
@ImplicitAliasesContextConfig
885925
@Retention(RetentionPolicy.RUNTIME)
886926
@interface TransitiveImplicitAliasesWithSkippedLevelContextConfig {
@@ -892,6 +932,17 @@ static class MetaCycleAnnotatedClass {
892932
String[] groovy() default {};
893933
}
894934

935+
@ImplicitAliasesContextConfig
936+
@Retention(RetentionPolicy.RUNTIME)
937+
@interface SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfig {
938+
939+
@AliasFor(annotation = ContextConfig.class, attribute = "locations")
940+
String xml() default "";
941+
942+
@AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "groovyScripts")
943+
String groovy() default "";
944+
}
945+
895946
/**
896947
* Invalid because the configuration declares a value for 'value' and
897948
* requires a value for the aliased 'locations'. So we likely end up with
@@ -958,6 +1009,21 @@ static class MetaCycleAnnotatedClass {
9581009
@Retention(RetentionPolicy.RUNTIME)
9591010
@interface TestComponentScan {
9601011

1012+
@AliasFor(attribute = "basePackages", annotation = ComponentScan.class)
1013+
String[] packages();
1014+
}
1015+
1016+
@ComponentScan
1017+
@Retention(RetentionPolicy.RUNTIME)
1018+
@interface ConventionBasedSinglePackageComponentScan {
1019+
1020+
String basePackages();
1021+
}
1022+
1023+
@ComponentScan
1024+
@Retention(RetentionPolicy.RUNTIME)
1025+
@interface AliasForBasedSinglePackageComponentScan {
1026+
9611027
@AliasFor(attribute = "basePackages", annotation = ComponentScan.class)
9621028
String pkg();
9631029
}
@@ -1132,10 +1198,18 @@ static class ImplicitAliasesContextConfigClass3 {
11321198
static class TransitiveImplicitAliasesContextConfigClass {
11331199
}
11341200

1201+
@SingleLocationTransitiveImplicitAliasesContextConfig(groovy = "test.groovy")
1202+
static class SingleLocationTransitiveImplicitAliasesContextConfigClass {
1203+
}
1204+
11351205
@TransitiveImplicitAliasesWithSkippedLevelContextConfig(xml = "test.xml")
11361206
static class TransitiveImplicitAliasesWithSkippedLevelContextConfigClass {
11371207
}
11381208

1209+
@SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfig(xml = "test.xml")
1210+
static class SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfigClass {
1211+
}
1212+
11391213
@ComposedImplicitAliasesContextConfig
11401214
static class ComposedImplicitAliasesContextConfigClass {
11411215
}
@@ -1152,10 +1226,18 @@ static class AliasedComposedContextConfigAndTestPropSourceClass {
11521226
static class ComponentScanWithBasePackagesAndValueAliasClass {
11531227
}
11541228

1155-
@TestComponentScan(pkg = "com.example.app.test")
1229+
@TestComponentScan(packages = "com.example.app.test")
11561230
static class TestComponentScanClass {
11571231
}
11581232

1233+
@ConventionBasedSinglePackageComponentScan(basePackages = "com.example.app.test")
1234+
static class ConventionBasedSinglePackageComponentScanClass {
1235+
}
1236+
1237+
@AliasForBasedSinglePackageComponentScan(pkg = "com.example.app.test")
1238+
static class AliasForBasedSinglePackageComponentScanClass {
1239+
}
1240+
11591241
@SpringAppConfig(Number.class)
11601242
static class SpringAppConfigClass {
11611243
}

0 commit comments

Comments
 (0)