Skip to content

Commit 64f593d

Browse files
committed
Support meta-annotation attr overrides in the TCF
Prior to this commit, the Spring TestContext Framework (TCF) supported the use of test-related annotations as meta-annotations for composing custom test stereotype annotations; however, attributes in custom stereotypes could not be used to override meta-annotation attributes. This commit addresses this by allowing attributes from the following annotations (when used as meta-annotations) to be overridden in custom stereotypes. - @ContextConfiguration - @activeprofiles - @DirtiesContext - @TransactionConfiguration - @timed - @TestExecutionListeners This support depends on functionality provided by AnnotatedElementUtils. See the 'Notes' below for further details and ramifications. Notes: - AnnotatedElementUtils does not support overrides for the 'value' attribute of an annotation. It is therefore not possible or not feasible to support meta-annotation attribute overrides for some test-related annotations. - @ContextHierarchy, @WebAppConfiguration, @Rollback, @repeat, and @ProfileValueSourceConfiguration define single 'value' attributes which cannot be overridden via Spring's meta-annotation attribute support. - Although @IfProfileValue has 'values' and 'name' attributes, the typical usage scenario involves the 'value' attribute which is not supported for meta-annotation attribute overrides. Furthermore, 'name' and 'values' are so generic that it is deemed unfeasible to provide meta-annotation attribute override support for these. - @BeforeTransaction and @AfterTransaction do not define any attributes that can be overridden. - Support for meta-annotation attribute overrides for @transactional is provided indirectly via SpringTransactionAnnotationParser. Implementation Details: - MetaAnnotationUtils.AnnotationDescriptor now provides access to the AnnotationAttributes for the described annotation. - MetaAnnotationUtils.AnnotationDescriptor now provides access to the root declaring class as well as the declaring class. - ContextLoaderUtils now retrieves AnnotationAttributes from AnnotationDescriptor to look up annotation attributes for @ContextConfiguration and @activeprofiles. - ContextConfigurationAttributes now provides a constructor to have its attributes sourced from an instance of AnnotationAttributes. - ContextLoaderUtils.resolveContextHierarchyAttributes() now throws an IllegalStateException if no class in the class hierarchy declares @ContextHierarchy. - TransactionalTestExecutionListener now uses AnnotatedElementUtils to look up annotation attributes for @TransactionConfiguration. - Implemented missing unit tests for @Rollback resolution in TransactionalTestExecutionListener. - SpringJUnit4ClassRunner now uses AnnotatedElementUtils to look up annotation attributes for @timed. - TestContextManager now retrieves AnnotationAttributes from AnnotationDescriptor to look up annotation attributes for @TestExecutionListeners. - DirtiesContextTestExecutionListener now uses AnnotatedElementUtils to look up annotation attributes for @DirtiesContext. Issue: SPR-11038
1 parent c5779e2 commit 64f593d

21 files changed

+1097
-162
lines changed

spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020

2121
import org.apache.commons.logging.Log;
2222
import org.apache.commons.logging.LogFactory;
23-
2423
import org.springframework.context.ApplicationContextInitializer;
2524
import org.springframework.context.ConfigurableApplicationContext;
25+
import org.springframework.core.annotation.AnnotationAttributes;
2626
import org.springframework.core.style.ToStringCreator;
2727
import org.springframework.util.Assert;
2828
import org.springframework.util.ObjectUtils;
@@ -68,21 +68,29 @@ public class ContextConfigurationAttributes {
6868
* @throws IllegalStateException if both the locations and value attributes have been declared
6969
*/
7070
private static String[] resolveLocations(Class<?> declaringClass, ContextConfiguration contextConfiguration) {
71-
Assert.notNull(declaringClass, "declaringClass must not be null");
71+
return resolveLocations(declaringClass, contextConfiguration.locations(), contextConfiguration.value());
72+
}
7273

73-
String[] locations = contextConfiguration.locations();
74-
String[] valueLocations = contextConfiguration.value();
74+
/**
75+
* Resolve resource locations from the supplied {@code locations} and
76+
* {@code value} arrays, which correspond to attributes of the same names in
77+
* the {@link ContextConfiguration} annotation.
78+
*
79+
* @throws IllegalStateException if both the locations and value attributes have been declared
80+
*/
81+
private static String[] resolveLocations(Class<?> declaringClass, String[] locations, String[] value) {
82+
Assert.notNull(declaringClass, "declaringClass must not be null");
7583

76-
if (!ObjectUtils.isEmpty(valueLocations) && !ObjectUtils.isEmpty(locations)) {
84+
if (!ObjectUtils.isEmpty(value) && !ObjectUtils.isEmpty(locations)) {
7785
String msg = String.format("Test class [%s] has been configured with @ContextConfiguration's 'value' %s "
7886
+ "and 'locations' %s attributes. Only one declaration of resource "
7987
+ "locations is permitted per @ContextConfiguration annotation.", declaringClass.getName(),
80-
ObjectUtils.nullSafeToString(valueLocations), ObjectUtils.nullSafeToString(locations));
88+
ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(locations));
8189
logger.error(msg);
8290
throw new IllegalStateException(msg);
8391
}
84-
else if (!ObjectUtils.isEmpty(valueLocations)) {
85-
locations = valueLocations;
92+
else if (!ObjectUtils.isEmpty(value)) {
93+
locations = value;
8694
}
8795

8896
return locations;
@@ -101,6 +109,25 @@ public ContextConfigurationAttributes(Class<?> declaringClass, ContextConfigurat
101109
contextConfiguration.inheritInitializers(), contextConfiguration.name(), contextConfiguration.loader());
102110
}
103111

112+
/**
113+
* Construct a new {@link ContextConfigurationAttributes} instance for the
114+
* supplied {@link ContextConfiguration @ContextConfiguration} annotation and
115+
* the {@linkplain Class test class} that declared it.
116+
* @param declaringClass the test class that declared {@code @ContextConfiguration}
117+
* @param annAttrs the annotation attributes from which to retrieve the attributes
118+
*/
119+
@SuppressWarnings("unchecked")
120+
public ContextConfigurationAttributes(Class<?> declaringClass, AnnotationAttributes annAttrs) {
121+
this(
122+
declaringClass,
123+
resolveLocations(declaringClass, annAttrs.getStringArray("locations"), annAttrs.getStringArray("value")),
124+
annAttrs.getClassArray("classes"),
125+
annAttrs.getBoolean("inheritLocations"),
126+
(Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[]) annAttrs.getClassArray("initializers"),
127+
annAttrs.getBoolean("inheritInitializers"), annAttrs.getString("name"),
128+
(Class<? extends ContextLoader>) annAttrs.getClass("loader"));
129+
}
130+
104131
/**
105132
* Construct a new {@link ContextConfigurationAttributes} instance for the
106133
* {@linkplain Class test class} that declared the

spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.apache.commons.logging.LogFactory;
3232
import org.springframework.context.ApplicationContextInitializer;
3333
import org.springframework.context.ConfigurableApplicationContext;
34+
import org.springframework.core.annotation.AnnotationAttributes;
3435
import org.springframework.core.annotation.AnnotationUtils;
3536
import org.springframework.test.context.MetaAnnotationUtils.AnnotationDescriptor;
3637
import org.springframework.test.context.MetaAnnotationUtils.UntypedAnnotationDescriptor;
@@ -192,9 +193,9 @@ static Class<? extends ContextLoader> resolveContextLoaderClass(Class<?> testCla
192193
}
193194

194195
/**
195-
* Convenience method for creating a {@link ContextConfigurationAttributes} instance
196-
* from the supplied {@link ContextConfiguration} and declaring class and then adding
197-
* the attributes to the supplied list.
196+
* Convenience method for creating a {@link ContextConfigurationAttributes}
197+
* instance from the supplied {@link ContextConfiguration} annotation and
198+
* declaring class and then adding the attributes to the supplied list.
198199
*/
199200
private static void convertContextConfigToConfigAttributesAndAddToList(ContextConfiguration contextConfiguration,
200201
Class<?> declaringClass, final List<ContextConfigurationAttributes> attributesList) {
@@ -211,6 +212,27 @@ private static void convertContextConfigToConfigAttributesAndAddToList(ContextCo
211212
attributesList.add(attributes);
212213
}
213214

215+
/**
216+
* Convenience method for creating a {@link ContextConfigurationAttributes}
217+
* instance from the supplied {@link AnnotationAttributes} and declaring
218+
* class and then adding the attributes to the supplied list.
219+
*
220+
* @since 4.0
221+
*/
222+
private static void convertAnnotationAttributesToConfigAttributesAndAddToList(AnnotationAttributes annAttrs,
223+
Class<?> declaringClass, final List<ContextConfigurationAttributes> attributesList) {
224+
if (logger.isTraceEnabled()) {
225+
logger.trace(String.format("Retrieved @ContextConfiguration attributes [%s] for declaring class [%s].",
226+
annAttrs, declaringClass.getName()));
227+
}
228+
229+
ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass, annAttrs);
230+
if (logger.isTraceEnabled()) {
231+
logger.trace("Resolved context configuration attributes: " + attributes);
232+
}
233+
attributesList.add(attributes);
234+
}
235+
214236
/**
215237
* Resolve the list of lists of {@linkplain ContextConfigurationAttributes context
216238
* configuration attributes} for the supplied {@linkplain Class test class} and its
@@ -243,6 +265,8 @@ private static void convertContextConfigToConfigAttributesAndAddToList(ContextCo
243265
* <em>present</em> on the supplied class; or if a given class in the class hierarchy
244266
* declares both {@code @ContextConfiguration} and {@code @ContextHierarchy} as
245267
* top-level annotations.
268+
* @throws IllegalStateException if no class in the class hierarchy declares
269+
* {@code @ContextHierarchy}.
246270
*
247271
* @since 3.2.2
248272
* @see #buildContextHierarchyMap(Class)
@@ -251,6 +275,7 @@ private static void convertContextConfigToConfigAttributesAndAddToList(ContextCo
251275
@SuppressWarnings("unchecked")
252276
static List<List<ContextConfigurationAttributes>> resolveContextHierarchyAttributes(Class<?> testClass) {
253277
Assert.notNull(testClass, "Class must not be null");
278+
Assert.state(findAnnotation(testClass, ContextHierarchy.class) != null, "@ContextHierarchy must be present");
254279

255280
final Class<ContextConfiguration> contextConfigType = ContextConfiguration.class;
256281
final Class<ContextHierarchy> contextHierarchyType = ContextHierarchy.class;
@@ -263,27 +288,25 @@ static List<List<ContextConfigurationAttributes>> resolveContextHierarchyAttribu
263288
contextConfigType.getName(), contextHierarchyType.getName(), testClass.getName()));
264289

265290
while (descriptor != null) {
266-
Class<?> rootDeclaringClass = descriptor.getDeclaringClass();
267-
Class<?> declaringClass = (descriptor.getStereotype() != null) ? descriptor.getStereotypeType()
268-
: rootDeclaringClass;
291+
Class<?> rootDeclaringClass = descriptor.getRootDeclaringClass();
292+
Class<?> declaringClass = descriptor.getDeclaringClass();
269293

270294
boolean contextConfigDeclaredLocally = isAnnotationDeclaredLocally(contextConfigType, declaringClass);
271295
boolean contextHierarchyDeclaredLocally = isAnnotationDeclaredLocally(contextHierarchyType, declaringClass);
272296

273297
if (contextConfigDeclaredLocally && contextHierarchyDeclaredLocally) {
274-
String msg = String.format("Test class [%s] has been configured with both @ContextConfiguration "
298+
String msg = String.format("Class [%s] has been configured with both @ContextConfiguration "
275299
+ "and @ContextHierarchy. Only one of these annotations may be declared on a test class "
276-
+ "or custom stereotype annotation.", rootDeclaringClass.getName());
300+
+ "or custom stereotype annotation.", declaringClass.getName());
277301
logger.error(msg);
278302
throw new IllegalStateException(msg);
279303
}
280304

281305
final List<ContextConfigurationAttributes> configAttributesList = new ArrayList<ContextConfigurationAttributes>();
282306

283307
if (contextConfigDeclaredLocally) {
284-
ContextConfiguration contextConfiguration = getAnnotation(declaringClass, contextConfigType);
285-
convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass,
286-
configAttributesList);
308+
convertAnnotationAttributesToConfigAttributesAndAddToList(descriptor.getAnnotationAttributes(),
309+
declaringClass, configAttributesList);
287310
}
288311
else if (contextHierarchyDeclaredLocally) {
289312
ContextHierarchy contextHierarchy = getAnnotation(declaringClass, contextHierarchyType);
@@ -293,7 +316,7 @@ else if (contextHierarchyDeclaredLocally) {
293316
}
294317
}
295318
else {
296-
// This should theoretically actually never happen...
319+
// This should theoretically never happen...
297320
String msg = String.format("Test class [%s] has been configured with neither @ContextConfiguration "
298321
+ "nor @ContextHierarchy as a class-level annotation.", rootDeclaringClass.getName());
299322
logger.error(msg);
@@ -405,13 +428,9 @@ static List<ContextConfigurationAttributes> resolveContextConfigurationAttribute
405428
annotationType.getName(), testClass.getName()));
406429

407430
while (descriptor != null) {
408-
Class<?> rootDeclaringClass = descriptor.getDeclaringClass();
409-
Class<?> declaringClass = (descriptor.getStereotype() != null) ? descriptor.getStereotypeType()
410-
: rootDeclaringClass;
411-
412-
convertContextConfigToConfigAttributesAndAddToList(descriptor.getAnnotation(), declaringClass,
413-
attributesList);
414-
descriptor = findAnnotationDescriptor(rootDeclaringClass.getSuperclass(), annotationType);
431+
convertAnnotationAttributesToConfigAttributesAndAddToList(descriptor.getAnnotationAttributes(),
432+
descriptor.getDeclaringClass(), attributesList);
433+
descriptor = findAnnotationDescriptor(descriptor.getRootDeclaringClass().getSuperclass(), annotationType);
415434
}
416435

417436
return attributesList;
@@ -489,20 +508,18 @@ static String[] resolveActiveProfiles(Class<?> testClass) {
489508
final Set<String> activeProfiles = new HashSet<String>();
490509

491510
while (descriptor != null) {
492-
Class<?> rootDeclaringClass = descriptor.getDeclaringClass();
493-
Class<?> declaringClass = (descriptor.getStereotype() != null) ? descriptor.getStereotypeType()
494-
: rootDeclaringClass;
511+
Class<?> declaringClass = descriptor.getDeclaringClass();
495512

496-
ActiveProfiles annotation = descriptor.getAnnotation();
513+
AnnotationAttributes annAttrs = descriptor.getAnnotationAttributes();
497514
if (logger.isTraceEnabled()) {
498-
logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s].", annotation,
499-
declaringClass.getName()));
515+
logger.trace(String.format("Retrieved @ActiveProfiles attributes [%s] for declaring class [%s].",
516+
annAttrs, declaringClass.getName()));
500517
}
501-
validateActiveProfilesConfiguration(declaringClass, annotation);
518+
validateActiveProfilesConfiguration(declaringClass, annAttrs);
502519

503-
String[] profiles = annotation.profiles();
504-
String[] valueProfiles = annotation.value();
505-
Class<? extends ActiveProfilesResolver> resolverClass = annotation.resolver();
520+
String[] profiles = annAttrs.getStringArray("profiles");
521+
String[] valueProfiles = annAttrs.getStringArray("value");
522+
Class<? extends ActiveProfilesResolver> resolverClass = annAttrs.getClass("resolver");
506523

507524
boolean resolverDeclared = !ActiveProfilesResolver.class.equals(resolverClass);
508525
boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles);
@@ -538,17 +555,17 @@ else if (valueDeclared) {
538555
}
539556
}
540557

541-
descriptor = annotation.inheritProfiles() ? findAnnotationDescriptor(rootDeclaringClass.getSuperclass(),
542-
annotationType) : null;
558+
descriptor = annAttrs.getBoolean("inheritProfiles") ? findAnnotationDescriptor(
559+
descriptor.getRootDeclaringClass().getSuperclass(), annotationType) : null;
543560
}
544561

545562
return StringUtils.toStringArray(activeProfiles);
546563
}
547564

548-
private static void validateActiveProfilesConfiguration(Class<?> declaringClass, ActiveProfiles annotation) {
549-
String[] valueProfiles = annotation.value();
550-
String[] profiles = annotation.profiles();
551-
Class<? extends ActiveProfilesResolver> resolverClass = annotation.resolver();
565+
private static void validateActiveProfilesConfiguration(Class<?> declaringClass, AnnotationAttributes annAttrs) {
566+
String[] valueProfiles = annAttrs.getStringArray("value");
567+
String[] profiles = annAttrs.getStringArray("profiles");
568+
Class<? extends ActiveProfilesResolver> resolverClass = annAttrs.getClass("resolver");
552569
boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles);
553570
boolean profilesDeclared = !ObjectUtils.isEmpty(profiles);
554571
boolean resolverDeclared = !ActiveProfilesResolver.class.equals(resolverClass);

spring-test/src/main/java/org/springframework/test/context/MetaAnnotationUtils.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import java.lang.annotation.Annotation;
2020

21+
import org.springframework.core.annotation.AnnotatedElementUtils;
22+
import org.springframework.core.annotation.AnnotationAttributes;
2123
import org.springframework.core.style.ToStringCreator;
2224
import org.springframework.util.Assert;
2325
import org.springframework.util.ObjectUtils;
@@ -124,7 +126,7 @@ public static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(Class
124126
* <p>
125127
* If the annotation is used as a meta-annotation, the descriptor also includes
126128
* the {@linkplain #getStereotype() stereotype} on which the annotation is
127-
* present. In such cases, the <em>declaring class</em> is not directly
129+
* present. In such cases, the <em>root declaring class</em> is not directly
128130
* annotated with the annotation but rather indirectly via the stereotype.
129131
*
130132
* <p>
@@ -133,6 +135,7 @@ public static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(Class
133135
* properties of the {@code AnnotationDescriptor} would be as follows.
134136
*
135137
* <ul>
138+
* <li>rootDeclaringClass: {@code TransactionalTests} class object</li>
136139
* <li>declaringClass: {@code TransactionalTests} class object</li>
137140
* <li>stereotype: {@code null}</li>
138141
* <li>annotation: instance of the {@code Transactional} annotation</li>
@@ -150,7 +153,8 @@ public static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(Class
150153
* properties of the {@code AnnotationDescriptor} would be as follows.
151154
*
152155
* <ul>
153-
* <li>declaringClass: {@code UserRepositoryTests} class object</li>
156+
* <li>rootDeclaringClass: {@code UserRepositoryTests} class object</li>
157+
* <li>declaringClass: {@code RepositoryTests} class object</li>
154158
* <li>stereotype: instance of the {@code RepositoryTests} annotation</li>
155159
* <li>annotation: instance of the {@code Transactional} annotation</li>
156160
* </ul>
@@ -170,22 +174,31 @@ public static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(Class
170174
*/
171175
public static class AnnotationDescriptor<T extends Annotation> {
172176

177+
private final Class<?> rootDeclaringClass;
173178
private final Class<?> declaringClass;
174179
private final Annotation stereotype;
175180
private final T annotation;
181+
private final AnnotationAttributes annotationAttributes;
176182

177183

178-
public AnnotationDescriptor(Class<?> declaringClass, T annotation) {
179-
this(declaringClass, null, annotation);
184+
public AnnotationDescriptor(Class<?> rootDeclaringClass, T annotation) {
185+
this(rootDeclaringClass, null, annotation);
180186
}
181187

182-
public AnnotationDescriptor(Class<?> declaringClass, Annotation stereotype, T annotation) {
183-
Assert.notNull(declaringClass, "declaringClass must not be null");
188+
public AnnotationDescriptor(Class<?> rootDeclaringClass, Annotation stereotype, T annotation) {
189+
Assert.notNull(rootDeclaringClass, "rootDeclaringClass must not be null");
184190
Assert.notNull(annotation, "annotation must not be null");
185191

186-
this.declaringClass = declaringClass;
192+
this.rootDeclaringClass = rootDeclaringClass;
193+
this.declaringClass = (stereotype != null) ? stereotype.annotationType() : rootDeclaringClass;
187194
this.stereotype = stereotype;
188195
this.annotation = annotation;
196+
this.annotationAttributes = AnnotatedElementUtils.getAnnotationAttributes(rootDeclaringClass,
197+
annotation.annotationType().getName());
198+
}
199+
200+
public Class<?> getRootDeclaringClass() {
201+
return this.rootDeclaringClass;
189202
}
190203

191204
public Class<?> getDeclaringClass() {
@@ -200,6 +213,10 @@ public Class<? extends Annotation> getAnnotationType() {
200213
return this.annotation.annotationType();
201214
}
202215

216+
public AnnotationAttributes getAnnotationAttributes() {
217+
return this.annotationAttributes;
218+
}
219+
203220
public Annotation getStereotype() {
204221
return this.stereotype;
205222
}
@@ -214,6 +231,7 @@ public Class<? extends Annotation> getStereotypeType() {
214231
@Override
215232
public String toString() {
216233
return new ToStringCreator(this)//
234+
.append("rootDeclaringClass", rootDeclaringClass)//
217235
.append("declaringClass", declaringClass)//
218236
.append("stereotype", stereotype)//
219237
.append("annotation", annotation)//

0 commit comments

Comments
 (0)