diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 606f76feba..7bd171a17d 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -1242,7 +1242,9 @@ public abstract class AnnotationUtils { * that are annotated with {@link AliasFor @AliasFor}. *

The supplied map must contain a key-value pair for every attribute * defined in the supplied {@code annotationType} that is not aliased or - * does not have a default value. + * does not have a default value. Nested maps and nested arrays of maps + * will be recursively synthesized into nested annotations or nested + * arrays of annotations, respectively. *

Note that {@link AnnotationAttributes} is a specialized type of * {@link Map} that is an ideal candidate for this method's * {@code attributes} argument. @@ -1259,6 +1261,8 @@ public abstract class AnnotationUtils { * @since 4.2 * @see #synthesizeAnnotation(Annotation, AnnotatedElement) * @see #synthesizeAnnotation(Class) + * @see #getAnnotationAttributes(AnnotatedElement, Annotation) + * @see #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean) */ @SuppressWarnings("unchecked") public static A synthesizeAnnotation(Map attributes, @@ -1298,10 +1302,10 @@ public abstract class AnnotationUtils { } /** - * Synthesize the supplied array of {@code annotations} by - * creating a new array of the same size and type and populating it - * with {@linkplain #synthesizeAnnotation(Annotation) synthesized} - * versions of the annotations from the input array. + * Synthesize an array of annotations from the supplied array + * of {@code annotations} by creating a new array of the same size and + * type and populating it with {@linkplain #synthesizeAnnotation(Annotation) + * synthesized} versions of the annotations from the input array. * @param annotations the array of annotations to synthesize * @param annotatedElement the element that is annotated with the supplied * array of annotations; may be {@code null} if unknown @@ -1326,6 +1330,38 @@ public abstract class AnnotationUtils { return synthesized; } + /** + * Synthesize an array of annotations from the supplied array + * of {@code maps} of annotation attributes by creating a new array of + * {@code annotationType} with the same size and populating it with + * {@linkplain #synthesizeAnnotation(Map, Class, AnnotatedElement) + * synthesized} versions of the maps from the input array. + * @param maps the array of maps of annotation attributes to synthesize + * @param annotationType the type of annotations to synthesize; never + * {@code null} + * @return a new array of synthesized annotations, or {@code null} if + * the supplied array is {@code null} + * @throws AnnotationConfigurationException if invalid configuration of + * {@code @AliasFor} is detected + * @since 4.2.1 + * @see #synthesizeAnnotation(Map, Class, AnnotatedElement) + * @see #synthesizeAnnotationArray(Annotation[], AnnotatedElement) + */ + @SuppressWarnings("unchecked") + static A[] synthesizeAnnotationArray(Map[] maps, Class 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 annotationType) { + Map originalAttributes, Class 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 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 nestedAnnotationType = (Class) 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 nestedAnnotationType = (Class) 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";