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 bddd71e1552..5fc7ef63346 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 @@ -1088,8 +1088,9 @@ public abstract class AnnotationUtils { * annotation of the specified {@code annotationType} and transparently * enforces attribute alias semantics for annotation attributes * that are annotated with {@link AliasFor @AliasFor}. - *

The supplied map must contain key-value pairs for every attribute - * defined by the supplied {@code annotationType}. + *

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. *

Note that {@link AnnotationAttributes} is a specialized type of * {@link Map} that is an ideal candidate for this method's * {@code attributes} argument. @@ -1100,7 +1101,10 @@ public abstract class AnnotationUtils { * corresponding to the supplied attributes; may be {@code null} if unknown * @return the synthesized annotation, or {@code null} if the supplied attributes * map is {@code null} - * @throws AnnotationConfigurationException if invalid configuration is detected + * @throws IllegalArgumentException if a required attribute is missing or if an + * attribute is not of the correct type + * @throws AnnotationConfigurationException if invalid configuration of + * {@code @AliasFor} is detected * @since 4.2 * @see #synthesizeAnnotation(Annotation, AnnotatedElement) */ 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 2a8704fabd1..ecbb1317a15 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 @@ -42,8 +42,9 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib /** * Construct a new {@code MapAnnotationAttributeExtractor}. - *

The supplied map must contain key-value pairs for every attribute - * defined in the supplied {@code annotationType}. + *

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. * @param attributes the map of annotation attributes; never {@code null} * @param annotationType the type of annotation to synthesize; never {@code null} * @param annotatedElement the element that is annotated with the annotation @@ -51,8 +52,7 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib */ MapAnnotationAttributeExtractor(Map attributes, Class annotationType, AnnotatedElement annotatedElement) { - super(annotationType, annotatedElement, new HashMap(attributes)); - validateAttributes(attributes, annotationType); + super(annotationType, annotatedElement, enrichAndValidateAttributes(new HashMap(attributes), annotationType)); } @Override @@ -71,22 +71,55 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib } /** - * Validate the supplied {@code attributes} map by verifying 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. + * Enrich and validate the supplied {@code 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 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 + * value of the attribute's default value (if defined), and otherwise + * an {@link IllegalArgumentException} will be thrown. + * @see AliasFor */ - private static void validateAttributes(Map attributes, Class annotationType) { + private static Map enrichAndValidateAttributes(Map attributes, + Class annotationType) { + + Map attributeAliasMap = getAttributeAliasMap(annotationType); + for (Method attributeMethod : getAttributeMethods(annotationType)) { String attributeName = attributeMethod.getName(); - Object attributeValue = attributes.get(attributeName); + + + // if attribute not present, check alias + if (attributeValue == null) { + String aliasName = attributeAliasMap.get(attributeName); + if (aliasName != null) { + Object aliasValue = attributes.get(aliasName); + if (aliasValue != null) { + attributeValue = aliasValue; + attributes.put(attributeName, attributeValue); + } + } + } + + // if alias not present, check default + if (attributeValue == null) { + Object defaultValue = getDefaultValue(annotationType, attributeName); + if (defaultValue != null) { + attributeValue = defaultValue; + attributes.put(attributeName, attributeValue); + } + } + + // if still null if (attributeValue == null) { throw new IllegalArgumentException(String.format( "Attributes map [%s] returned null for required attribute [%s] defined by annotation type [%s].", attributes, attributeName, annotationType.getName())); } + // else, ensure correct type Class returnType = attributeMethod.getReturnType(); if (!ClassUtils.isAssignable(returnType, attributeValue.getClass())) { throw new IllegalArgumentException(String.format( @@ -95,6 +128,8 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib attributeValue.getClass().getName(), attributeName, returnType.getName(), annotationType.getName())); } } + + return attributes; } } 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 b35f57d8ee8..94c1fb9d83b 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 @@ -24,6 +24,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -55,6 +56,8 @@ import static org.springframework.core.annotation.AnnotationUtils.*; */ public class AnnotationUtilsTests { + private static final Map EMPTY_ATTRS = Collections.emptyMap(); + @Rule public final ExpectedException exception = ExpectedException.none(); @@ -688,25 +691,52 @@ public class AnnotationUtilsTests { assertEquals("value from synthesized component: ", "webController", synthesizedComponent.value()); } + @Test + public void synthesizeAnnotationFromMapWithEmptyAttributesWithDefaultsWithoutAttributeAliases() throws Exception { + AnnotationWithDefaults annotationWithDefaults = synthesizeAnnotation(EMPTY_ATTRS, AnnotationWithDefaults.class, null); + assertNotNull(annotationWithDefaults); + assertEquals("text: ", "enigma", annotationWithDefaults.text()); + assertTrue("predicate: ", annotationWithDefaults.predicate()); + assertArrayEquals("characters: ", new char[] { 'a', 'b', 'c' }, annotationWithDefaults.characters()); + } + + @Test + public void synthesizeAnnotationFromMapWithEmptyAttributesWithDefaultsWithAttributeAliases() throws Exception { + ContextConfig contextConfig = synthesizeAnnotation(EMPTY_ATTRS, ContextConfig.class, null); + assertNotNull(contextConfig); + assertEquals("value: ", "", contextConfig.value()); + assertEquals("locations: ", "", contextConfig.locations()); + } + + @Test + public void synthesizeAnnotationFromMapWithMinimalAttributesWithAttributeAliases() throws Exception { + Map map = new HashMap(); + map.put("locations", "test.xml"); + ContextConfig contextConfig = synthesizeAnnotation(map, ContextConfig.class, null); + assertNotNull(contextConfig); + assertEquals("value: ", "test.xml", contextConfig.value()); + assertEquals("locations: ", "test.xml", contextConfig.locations()); + } + @Test public void synthesizeAnnotationFromMapWithMissingAttributeValue() throws Exception { - exception.expect(IllegalArgumentException.class); - exception.expectMessage(startsWith("Attributes map")); - exception.expectMessage(containsString("returned null for required attribute [value]")); - exception.expectMessage(containsString("defined by annotation type [" + Component.class.getName() + "]")); - synthesizeAnnotation(new HashMap(), Component.class, null); + assertMissingTextAttribute(EMPTY_ATTRS); } @Test public void synthesizeAnnotationFromMapWithNullAttributeValue() throws Exception { Map map = new HashMap(); - map.put(VALUE, null); + map.put("text", null); + assertTrue(map.containsKey("text")); + assertMissingTextAttribute(map); + } + private void assertMissingTextAttribute(Map attributes) { exception.expect(IllegalArgumentException.class); exception.expectMessage(startsWith("Attributes map")); - exception.expectMessage(containsString("returned null for required attribute [value]")); - exception.expectMessage(containsString("defined by annotation type [" + Component.class.getName() + "]")); - synthesizeAnnotation(map, Component.class, null); + exception.expectMessage(containsString("returned null for required attribute [text]")); + exception.expectMessage(containsString("defined by annotation type [" + AnnotationWithoutDefaults.class.getName() + "]")); + synthesizeAnnotation(attributes, AnnotationWithoutDefaults.class, null); } @Test @@ -945,14 +975,14 @@ public class AnnotationUtilsTests { } - @Component(value = "meta1") + @Component("meta1") @Order @Retention(RetentionPolicy.RUNTIME) @Inherited @interface Meta1 { } - @Component(value = "meta2") + @Component("meta2") @Transactional(readOnly = true) @Retention(RetentionPolicy.RUNTIME) @interface Meta2 { @@ -1427,4 +1457,16 @@ public class AnnotationUtilsTests { static class ComponentScanClass { } + @Retention(RetentionPolicy.RUNTIME) + @interface AnnotationWithDefaults { + String text() default "enigma"; + boolean predicate() default true; + char[] characters() default {'a', 'b', 'c'}; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AnnotationWithoutDefaults { + String text(); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java index 60d81664d42..1e6c5eb8533 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -30,6 +30,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; @@ -133,11 +134,8 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis private static final Log logger = LogFactory.getLog(TransactionalTestExecutionListener.class); - private static final String DEFAULT_TRANSACTION_MANAGER_NAME = (String) getDefaultValue( - TransactionConfiguration.class, "transactionManager"); - - private static final Boolean DEFAULT_DEFAULT_ROLLBACK = (Boolean) getDefaultValue(TransactionConfiguration.class, - "defaultRollback"); + private static final TransactionConfiguration defaultTransactionConfiguration = + AnnotationUtils.synthesizeAnnotation(Collections. emptyMap(), TransactionConfiguration.class, null); protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(); @@ -506,21 +504,14 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis txConfig, clazz)); } - String transactionManagerName; - boolean defaultRollback; - if (txConfig != null) { - transactionManagerName = txConfig.transactionManager(); - defaultRollback = txConfig.defaultRollback(); - } - else { - transactionManagerName = DEFAULT_TRANSACTION_MANAGER_NAME; - defaultRollback = DEFAULT_DEFAULT_ROLLBACK; + if (txConfig == null) { + txConfig = defaultTransactionConfiguration; } TransactionConfigurationAttributes configAttributes = new TransactionConfigurationAttributes( - transactionManagerName, defaultRollback); + txConfig.transactionManager(), txConfig.defaultRollback()); if (logger.isDebugEnabled()) { - logger.debug(String.format("Retrieved TransactionConfigurationAttributes %s for class [%s].", + logger.debug(String.format("Using TransactionConfigurationAttributes %s for class [%s].", configAttributes, clazz)); } this.configurationAttributes = configAttributes;