annotationType) {
+ Assert.notNull(annotationType, "annotationType must not be null");
+
+ if (maps == null) {
+ return null;
+ }
+
+ A[] synthesized = (A[]) Array.newInstance(annotationType, maps.length);
+ for (int i = 0; i < maps.length; i++) {
+ synthesized[i] = synthesizeAnnotation(maps[i], annotationType, null);
+ }
+ return synthesized;
+ }
+
/**
* Get a map of all attribute alias pairs, declared via {@code @AliasFor}
* in the supplied annotation type.
diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java
index 05ab3bd185..e8a2d9adc5 100644
--- a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java
+++ b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java
@@ -74,20 +74,24 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib
/**
- * Enrich and validate the supplied {@code attributes} map by ensuring
+ * Enrich and validate the supplied attributes map by ensuring
* that it contains a non-null entry for each annotation attribute in
* the specified {@code annotationType} and that the type of the entry
* matches the return type for the corresponding annotation attribute.
+ * If an entry is a map (presumably of annotation attributes), an
+ * attempt will be made to synthesize an annotation from it. Similarly,
+ * if an entry is an array of maps, an attempt will be made to synthesize
+ * an array of annotations from those maps.
*
If an attribute is missing in the supplied map, it will be set
- * either to value of its alias (if an alias value exists) or to the
+ * either to the value of its alias (if an alias exists) or to the
* value of the attribute's default value (if defined), and otherwise
* an {@link IllegalArgumentException} will be thrown.
- * @see AliasFor
*/
+ @SuppressWarnings("unchecked")
private static Map enrichAndValidateAttributes(
- Map original, Class extends Annotation> annotationType) {
+ Map originalAttributes, Class extends Annotation> annotationType) {
- Map attributes = new HashMap(original);
+ Map attributes = new HashMap(originalAttributes);
Map attributeAliasMap = getAttributeAliasMap(annotationType);
for (Method attributeMethod : getAttributeMethods(annotationType)) {
@@ -122,13 +126,41 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib
attributes, attributeName, annotationType.getName()));
}
- // else, ensure correct type
- Class> returnType = attributeMethod.getReturnType();
- if (!ClassUtils.isAssignable(returnType, attributeValue.getClass())) {
- throw new IllegalArgumentException(String.format(
- "Attributes map [%s] returned a value of type [%s] for attribute [%s], "
- + "but a value of type [%s] is required as defined by annotation type [%s].", attributes,
- attributeValue.getClass().getName(), attributeName, returnType.getName(), annotationType.getName()));
+ // finally, ensure correct type
+ Class> requiredReturnType = attributeMethod.getReturnType();
+ Class extends Object> actualReturnType = attributeValue.getClass();
+ if (!ClassUtils.isAssignable(requiredReturnType, actualReturnType)) {
+ boolean converted = false;
+
+ // Nested map representing a single annotation?
+ if (Annotation.class.isAssignableFrom(requiredReturnType)
+ && Map.class.isAssignableFrom(actualReturnType)) {
+
+ Class extends Annotation> nestedAnnotationType = (Class extends Annotation>) requiredReturnType;
+ Map map = (Map) attributeValue;
+ attributes.put(attributeName, synthesizeAnnotation(map, nestedAnnotationType, null));
+ converted = true;
+ }
+
+ // Nested array of maps representing an array of annotations?
+ else if (requiredReturnType.isArray()
+ && Annotation.class.isAssignableFrom(requiredReturnType.getComponentType())
+ && actualReturnType.isArray()
+ && Map.class.isAssignableFrom(actualReturnType.getComponentType())) {
+
+ Class extends Annotation> nestedAnnotationType = (Class extends Annotation>) requiredReturnType.getComponentType();
+ Map[] maps = (Map[]) attributeValue;
+ attributes.put(attributeName, synthesizeAnnotationArray(maps, nestedAnnotationType));
+ converted = true;
+ }
+
+ if (!converted) {
+ throw new IllegalArgumentException(String.format(
+ "Attributes map [%s] returned a value of type [%s] for attribute [%s], "
+ + "but a value of type [%s] is required as defined by annotation type [%s].",
+ attributes, actualReturnType.getName(), attributeName, requiredReturnType.getName(),
+ annotationType.getName()));
+ }
}
}
diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java
index 9ff5025581..b1095d13c9 100644
--- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java
+++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java
@@ -883,6 +883,64 @@ public class AnnotationUtilsTests {
assertEquals("value from synthesized component: ", "webController", synthesizedComponent.value());
}
+ @Test
+ @SuppressWarnings("unchecked")
+ public void synthesizeAnnotationFromMapWithNestedMap() throws Exception {
+ ComponentScanSingleFilter componentScan = ComponentScanSingleFilterClass.class.getAnnotation(ComponentScanSingleFilter.class);
+ assertNotNull(componentScan);
+ assertEquals("value from ComponentScan: ", "*Foo", componentScan.value().pattern());
+
+ AnnotationAttributes attributes = getAnnotationAttributes(ComponentScanSingleFilterClass.class, componentScan,
+ false, true);
+ assertNotNull(attributes);
+ assertEquals(ComponentScanSingleFilter.class, attributes.annotationType());
+
+ Map filterMap = (Map) attributes.get("value");
+ assertNotNull(filterMap);
+ assertEquals("*Foo", filterMap.get("pattern"));
+
+ // Modify nested map
+ filterMap.put("pattern", "newFoo");
+ filterMap.put("enigma", 42);
+
+ ComponentScanSingleFilter synthesizedComponentScan = synthesizeAnnotation(attributes,
+ ComponentScanSingleFilter.class, ComponentScanSingleFilterClass.class);
+ assertNotNull(synthesizedComponentScan);
+
+ assertNotSame(componentScan, synthesizedComponentScan);
+ assertEquals("value from synthesized ComponentScan: ", "newFoo", synthesizedComponentScan.value().pattern());
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void synthesizeAnnotationFromMapWithNestedArrayOfMaps() throws Exception {
+ ComponentScan componentScan = ComponentScanClass.class.getAnnotation(ComponentScan.class);
+ assertNotNull(componentScan);
+
+ AnnotationAttributes attributes = getAnnotationAttributes(ComponentScanClass.class, componentScan, false, true);
+ assertNotNull(attributes);
+ assertEquals(ComponentScan.class, attributes.annotationType());
+
+ Map[] filters = (Map[]) attributes.get("excludeFilters");
+ assertNotNull(filters);
+
+ List patterns = stream(filters).map(m -> (String) m.get("pattern")).collect(toList());
+ assertEquals(asList("*Foo", "*Bar"), patterns);
+
+ // Modify nested maps
+ filters[0].put("pattern", "newFoo");
+ filters[0].put("enigma", 42);
+ filters[1].put("pattern", "newBar");
+ filters[1].put("enigma", 42);
+
+ ComponentScan synthesizedComponentScan = synthesizeAnnotation(attributes, ComponentScan.class, ComponentScanClass.class);
+ assertNotNull(synthesizedComponentScan);
+
+ assertNotSame(componentScan, synthesizedComponentScan);
+ patterns = stream(synthesizedComponentScan.excludeFilters()).map(Filter::pattern).collect(toList());
+ assertEquals(asList("newFoo", "newBar"), patterns);
+ }
+
@Test
public void synthesizeAnnotationFromDefaultsWithoutAttributeAliases() throws Exception {
AnnotationWithDefaults annotationWithDefaults = synthesizeAnnotation(AnnotationWithDefaults.class);
@@ -1711,6 +1769,18 @@ public class AnnotationUtilsTests {
static class ComponentScanClass {
}
+ /**
+ * Mock of {@code org.springframework.context.annotation.ComponentScan}
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface ComponentScanSingleFilter {
+ Filter value();
+ }
+
+ @ComponentScanSingleFilter(@Filter(pattern = "*Foo"))
+ static class ComponentScanSingleFilterClass {
+ }
+
@Retention(RetentionPolicy.RUNTIME)
@interface AnnotationWithDefaults {
String text() default "enigma";