Skip to content

Commit

Permalink
Stop using SpringFactoriesLoader.loadFactoryNames() in spring-test
Browse files Browse the repository at this point in the history
Since SpringFactoriesLoader.loadFactoryNames() will be deprecated in
gh-27954, this commit removes the use of it in the spring-test module.

Specifically, this commit removes the protected
getDefaultTestExecutionListenerClasses() and
getDefaultTestExecutionListenerClassNames() methods from
AbstractTestContextBootstrapper and replaces them with a new protected
getDefaultTestExecutionListeners() method that makes use of new APIs
introduced in SpringFactoriesLoader for 6.0.

Third-party subclasses of AbstractTestContextBootstrapper that have
overridden or used getDefaultTestExecutionListenerClasses() or
getDefaultTestExecutionListenerClassNames() will therefore need to
migrate to getDefaultTestExecutionListeners() in Spring Framework 6.0.

Closes gh-28666
  • Loading branch information
sbrannen committed Jun 24, 2022
1 parent 24c4614 commit d1b65f6
Showing 1 changed file with 71 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand All @@ -32,6 +33,7 @@
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
import org.springframework.lang.Nullable;
import org.springframework.test.context.BootstrapContext;
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
Expand Down Expand Up @@ -112,7 +114,7 @@ public TestContext buildTestContext() {
public final List<TestExecutionListener> getTestExecutionListeners() {
Class<?> clazz = getBootstrapContext().getTestClass();
Class<TestExecutionListeners> annotationType = TestExecutionListeners.class;
List<Class<? extends TestExecutionListener>> classesList = new ArrayList<>();
List<TestExecutionListener> listeners = new ArrayList<>(8);
boolean usingDefaults = false;

AnnotationDescriptor<TestExecutionListeners> descriptor =
Expand All @@ -125,7 +127,7 @@ public final List<TestExecutionListener> getTestExecutionListeners() {
clazz.getName()));
}
usingDefaults = true;
classesList.addAll(getDefaultTestExecutionListenerClasses());
listeners.addAll(getDefaultTestExecutionListeners());
}
else {
// Traverse the class hierarchy...
Expand All @@ -149,24 +151,27 @@ public final List<TestExecutionListener> getTestExecutionListeners() {
"@TestExecutionListeners for class [%s].", descriptor.getRootDeclaringClass().getName()));
}
usingDefaults = true;
classesList.addAll(getDefaultTestExecutionListenerClasses());
listeners.addAll(getDefaultTestExecutionListeners());
}

classesList.addAll(0, Arrays.asList(testExecutionListeners.listeners()));
listeners.addAll(0, instantiateListeners(testExecutionListeners.listeners()));

descriptor = (inheritListeners ? parentDescriptor : null);
}
}

Collection<Class<? extends TestExecutionListener>> classesToUse = classesList;
// Remove possible duplicates if we loaded default listeners.
if (usingDefaults) {
classesToUse = new LinkedHashSet<>(classesList);
}
// Remove possible duplicates if we loaded default listeners.
List<TestExecutionListener> uniqueListeners = new ArrayList<>(listeners.size());
listeners.forEach(listener -> {
Class<? extends TestExecutionListener> listenerClass = listener.getClass();
if (uniqueListeners.stream().map(Object::getClass).noneMatch(listenerClass::equals)) {
uniqueListeners.add(listener);
}
});
listeners = uniqueListeners;

List<TestExecutionListener> listeners = instantiateListeners(classesToUse);
// Sort by Ordered/@Order if we loaded default listeners.
if (usingDefaults) {
// Sort by Ordered/@Order if we loaded default listeners.
AnnotationAwareOrderComparator.sort(listeners);
}

Expand All @@ -176,8 +181,61 @@ public final List<TestExecutionListener> getTestExecutionListeners() {
return listeners;
}

private List<TestExecutionListener> instantiateListeners(Collection<Class<? extends TestExecutionListener>> classes) {
List<TestExecutionListener> listeners = new ArrayList<>(classes.size());
/**
* Get the default {@link TestExecutionListener TestExecutionListeners} for
* this bootstrapper.
* <p>This method is invoked by {@link #getTestExecutionListeners()}.
* <p>The default implementation looks up and instantiates all
* {@code org.springframework.test.context.TestExecutionListener} entries
* configured in all {@code META-INF/spring.factories} files on the classpath.
* <p>If a particular listener cannot be loaded due to a {@link LinkageError}
* or {@link ClassNotFoundException}, a {@code DEBUG} message will be logged,
* but the associated exception will not be rethrown. A {@link RuntimeException}
* or any other {@link Error} will be rethrown. Any other exception will be
* thrown wrapped in an {@link IllegalStateException}.
* @return an <em>unmodifiable</em> list of default {@code TestExecutionListener}
* instances
* @since 6.0
* @see SpringFactoriesLoader#forDefaultResourceLocation()
* @see SpringFactoriesLoader#load(Class, FailureHandler)
*/
protected List<TestExecutionListener> getDefaultTestExecutionListeners() {
FailureHandler failureHandler = (factoryType, factoryImplementationName, ex) -> {
if (ex instanceof LinkageError || ex instanceof ClassNotFoundException) {
if (logger.isDebugEnabled()) {
logger.debug("Could not load default TestExecutionListener [" + factoryImplementationName +
"]. Specify custom listener classes or make the default listener classes available.", ex);
}
}
else {
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (ex instanceof Error error) {
throw error;
}
throw new IllegalStateException("Failed to load default TestExecutionListener [" +
factoryImplementationName + "].", ex);
}
};

List<TestExecutionListener> listeners = SpringFactoriesLoader.forDefaultResourceLocation()
.load(TestExecutionListener.class, failureHandler);

if (logger.isInfoEnabled()) {
List<String> classNames = listeners.stream()
.map(listener -> listener.getClass().getName())
.collect(Collectors.toList());
logger.info(String.format("Loaded default TestExecutionListener implementations from location [%s]: %s",
SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames));
}

return Collections.unmodifiableList(listeners);
}

@SuppressWarnings("unchecked")
private List<TestExecutionListener> instantiateListeners(Class<? extends TestExecutionListener>... classes) {
List<TestExecutionListener> listeners = new ArrayList<>(classes.length);
for (Class<? extends TestExecutionListener> listenerClass : classes) {
try {
listeners.add(BeanUtils.instantiateClass(listenerClass));
Expand All @@ -201,53 +259,6 @@ private List<TestExecutionListener> instantiateListeners(Collection<Class<? exte
return listeners;
}

/**
* Get the default {@link TestExecutionListener} classes for this bootstrapper.
* <p>This method is invoked by {@link #getTestExecutionListeners()} and
* delegates to {@link #getDefaultTestExecutionListenerClassNames()} to
* retrieve the class names.
* <p>If a particular class cannot be loaded, a {@code DEBUG} message will
* be logged, but the associated exception will not be rethrown.
*/
@SuppressWarnings("unchecked")
protected Set<Class<? extends TestExecutionListener>> getDefaultTestExecutionListenerClasses() {
Set<Class<? extends TestExecutionListener>> defaultListenerClasses = new LinkedHashSet<>();
ClassLoader cl = getClass().getClassLoader();
for (String className : getDefaultTestExecutionListenerClassNames()) {
try {
defaultListenerClasses.add((Class<? extends TestExecutionListener>) ClassUtils.forName(className, cl));
}
catch (Throwable ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not load default TestExecutionListener class [" + className +
"]. Specify custom listener classes or make the default listener classes available.", ex);
}
}
}
return defaultListenerClasses;
}

/**
* Get the names of the default {@link TestExecutionListener} classes for
* this bootstrapper.
* <p>The default implementation looks up all
* {@code org.springframework.test.context.TestExecutionListener} entries
* configured in all {@code META-INF/spring.factories} files on the classpath.
* <p>This method is invoked by {@link #getDefaultTestExecutionListenerClasses()}.
* @return an <em>unmodifiable</em> list of names of default {@code TestExecutionListener}
* classes
* @see SpringFactoriesLoader#loadFactoryNames
*/
protected List<String> getDefaultTestExecutionListenerClassNames() {
List<String> classNames =
SpringFactoriesLoader.loadFactoryNames(TestExecutionListener.class, getClass().getClassLoader());
if (logger.isInfoEnabled()) {
logger.info(String.format("Loaded default TestExecutionListener class names from location [%s]: %s",
SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames));
}
return Collections.unmodifiableList(classNames);
}

/**
* {@inheritDoc}
*/
Expand Down

0 comments on commit d1b65f6

Please sign in to comment.