Synthesize annotation from map w/ minimal attributes
The initial support for synthesizing an annotation from a Map (or AnnotationAttributes) introduced in SPR-13067 required that the map contain key-value pairs for every attribute defined by the supplied annotationType. However, there are use cases that would benefit from being able to supply a reduced set of attributes and still have the annotation synthesized properly. This commit refines the validation mechanism in MapAnnotationAttributeExtractor so that a reduced set of attributes may be supplied. Specifically, if an attribute is missing in the supplied map the attribute will be set either to value of its alias (if an alias value configured via @AliasFor exists) or to the value of the attribute's default value (if defined), and otherwise an exception will be thrown. Furthermore, TransactionalTestExecutionListener has been refactored to take advantage of this new feature by synthesizing an instance of @TransactionConfiguration solely from the default values of its declared attributes. Issue: SPR-13087
This commit is contained in:
parent
1e39a18819
commit
ece12f9d37
|
|
@ -1088,8 +1088,9 @@ public abstract class AnnotationUtils {
|
|||
* annotation of the specified {@code annotationType} and transparently
|
||||
* enforces <em>attribute alias</em> semantics for annotation attributes
|
||||
* that are annotated with {@link AliasFor @AliasFor}.
|
||||
* <p>The supplied map must contain key-value pairs for every attribute
|
||||
* defined by the supplied {@code annotationType}.
|
||||
* <p>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.
|
||||
* <p>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)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib
|
|||
|
||||
/**
|
||||
* Construct a new {@code MapAnnotationAttributeExtractor}.
|
||||
* <p>The supplied map must contain key-value pairs for every attribute
|
||||
* defined in the supplied {@code annotationType}.
|
||||
* <p>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<String, Object> attributes, Class<? extends Annotation> annotationType,
|
||||
AnnotatedElement annotatedElement) {
|
||||
super(annotationType, annotatedElement, new HashMap<String, Object>(attributes));
|
||||
validateAttributes(attributes, annotationType);
|
||||
super(annotationType, annotatedElement, enrichAndValidateAttributes(new HashMap<String, Object>(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.
|
||||
* <p>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<String, Object> attributes, Class<? extends Annotation> annotationType) {
|
||||
private static Map<String, Object> enrichAndValidateAttributes(Map<String, Object> attributes,
|
||||
Class<? extends Annotation> annotationType) {
|
||||
|
||||
Map<String, String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, Object> 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<String, Object> map = new HashMap<String, Object>();
|
||||
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<String, Object>(), Component.class, null);
|
||||
assertMissingTextAttribute(EMPTY_ATTRS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void synthesizeAnnotationFromMapWithNullAttributeValue() throws Exception {
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put(VALUE, null);
|
||||
map.put("text", null);
|
||||
assertTrue(map.containsKey("text"));
|
||||
assertMissingTextAttribute(map);
|
||||
}
|
||||
|
||||
private void assertMissingTextAttribute(Map<String, Object> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.<String, Object> 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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue