提交 9683da52 编写于 作者: J Juergen Hoeller

Avoid annotation synthesizing for getAnnotationAttributes retrieval

Issue: SPR-13621
上级 971f0469
......@@ -977,7 +977,7 @@ public class AnnotatedElementUtils {
* annotation attributes from lower levels in the annotation hierarchy
* during the {@link #postProcess} phase.
* @since 4.2
* @see AnnotationUtils#getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean, boolean)
* @see AnnotationUtils#retrieveAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean)
* @see AnnotationUtils#postProcessAnnotationAttributes
*/
private static class MergedAnnotationAttributesProcessor implements Processor<AnnotationAttributes> {
......@@ -1003,8 +1003,8 @@ public class AnnotatedElementUtils {
public AnnotationAttributes process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) {
boolean found = (this.annotationType != null ? annotation.annotationType() == this.annotationType :
annotation.annotationType().getName().equals(this.annotationName));
return (found ? AnnotationUtils.getAnnotationAttributes(annotatedElement, annotation,
this.classValuesAsString, this.nestedAnnotationsAsMap, true) : null);
return (found ? AnnotationUtils.retrieveAnnotationAttributes(annotatedElement, annotation,
this.classValuesAsString, this.nestedAnnotationsAsMap) : null);
}
@Override
......
......@@ -25,8 +25,8 @@ import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
......@@ -112,12 +112,6 @@ public abstract class AnnotationUtils {
public static final String VALUE = "value";
/**
* An object that can be stored in {@link AnnotationAttributes} as a
* placeholder for an attribute's declared default value.
*/
private static final Object DEFAULT_VALUE_PLACEHOLDER = new String("<SPRING DEFAULT VALUE PLACEHOLDER>");
private static final Map<AnnotationCacheKey, Annotation> findAnnotationCache =
new ConcurrentReferenceHashMap<AnnotationCacheKey, Annotation>(256);
......@@ -1002,7 +996,10 @@ public abstract class AnnotationUtils {
public static AnnotationAttributes getAnnotationAttributes(AnnotatedElement annotatedElement,
Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
return getAnnotationAttributes(annotatedElement, annotation, classValuesAsString, nestedAnnotationsAsMap, false);
AnnotationAttributes attributes =
retrieveAnnotationAttributes(annotatedElement, annotation, classValuesAsString, nestedAnnotationsAsMap);
postProcessAnnotationAttributes(annotatedElement, attributes, classValuesAsString, nestedAnnotationsAsMap);
return attributes;
}
/**
......@@ -1010,16 +1007,9 @@ public abstract class AnnotationUtils {
* <p>This method provides fully recursive annotation reading capabilities on par with
* the reflection-based {@link org.springframework.core.type.StandardAnnotationMetadata}.
* <p><strong>NOTE</strong>: This variant of {@code getAnnotationAttributes()} is
* only intended for use within the framework. Specifically, the {@code mergeMode} flag
* can be set to {@code true} in order to support processing of attribute aliases while
* merging attributes within an annotation hierarchy. When running in <em>merge mode</em>,
* the following special rules apply:
* only intended for use within the framework. The following special rules apply:
* <ol>
* <li>The supplied annotation will <em>not</em> be
* {@linkplain #synthesizeAnnotation synthesized} before retrieving its attributes;
* however, nested annotations and arrays of nested annotations <em>will</em> be
* synthesized.</li>
* <li>Default values will be replaced with {@link #DEFAULT_VALUE_PLACEHOLDER}.</li>
* <li>Default values will be replaced with default value placeholders.</li>
* <li>The resulting, merged annotation attributes should eventually be
* {@linkplain #postProcessAnnotationAttributes post-processed} in order to
* ensure that placeholders have been replaced by actual default values and
......@@ -1035,33 +1025,26 @@ public abstract class AnnotationUtils {
* {@link AnnotationAttributes} maps (for compatibility with
* {@link org.springframework.core.type.AnnotationMetadata}) or to preserve them as
* {@code Annotation} instances
* @param mergeMode whether the annotation attributes should be created
* using <em>merge mode</em>
* @return the annotation attributes (a specialized Map) with attribute names as keys
* and corresponding attribute values as values (never {@code null})
* @since 4.2
* @see #postProcessAnnotationAttributes
*/
static AnnotationAttributes getAnnotationAttributes(AnnotatedElement annotatedElement, Annotation annotation,
boolean classValuesAsString, boolean nestedAnnotationsAsMap, boolean mergeMode) {
if (!mergeMode) {
annotation = synthesizeAnnotation(annotation, annotatedElement);
}
static AnnotationAttributes retrieveAnnotationAttributes(AnnotatedElement annotatedElement, Annotation annotation,
boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
Class<? extends Annotation> annotationType = annotation.annotationType();
AnnotationAttributes attrs = new AnnotationAttributes(annotationType);
AnnotationAttributes attributes = new AnnotationAttributes(annotationType);
for (Method method : getAttributeMethods(annotationType)) {
try {
Object value = method.invoke(annotation);
Object attributeValue = method.invoke(annotation);
Object defaultValue = method.getDefaultValue();
if (mergeMode && defaultValue != null) {
if (ObjectUtils.nullSafeEquals(value, defaultValue)) {
value = DEFAULT_VALUE_PLACEHOLDER;
}
if (defaultValue != null && ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
attributeValue = new DefaultValueHolder(defaultValue);
}
attrs.put(method.getName(),
adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap));
attributes.put(method.getName(),
adaptValue(annotatedElement, attributeValue, classValuesAsString, nestedAnnotationsAsMap));
}
catch (Exception ex) {
if (ex instanceof InvocationTargetException) {
......@@ -1071,7 +1054,8 @@ public abstract class AnnotationUtils {
throw new IllegalStateException("Could not obtain annotation attribute value for " + method, ex);
}
}
return attrs;
return attributes;
}
/**
......@@ -1109,7 +1093,6 @@ public abstract class AnnotationUtils {
if (value instanceof Annotation) {
Annotation annotation = (Annotation) value;
if (nestedAnnotationsAsMap) {
return getAnnotationAttributes(annotatedElement, annotation, classValuesAsString, true);
}
......@@ -1120,12 +1103,11 @@ public abstract class AnnotationUtils {
if (value instanceof Annotation[]) {
Annotation[] annotations = (Annotation[]) value;
if (nestedAnnotationsAsMap) {
AnnotationAttributes[] mappedAnnotations = new AnnotationAttributes[annotations.length];
for (int i = 0; i < annotations.length; i++) {
mappedAnnotations[i] = getAnnotationAttributes(annotatedElement, annotations[i],
classValuesAsString, true);
mappedAnnotations[i] =
getAnnotationAttributes(annotatedElement, annotations[i], classValuesAsString, true);
}
return mappedAnnotations;
}
......@@ -1138,6 +1120,101 @@ public abstract class AnnotationUtils {
return value;
}
/**
* Post-process the supplied {@link AnnotationAttributes}.
* <p>Specifically, this method enforces <em>attribute alias</em> semantics
* for annotation attributes that are annotated with {@link AliasFor @AliasFor}
* and replaces default value placeholders with their original default values.
* @param annotatedElement the element that is annotated with an annotation or
* annotation hierarchy from which the supplied attributes were created;
* may be {@code null} if unknown
* @param attributes the annotation attributes to post-process
* @param classValuesAsString whether to convert Class references into Strings (for
* compatibility with {@link org.springframework.core.type.AnnotationMetadata})
* or to preserve them as Class references
* @param nestedAnnotationsAsMap whether to convert nested annotations into
* {@link AnnotationAttributes} maps (for compatibility with
* {@link org.springframework.core.type.AnnotationMetadata}) or to preserve them as
* {@code Annotation} instances
* @since 4.2
* @see #retrieveAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean)
* @see #getDefaultValue(Class, String)
*/
static void postProcessAnnotationAttributes(AnnotatedElement annotatedElement,
AnnotationAttributes attributes, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
// Abort?
if (attributes == null) {
return;
}
Class<? extends Annotation> annotationType = attributes.annotationType();
// Track which attribute values have already been replaced so that we can short
// circuit the search algorithms.
Set<String> valuesAlreadyReplaced = new HashSet<String>();
// Validate @AliasFor configuration
Map<String, List<String>> aliasMap = getAttributeAliasMap(annotationType);
for (String attributeName : aliasMap.keySet()) {
if (valuesAlreadyReplaced.contains(attributeName)) {
continue;
}
Object value = attributes.get(attributeName);
boolean valuePresent = (value != null && !(value instanceof DefaultValueHolder));
for (String aliasedAttributeName : aliasMap.get(attributeName)) {
if (valuesAlreadyReplaced.contains(aliasedAttributeName)) {
continue;
}
Object aliasedValue = attributes.get(aliasedAttributeName);
boolean aliasPresent = (aliasedValue != null && !(aliasedValue instanceof DefaultValueHolder));
// Something to validate or replace with an alias?
if (valuePresent || aliasPresent) {
if (valuePresent && aliasPresent) {
// Since annotation attributes can be arrays, we must use ObjectUtils.nullSafeEquals().
if (!ObjectUtils.nullSafeEquals(value, aliasedValue)) {
String elementAsString = (annotatedElement != null ? annotatedElement.toString() : "unknown element");
throw new AnnotationConfigurationException(String.format(
"In AnnotationAttributes for annotation [%s] declared on %s, " +
"attribute '%s' and its alias '%s' are declared with values of [%s] and [%s], " +
"but only one is permitted.", annotationType.getName(), elementAsString,
attributeName, aliasedAttributeName, ObjectUtils.nullSafeToString(value),
ObjectUtils.nullSafeToString(aliasedValue)));
}
}
else if (aliasPresent) {
// Replace value with aliasedValue
attributes.put(attributeName,
adaptValue(annotatedElement, aliasedValue, classValuesAsString, nestedAnnotationsAsMap));
valuesAlreadyReplaced.add(attributeName);
}
else {
// Replace aliasedValue with value
attributes.put(aliasedAttributeName,
adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap));
valuesAlreadyReplaced.add(aliasedAttributeName);
}
}
}
}
// Replace any remaining placeholders with actual default values
for (String attributeName : attributes.keySet()) {
if (valuesAlreadyReplaced.contains(attributeName)) {
continue;
}
Object value = attributes.get(attributeName);
if (value instanceof DefaultValueHolder) {
value = ((DefaultValueHolder) value).defaultValue;
attributes.put(attributeName,
adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap));
}
}
}
/**
* Retrieve the <em>value</em> of the {@code value} attribute of a
* single-element Annotation, given an annotation instance.
......@@ -1436,7 +1513,7 @@ public abstract class AnnotationUtils {
return map;
}
map = new HashMap<String, List<String>>();
map = new LinkedHashMap<String, List<String>>();
for (Method attribute : getAttributeMethods(annotationType)) {
List<String> aliasNames = getAttributeAliasNames(attribute);
if (!aliasNames.isEmpty()) {
......@@ -1609,103 +1686,6 @@ public abstract class AnnotationUtils {
return (method != null && method.getName().equals("annotationType") && method.getParameterTypes().length == 0);
}
/**
* Post-process the supplied {@link AnnotationAttributes}.
* <p>Specifically, this method enforces <em>attribute alias</em> semantics
* for annotation attributes that are annotated with {@link AliasFor @AliasFor}
* and replaces {@linkplain #DEFAULT_VALUE_PLACEHOLDER placeholders} with their
* original default values.
* @param element the element that is annotated with an annotation or
* annotation hierarchy from which the supplied attributes were created;
* may be {@code null} if unknown
* @param attributes the annotation attributes to post-process
* @param classValuesAsString whether to convert Class references into Strings (for
* compatibility with {@link org.springframework.core.type.AnnotationMetadata})
* or to preserve them as Class references
* @param nestedAnnotationsAsMap whether to convert nested annotations into
* {@link AnnotationAttributes} maps (for compatibility with
* {@link org.springframework.core.type.AnnotationMetadata}) or to preserve them as
* {@code Annotation} instances
* @since 4.2
* @see #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean, boolean)
* @see #DEFAULT_VALUE_PLACEHOLDER
* @see #getDefaultValue(Class, String)
*/
static void postProcessAnnotationAttributes(AnnotatedElement element, AnnotationAttributes attributes,
boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
// Abort?
if (attributes == null) {
return;
}
Class<? extends Annotation> annotationType = attributes.annotationType();
// Track which attribute values have already been replaced so that we can short
// circuit the search algorithms.
Set<String> valuesAlreadyReplaced = new HashSet<String>();
// Validate @AliasFor configuration
Map<String, List<String>> aliasMap = getAttributeAliasMap(annotationType);
for (String attributeName : aliasMap.keySet()) {
if (valuesAlreadyReplaced.contains(attributeName)) {
continue;
}
Object value = attributes.get(attributeName);
boolean valuePresent = (value != null && value != DEFAULT_VALUE_PLACEHOLDER);
for (String aliasedAttributeName : aliasMap.get(attributeName)) {
if (valuesAlreadyReplaced.contains(aliasedAttributeName)) {
continue;
}
Object aliasedValue = attributes.get(aliasedAttributeName);
boolean aliasPresent = (aliasedValue != null && aliasedValue != DEFAULT_VALUE_PLACEHOLDER);
// Something to validate or replace with an alias?
if (valuePresent || aliasPresent) {
if (valuePresent && aliasPresent) {
// Since annotation attributes can be arrays, we must use ObjectUtils.nullSafeEquals().
if (!ObjectUtils.nullSafeEquals(value, aliasedValue)) {
String elementAsString = (element != null ? element.toString() : "unknown element");
String msg = String.format("In AnnotationAttributes for annotation [%s] declared on [%s], " +
"attribute [%s] and its alias [%s] are declared with values of [%s] and [%s], " +
"but only one declaration is permitted.", annotationType.getName(),
elementAsString, attributeName, aliasedAttributeName,
ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(aliasedValue));
throw new AnnotationConfigurationException(msg);
}
}
else if (aliasPresent) {
// Replace value with aliasedValue
attributes.put(attributeName,
adaptValue(element, aliasedValue, classValuesAsString, nestedAnnotationsAsMap));
valuesAlreadyReplaced.add(attributeName);
}
else {
// Replace aliasedValue with value
attributes.put(aliasedAttributeName,
adaptValue(element, value, classValuesAsString, nestedAnnotationsAsMap));
valuesAlreadyReplaced.add(aliasedAttributeName);
}
}
}
}
// Replace any remaining placeholders with actual default values
for (String attributeName : attributes.keySet()) {
if (valuesAlreadyReplaced.contains(attributeName)) {
continue;
}
Object value = attributes.get(attributeName);
if (value == DEFAULT_VALUE_PLACEHOLDER) {
attributes.put(attributeName,
adaptValue(element, getDefaultValue(annotationType, attributeName), classValuesAsString,
nestedAnnotationsAsMap));
}
}
}
/**
* <p>If the supplied throwable is an {@link AnnotationConfigurationException},
* it will be cast to an {@code AnnotationConfigurationException} and thrown,
......@@ -2163,4 +2143,14 @@ public abstract class AnnotationUtils {
}
}
private static class DefaultValueHolder {
final Object defaultValue;
public DefaultValueHolder(Object defaultValue) {
this.defaultValue = defaultValue;
}
}
}
......@@ -94,8 +94,7 @@ public class AnnotatedElementUtilsTests {
public void hasMetaAnnotationTypesOnClassWithMetaDepth2() {
assertTrue(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, TX_NAME));
assertTrue(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, Component.class.getName()));
assertFalse(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class,
ComposedTransactionalComponent.class.getName()));
assertFalse(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class.getName()));
}
@Test
......@@ -111,7 +110,7 @@ public class AnnotatedElementUtilsTests {
@Test
public void isAnnotatedOnSubclassWithMetaDepth0() {
assertFalse("isAnnotated() does not search the class hierarchy.",
isAnnotated(SubTransactionalComponentClass.class, TransactionalComponent.class.getName()));
isAnnotated(SubTransactionalComponentClass.class, TransactionalComponent.class.getName()));
}
@Test
......@@ -124,8 +123,7 @@ public class AnnotatedElementUtilsTests {
public void isAnnotatedOnClassWithMetaDepth2() {
assertTrue(isAnnotated(ComposedTransactionalComponentClass.class, TX_NAME));
assertTrue(isAnnotated(ComposedTransactionalComponentClass.class, Component.class.getName()));
assertTrue(isAnnotated(ComposedTransactionalComponentClass.class,
ComposedTransactionalComponent.class.getName()));
assertTrue(isAnnotated(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class.getName()));
}
@Test
......@@ -167,7 +165,6 @@ public class AnnotatedElementUtilsTests {
* type within the class hierarchy. Such undesirable behavior would cause the
* logic in {@link org.springframework.context.annotation.ProfileCondition}
* to fail.
*
* @see org.springframework.core.env.EnvironmentSystemIntegrationTests#mostSpecificDerivedClassDrivesEnvironment_withDevEnvAndDerivedDevConfigClass
*/
@Test
......@@ -179,7 +176,6 @@ public class AnnotatedElementUtilsTests {
/**
* Note: this functionality is required by {@link org.springframework.context.annotation.ProfileCondition}.
*
* @see org.springframework.core.env.EnvironmentSystemIntegrationTests
*/
@Test
......@@ -187,7 +183,7 @@ public class AnnotatedElementUtilsTests {
MultiValueMap<String, Object> attributes = getAllAnnotationAttributes(TxFromMultipleComposedAnnotations.class, TX_NAME);
assertNotNull("Annotation attributes map for @Transactional on TxFromMultipleComposedAnnotations", attributes);
assertEquals("value for TxFromMultipleComposedAnnotations.", asList("TxInheritedComposed", "TxComposed"),
attributes.get("value"));
attributes.get("value"));
}
@Test
......@@ -419,12 +415,12 @@ public class AnnotatedElementUtilsTests {
public void getMergedAnnotationAttributesWithInvalidConventionBasedComposedAnnotation() {
Class<?> element = InvalidConventionBasedComposedContextConfigClass.class;
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(either(containsString("attribute [value] and its alias [locations]")).or(
containsString("attribute [locations] and its alias [value]")));
exception.expectMessage(either(containsString("attribute 'value' and its alias 'locations'")).or(
containsString("attribute 'locations' and its alias 'value'")));
exception.expectMessage(either(
containsString("values of [{duplicateDeclaration}] and [{requiredLocationsDeclaration}]")).or(
containsString("values of [{requiredLocationsDeclaration}] and [{duplicateDeclaration}]")));
exception.expectMessage(containsString("but only one declaration is permitted"));
containsString("values of [{duplicateDeclaration}] and [{requiredLocationsDeclaration}]")).or(
containsString("values of [{requiredLocationsDeclaration}] and [{duplicateDeclaration}]")));
exception.expectMessage(containsString("but only one is permitted"));
getMergedAnnotationAttributes(element, ContextConfig.class);
}
......@@ -432,11 +428,11 @@ public class AnnotatedElementUtilsTests {
public void getMergedAnnotationAttributesWithInvalidAliasedComposedAnnotation() {
Class<?> element = InvalidAliasedComposedContextConfigClass.class;
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(either(containsString("attribute [value] and its alias [locations]")).or(
containsString("attribute [locations] and its alias [value]")));
exception.expectMessage(either(containsString("attribute 'value' and its alias 'locations'")).or(
containsString("attribute 'locations' and its alias 'value'")));
exception.expectMessage(either(containsString("values of [{duplicateDeclaration}] and [{test.xml}]")).or(
containsString("values of [{test.xml}] and [{duplicateDeclaration}]")));
exception.expectMessage(containsString("but only one declaration is permitted"));
containsString("values of [{test.xml}] and [{duplicateDeclaration}]")));
exception.expectMessage(containsString("but only one is permitted"));
getMergedAnnotationAttributes(element, ContextConfig.class);
}
......@@ -493,11 +489,9 @@ public class AnnotatedElementUtilsTests {
/**
* <p>{@code AbstractClassWithInheritedAnnotation} declares {@code handleParameterized(T)}; whereas,
* {@code ConcreteClassWithInheritedAnnotation} declares {@code handleParameterized(String)}.
*
* <p>As of Spring 4.2 RC1, {@code AnnotatedElementUtils.processWithFindSemantics()} does not resolve an
* <p>As of Spring 4.2, {@code AnnotatedElementUtils.processWithFindSemantics()} does not resolve an
* <em>equivalent</em> method in {@code AbstractClassWithInheritedAnnotation} for the <em>bridged</em>
* {@code handleParameterized(String)} method.
*
* @since 4.2
*/
@Test
......@@ -517,6 +511,7 @@ public class AnnotatedElementUtilsTests {
Method[] methods = StringGenericParameter.class.getMethods();
Method bridgeMethod = null;
Method bridgedMethod = null;
for (Method method : methods) {
if ("getFor".equals(method.getName()) && !method.getParameterTypes()[0].equals(Integer.class)) {
if (method.getReturnType().equals(Object.class)) {
......@@ -546,13 +541,13 @@ public class AnnotatedElementUtilsTests {
String qualifier = "aliasForQualifier";
// 1) Find and merge AnnotationAttributes from the annotation hierarchy
AnnotationAttributes attributes = findMergedAnnotationAttributes(AliasedTransactionalComponentClass.class,
AliasedTransactional.class);
AnnotationAttributes attributes = findMergedAnnotationAttributes(
AliasedTransactionalComponentClass.class, AliasedTransactional.class);
assertNotNull("@AliasedTransactional on AliasedTransactionalComponentClass.", attributes);
// 2) Synthesize the AnnotationAttributes back into the target annotation
AliasedTransactional annotation = AnnotationUtils.synthesizeAnnotation(attributes,
AliasedTransactional.class, AliasedTransactionalComponentClass.class);
AliasedTransactional.class, AliasedTransactionalComponentClass.class);
assertNotNull(annotation);
// 3) Verify that the AnnotationAttributes and synthesized annotation are equivalent
......@@ -573,8 +568,8 @@ public class AnnotatedElementUtilsTests {
@Test
public void findMergedAnnotationForMultipleMetaAnnotationsWithClashingAttributeNames() {
final String[] xmlLocations = asArray("test.xml");
final String[] propFiles = asArray("test.properties");
String[] xmlLocations = asArray("test.xml");
String[] propFiles = asArray("test.properties");
Class<?> element = AliasedComposedContextConfigAndTestPropSourceClass.class;
......@@ -600,6 +595,7 @@ public class AnnotatedElementUtilsTests {
String[] expected = asArray("com.example.app.test");
Class<?> element = TestComponentScanClass.class;
AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class);
assertNotNull("Should find @ComponentScan on " + element, attributes);
assertArrayEquals("basePackages for " + element, expected, attributes.getStringArray("basePackages"));
......@@ -628,9 +624,10 @@ public class AnnotatedElementUtilsTests {
@Test
public void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() {
final String[] EMPTY = new String[] {};
final String[] EMPTY = new String[0];
Class<?> element = SpringAppConfigClass.class;
ContextConfig contextConfig = findMergedAnnotation(element, ContextConfig.class);
assertNotNull("Should find @ContextConfig on " + element, contextConfig);
assertArrayEquals("locations for " + element, EMPTY, contextConfig.locations());
// 'value' in @SpringAppConfig should not override 'value' in @ContextConfig
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册