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 extends Annotation> 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 extends Annotation> annotationType) {
+ private static Map enrichAndValidateAttributes(Map attributes,
+ Class extends Annotation> 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;