Support @[Before|After]Transaction on default methods

Prior to this commit, @BeforeTransaction and @AfterTransaction could
only be declared on methods within test classes. However, JUnit 5 as
well as some existing third-party Runner implementations for JUnit 4
already support Java 8 based interface default methods in various
scenarios -- for example, @Test, @BeforeEach, etc.

This commit brings the Spring TestContext Framework up to date by
supporting the declaration of @BeforeTransaction and @AfterTransaction
on interface default methods.

Issue: SPR-14183
This commit is contained in:
Sam Brannen
2016-05-03 18:53:17 +02:00
parent 7ce5ba4a3f
commit 0f6711fe3b
6 changed files with 46 additions and 101 deletions

View File

@@ -23,12 +23,16 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <p>Test annotation to indicate that the annotated {@code void} method
* <p>Test annotation which indicates that the annotated {@code void} method
* should be executed <em>after</em> a transaction is ended for a test method
* configured to run within a transaction via the {@code @Transactional} annotation.
* configured to run within a transaction via Spring's {@code @Transactional}
* annotation.
*
* <p>The {@code @AfterTransaction} methods of superclasses will be executed
* after those of the current class.
* <p>As of Spring Framework 4.3, {@code @AfterTransaction} may be declared on
* Java 8 based interface default methods.
*
* <p>{@code @AfterTransaction} methods declared in superclasses or as interface
* default methods will be executed after those of the current test class.
*
* <p>As of Spring Framework 4.0, this annotation may be used as a
* <em>meta-annotation</em> to create custom <em>composed annotations</em>.

View File

@@ -23,12 +23,16 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <p>Test annotation to indicate that the annotated {@code void} method
* <p>Test annotation which indicates that the annotated {@code void} method
* should be executed <em>before</em> a transaction is started for a test method
* configured to run within a transaction via the {@code @Transactional} annotation.
* configured to run within a transaction via Spring's {@code @Transactional}
* annotation.
*
* <p>The {@code @BeforeTransaction} methods of superclasses will be executed
* before those of the current class.
* <p>As of Spring Framework 4.3, {@code @BeforeTransaction} may be declared on
* Java 8 based interface default methods.
*
* <p>{@code @BeforeTransaction} methods declared in superclasses or as interface
* default methods will be executed before those of the current test class.
*
* <p>As of Spring Framework 4.0, this annotation may be used as a
* <em>meta-annotation</em> to create custom <em>composed annotations</em>.

View File

@@ -97,9 +97,10 @@ import org.springframework.util.StringUtils;
* <p>When executing transactional tests, it is sometimes useful to be able to
* execute certain <em>set up</em> or <em>tear down</em> code outside of a
* transaction. {@code TransactionalTestExecutionListener} provides such
* support for methods annotated with
* {@link BeforeTransaction @BeforeTransaction} or
* {@link AfterTransaction @AfterTransaction}.
* support for methods annotated with {@link BeforeTransaction @BeforeTransaction}
* or {@link AfterTransaction @AfterTransaction}. As of Spring Framework 4.3,
* {@code @BeforeTransaction} and {@code @AfterTransaction} may also be declared
* on Java 8 based interface default methods.
*
* <h3>Configuring a Transaction Manager</h3>
* <p>{@code TransactionalTestExecutionListener} expects a
@@ -431,90 +432,23 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
}
/**
* Gets all superclasses of the supplied {@link Class class}, including the
* class itself. The ordering of the returned list will begin with the
* supplied class and continue up the class hierarchy, excluding {@link Object}.
* <p>Note: This code has been borrowed from
* {@link org.junit.internal.runners.TestClass#getSuperClasses(Class)} and
* adapted.
* @param clazz the class for which to retrieve the superclasses
* @return all superclasses of the supplied class, excluding {@code Object}
*/
private List<Class<?>> getSuperClasses(Class<?> clazz) {
List<Class<?>> results = new ArrayList<Class<?>>();
Class<?> current = clazz;
while (current != null && Object.class != current) {
results.add(current);
current = current.getSuperclass();
}
return results;
}
/**
* Gets all methods in the supplied {@link Class class} and its superclasses
* Get all methods in the supplied {@link Class class} and its superclasses
* which are annotated with the supplied {@code annotationType} but
* which are not <em>shadowed</em> by methods overridden in subclasses.
* <p>Note: This code has been borrowed from
* {@link org.junit.internal.runners.TestClass#getAnnotatedMethods(Class)}
* and adapted.
* <p>Default methods on interfaces are also detected.
* @param clazz the class for which to retrieve the annotated methods
* @param annotationType the annotation type for which to search
* @return all annotated methods in the supplied class and its superclasses
* as well as annotated interface default methods
*/
private List<Method> getAnnotatedMethods(Class<?> clazz, Class<? extends Annotation> annotationType) {
List<Method> results = new ArrayList<Method>();
for (Class<?> current : getSuperClasses(clazz)) {
for (Method method : current.getDeclaredMethods()) {
Annotation annotation = AnnotationUtils.getAnnotation(method, annotationType);
if (annotation != null && !isShadowed(method, results)) {
results.add(method);
}
List<Method> methods = new ArrayList<Method>(4);
for (Method method : ReflectionUtils.getUniqueDeclaredMethods(clazz)) {
if (AnnotationUtils.getAnnotation(method, annotationType) != null) {
methods.add(method);
}
}
return results;
}
/**
* Determine if the supplied {@link Method method} is <em>shadowed</em> by
* a method in the supplied {@link List list} of previous methods.
* <p>Note: This code has been borrowed from
* {@link org.junit.internal.runners.TestClass#isShadowed(Method, List)}.
* @param method the method to check for shadowing
* @param previousMethods the list of methods which have previously been processed
* @return {@code true} if the supplied method is shadowed by a
* method in the {@code previousMethods} list
*/
private boolean isShadowed(Method method, List<Method> previousMethods) {
for (Method each : previousMethods) {
if (isShadowed(method, each)) {
return true;
}
}
return false;
}
/**
* Determine if the supplied {@linkplain Method current method} is
* <em>shadowed</em> by a {@linkplain Method previous method}.
* <p>Note: This code has been borrowed from
* {@link org.junit.internal.runners.TestClass#isShadowed(Method, Method)}.
* @param current the current method
* @param previous the previous method
* @return {@code true} if the previous method shadows the current one
*/
private boolean isShadowed(Method current, Method previous) {
if (!previous.getName().equals(current.getName())) {
return false;
}
if (previous.getParameterTypes().length != current.getParameterTypes().length) {
return false;
}
for (int i = 0; i < previous.getParameterTypes().length; i++) {
if (!previous.getParameterTypes()[i].equals(current.getParameterTypes()[i])) {
return false;
}
}
return true;
return methods;
}
/**