directly present) on the supplied
- * {@code clazz}.
- * The supplied {@link Class} may represent any type.
+ * {@code annotatedElement}.
+ *
The supplied {@link AnnotatedElement} may represents any annotated element.
*
Meta-annotations will not be searched.
*
Note: This method does not determine if the annotation
* is {@linkplain java.lang.annotation.Inherited inherited}. For greater
* clarity regarding inherited annotations, consider using
* {@link #isAnnotationInherited(Class, Class)} instead.
* @param annotationType the annotation type to look for
- * @param clazz the class to check for the annotation on
+ * @param annotatedElement the element to check for the annotation on
* @return {@code true} if an annotation of the specified {@code annotationType}
* is directly present
* @see java.lang.Class#getDeclaredAnnotations()
* @see java.lang.Class#getDeclaredAnnotation(Class)
* @see #isAnnotationInherited(Class, Class)
*/
- public static boolean isAnnotationDeclaredLocally(Class extends Annotation> annotationType, Class> clazz) {
+ public static boolean isAnnotationDeclaredLocally(Class extends Annotation> annotationType,
+ AnnotatedElement annotatedElement) {
Assert.notNull(annotationType, "Annotation type must not be null");
- Assert.notNull(clazz, "Class must not be null");
+ Assert.notNull(annotatedElement, "Annotated element must not be null");
try {
- for (Annotation ann : clazz.getDeclaredAnnotations()) {
+ for (Annotation ann : annotatedElement.getDeclaredAnnotations()) {
if (ann.annotationType() == annotationType) {
return true;
}
}
}
catch (Throwable ex) {
- handleIntrospectionFailure(clazz, ex);
+ handleIntrospectionFailure(annotatedElement, ex);
}
return false;
}
@@ -832,7 +833,7 @@ public static boolean isAnnotationDeclaredLocally(Class extends Annotation> an
* @return {@code true} if an annotation of the specified {@code annotationType}
* is present and inherited
* @see Class#isAnnotationPresent(Class)
- * @see #isAnnotationDeclaredLocally(Class, Class)
+ * @see #isAnnotationDeclaredLocally(Class, AnnotatedElement)
*/
public static boolean isAnnotationInherited(Class extends Annotation> annotationType, Class> clazz) {
Assert.notNull(annotationType, "Annotation type must not be null");
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 81e795295d35..c511df26f2ae 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
@@ -407,6 +407,14 @@ public void isAnnotationDeclaredLocallyForAllScenarios() throws Exception {
assertFalse(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationInterface.class));
assertTrue(isAnnotationDeclaredLocally(Order.class, NonInheritedAnnotationClass.class));
assertFalse(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationClass.class));
+
+ // inherited method-level annotation; note: @Transactional is inherited
+ assertTrue(isAnnotationDeclaredLocally(Transactional.class, InheritedAnnotationClass.class.getMethod("something")));
+ assertFalse(isAnnotationDeclaredLocally(Transactional.class, SubInheritedAnnotationClass.class.getMethod("something")));
+
+ // non-inherited method-level annotation; note: @Order is not inherited
+ assertTrue(isAnnotationDeclaredLocally(Order.class, NonInheritedAnnotationInterface.class.getMethod("something")));
+ assertFalse(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationInterface.class.getMethod("something")));
}
@Test
@@ -1719,9 +1727,13 @@ public interface SubSubInheritedAnnotationInterface extends SubInheritedAnnotati
@Order
public interface NonInheritedAnnotationInterface {
+ @Order
+ void something();
}
public interface SubNonInheritedAnnotationInterface extends NonInheritedAnnotationInterface {
+ @Override
+ void something();
}
public interface SubSubNonInheritedAnnotationInterface extends SubNonInheritedAnnotationInterface {
@@ -1735,9 +1747,17 @@ public interface NonAnnotatedInterface {
@Transactional
public static class InheritedAnnotationClass {
+ @Transactional
+ public void something() {
+ // for test purposes
+ }
}
public static class SubInheritedAnnotationClass extends InheritedAnnotationClass {
+ @Override
+ public void something() {
+ // for test purposes
+ }
}
@Order
diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java
index 5b81650b7dcc..4aabf396ba95 100644
--- a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java
+++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java
@@ -17,6 +17,7 @@
package org.springframework.test.context;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
import java.util.Set;
import org.apache.commons.logging.Log;
@@ -50,6 +51,9 @@ abstract class BootstrapUtils {
private static final String DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME =
"org.springframework.test.context.support.DefaultTestContextBootstrapper";
+ private static final String DEFAULT_TEST_METHOD_CONTEXT_BOOTSTRAPPER_CLASS_NAME =
+ "org.springframework.test.context.support.DefaultTestMethodContextBootstrapper";
+
private static final String DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME =
"org.springframework.test.context.web.WebTestContextBootstrapper";
@@ -147,6 +151,42 @@ static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext b
}
}
+ /**
+ * Resolve the {@link TestContextBootstrapper} type for the supplied test method.
+ *
Will use
+ * {@link org.springframework.test.context.support.DefaultTestMethodContextBootstrapper
+ * DefaultTestMethodContextBootstrapper}.
+ * @param testMethod the method for which to create context bootstrapper
+ * @return a fully configured {@code TestContextBootstrapper} for particular method
+ */
+ @SuppressWarnings("unchecked")
+ static TestContextBootstrapper resolveTestContextBootstrapper(Method testMethod) {
+ final BootstrapContext bootstrapContext = createBootstrapContext(testMethod.getDeclaringClass());
+ Class> testClass = bootstrapContext.getTestClass();
+
+ Class> clazz = null;
+ try {
+ clazz = ClassUtils.forName(DEFAULT_TEST_METHOD_CONTEXT_BOOTSTRAPPER_CLASS_NAME,
+ BootstrapUtils.class.getClassLoader());
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Instantiating TestContextBootstrapper for test class [%s] from class [%s]",
+ testClass.getName(), clazz.getName()));
+ }
+ TestContextBootstrapper testContextBootstrapper = BeanUtils.instantiateClass(
+ (Constructor extends TestContextBootstrapper>) clazz.getConstructor(Method.class), testMethod);
+ testContextBootstrapper.setBootstrapContext(bootstrapContext);
+ return testContextBootstrapper;
+ }
+ catch (IllegalStateException ex) {
+ throw ex;
+ }
+ catch (Throwable ex) {
+ throw new IllegalStateException("Could not load TestContextBootstrapper [" + clazz +
+ "]. Specify @BootstrapWith's 'value' attribute or make the default bootstrapper class available.",
+ ex);
+ }
+ }
+
private static Class> resolveExplicitTestContextBootstrapper(Class> testClass) {
Set annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class);
if (annotations.size() < 1) {
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java
index 669adbd65c74..7b407f7b4eac 100644
--- a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java
@@ -83,7 +83,7 @@
* @see MergedContextConfiguration
* @see org.springframework.context.ApplicationContext
*/
-@Target(ElementType.TYPE)
+@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java
index f32731dfa5b6..9ffc829598f1 100644
--- a/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java
@@ -16,6 +16,7 @@
package org.springframework.test.context;
+import java.lang.reflect.Method;
import java.util.Arrays;
import org.apache.commons.logging.Log;
@@ -51,6 +52,8 @@ public class ContextConfigurationAttributes {
private final Class> declaringClass;
+ private final Method declaringMethod;
+
private Class>[] classes;
private String[] locations;
@@ -149,6 +152,14 @@ public ContextConfigurationAttributes(
Class> declaringClass, String[] locations, Class>[] classes, boolean inheritLocations,
Class extends ApplicationContextInitializer extends ConfigurableApplicationContext>>[] initializers,
boolean inheritInitializers, String name, Class extends ContextLoader> contextLoaderClass) {
+ this(declaringClass, null, locations, classes, inheritLocations, initializers, inheritInitializers,
+ name, contextLoaderClass);
+ }
+
+ public ContextConfigurationAttributes(
+ Class> declaringClass, Method declaringMethod, String[] locations, Class>[] classes, boolean inheritLocations,
+ Class extends ApplicationContextInitializer extends ConfigurableApplicationContext>>[] initializers,
+ boolean inheritInitializers, String name, Class extends ContextLoader> contextLoaderClass) {
Assert.notNull(declaringClass, "declaringClass must not be null");
Assert.notNull(contextLoaderClass, "contextLoaderClass must not be null");
@@ -156,13 +167,14 @@ public ContextConfigurationAttributes(
if (!ObjectUtils.isEmpty(locations) && !ObjectUtils.isEmpty(classes) && logger.isDebugEnabled()) {
logger.debug(String.format(
"Test class [%s] has been configured with @ContextConfiguration's 'locations' (or 'value') %s " +
- "and 'classes' %s attributes. Most SmartContextLoader implementations support " +
- "only one declaration of resources per @ContextConfiguration annotation.",
+ "and 'classes' %s attributes. Most SmartContextLoader implementations support " +
+ "only one declaration of resources per @ContextConfiguration annotation.",
declaringClass.getName(), ObjectUtils.nullSafeToString(locations),
ObjectUtils.nullSafeToString(classes)));
}
this.declaringClass = declaringClass;
+ this.declaringMethod = declaringMethod;
this.locations = locations;
this.classes = classes;
this.inheritLocations = inheritLocations;
@@ -183,6 +195,16 @@ public Class> getDeclaringClass() {
return this.declaringClass;
}
+ /**
+ * Get the {@linkplain Method method} that declared the
+ * {@link ContextConfiguration @ContextConfiguration} annotation, either explicitly
+ * or implicitly.
+ * @return the declaring method (may be {@code null})
+ */
+ public Method getDeclaringMethod() {
+ return this.declaringMethod;
+ }
+
/**
* Set the processed annotated classes, effectively overriding the
* original value declared via {@link ContextConfiguration @ContextConfiguration}.
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java
index b22dc18ad599..7c168b4dc02f 100644
--- a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java
@@ -139,7 +139,7 @@
* @see ContextConfiguration
* @see org.springframework.context.ApplicationContext
*/
-@Target(ElementType.TYPE)
+@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java
index 1d625d4a935a..6857066d0cd1 100644
--- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java
+++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java
@@ -25,6 +25,8 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
+import org.springframework.test.util.MetaAnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
@@ -282,10 +284,11 @@ public void prepareTestInstance(Object testInstance) throws Exception {
public void beforeTestMethod(Object testInstance, Method testMethod) throws Exception {
String callbackName = "beforeTestMethod";
prepareForBeforeCallback(callbackName, testInstance, testMethod);
+ TestContext preparedContext = prepareTestContextBeforeMethod(getTestContext());
for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
try {
- testExecutionListener.beforeTestMethod(getTestContext());
+ testExecutionListener.beforeTestMethod(preparedContext);
}
catch (Throwable ex) {
handleBeforeException(ex, callbackName, testExecutionListener, testInstance, testMethod);
@@ -294,6 +297,46 @@ public void beforeTestMethod(Object testInstance, Method testMethod) throws Exce
}
/**
+ * Prepares {@linkplain TestContext test context} in the sense
+ * that if current test method annotated with context configuration
+ * annotation e.g. {@link ContextConfiguration}, {@link ContextHierarchy}
+ * then emits throw-away context, constructed specifically for current
+ * test method, otherwise returns original test context without modifications.
+ *
+ * @param originalTestContext test class test context
+ * @return throw-away context if current method has context annotation or
+ * originalTestContext otherwise.
+ *
+ * @since 5.0
+ */
+ private TestContext prepareTestContextBeforeMethod(TestContext originalTestContext) {
+ Method testMethod = originalTestContext.getTestMethod();
+ if (testMethod != null && MetaAnnotationUtils.findAnnotationDescriptorForTypes(testMethod,
+ ContextConfiguration.class, ContextHierarchy.class) != null) {
+ return createThrowAwayContext(testMethod, originalTestContext);
+ }
+ return originalTestContext;
+ }
+
+ /**
+ * Creates throw-away context for supplied {@linkplain Method testMethod}
+ * and copies the state of original context in the created one.
+ * Sets an attribute {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
+ * of the created context to true
to make reinjection.
+ * @param testMethod test method annotated with context configuration annotation
+ * @param originalContext original test context to copy state into created one
+ * @return throw-away context for specified test method
+ * @since 5.0
+ */
+ private TestContext createThrowAwayContext(Method testMethod, TestContext originalContext) {
+ TestContext throwAwayContext = BootstrapUtils.resolveTestContextBootstrapper(testMethod).buildTestContext();
+ throwAwayContext.updateState(originalContext.getTestInstance(), testMethod, null);
+ throwAwayContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE,
+ Boolean.TRUE);
+ return throwAwayContext;
+ }
+
+ /**
* Hook for pre-processing a test immediately before execution of
* the {@linkplain java.lang.reflect.Method test method} in the supplied
* {@linkplain TestContext test context} — for example, for timing
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java
index 376cdaa332c8..722f6c1f99bf 100644
--- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java
+++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java
@@ -25,6 +25,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -278,36 +279,45 @@ public final MergedContextConfiguration buildMergedContextConfiguration() {
}
if (AnnotationUtils.findAnnotation(testClass, ContextHierarchy.class) != null) {
- Map> hierarchyMap =
- ContextLoaderUtils.buildContextHierarchyMap(testClass);
- MergedContextConfiguration parentConfig = null;
- MergedContextConfiguration mergedConfig = null;
-
- for (List list : hierarchyMap.values()) {
- List reversedList = new ArrayList<>(list);
- Collections.reverse(reversedList);
-
- // Don't use the supplied testClass; instead ensure that we are
- // building the MCC for the actual test class that declared the
- // configuration for the current level in the context hierarchy.
- Assert.notEmpty(reversedList, "ContextConfigurationAttributes list must not be empty");
- Class> declaringClass = reversedList.get(0).getDeclaringClass();
-
- mergedConfig = buildMergedContextConfiguration(
- declaringClass, reversedList, parentConfig, cacheAwareContextLoaderDelegate, true);
- parentConfig = mergedConfig;
- }
-
- // Return the last level in the context hierarchy
- return mergedConfig;
- }
- else {
+ return buildMergedConfigFromHierarchyMap(() -> ContextLoaderUtils.buildContextHierarchyMap(testClass));
+ } else {
return buildMergedContextConfiguration(testClass,
ContextLoaderUtils.resolveContextConfigurationAttributes(testClass),
null, cacheAwareContextLoaderDelegate, true);
}
}
+ /**
+ * Convenience method to build {@linkplain MergedContextConfiguration merged configuration}
+ * for the supplied context hierarchy map.
+ * @param hierarchyMapSupplier supplies process with context hierarchy map
+ * @return the merged context configuration
+ */
+ protected MergedContextConfiguration buildMergedConfigFromHierarchyMap(
+ Supplier