From d1b65f6d3e90af3f55d1b4d347afb3ebe9a3de44 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 24 Jun 2022 16:25:29 +0200 Subject: [PATCH] Stop using SpringFactoriesLoader.loadFactoryNames() in spring-test 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 --- .../AbstractTestContextBootstrapper.java | 131 ++++++++++-------- 1 file changed, 71 insertions(+), 60 deletions(-) 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 dcdfd6bc19f..157bb6ba267 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 @@ -24,6 +24,7 @@ import java.util.LinkedHashSet; 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; @@ -32,6 +33,7 @@ import org.springframework.beans.BeanInstantiationException; 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; @@ -112,7 +114,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot public final List getTestExecutionListeners() { Class clazz = getBootstrapContext().getTestClass(); Class annotationType = TestExecutionListeners.class; - List> classesList = new ArrayList<>(); + List listeners = new ArrayList<>(8); boolean usingDefaults = false; AnnotationDescriptor descriptor = @@ -125,7 +127,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot clazz.getName())); } usingDefaults = true; - classesList.addAll(getDefaultTestExecutionListenerClasses()); + listeners.addAll(getDefaultTestExecutionListeners()); } else { // Traverse the class hierarchy... @@ -149,24 +151,27 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot "@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> 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 uniqueListeners = new ArrayList<>(listeners.size()); + listeners.forEach(listener -> { + Class listenerClass = listener.getClass(); + if (uniqueListeners.stream().map(Object::getClass).noneMatch(listenerClass::equals)) { + uniqueListeners.add(listener); + } + }); + listeners = uniqueListeners; - List 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); } @@ -176,8 +181,61 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot return listeners; } - private List instantiateListeners(Collection> classes) { - List listeners = new ArrayList<>(classes.size()); + /** + * Get the default {@link TestExecutionListener TestExecutionListeners} for + * this bootstrapper. + *

This method is invoked by {@link #getTestExecutionListeners()}. + *

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. + *

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 unmodifiable list of default {@code TestExecutionListener} + * instances + * @since 6.0 + * @see SpringFactoriesLoader#forDefaultResourceLocation() + * @see SpringFactoriesLoader#load(Class, FailureHandler) + */ + protected List 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 listeners = SpringFactoriesLoader.forDefaultResourceLocation() + .load(TestExecutionListener.class, failureHandler); + + if (logger.isInfoEnabled()) { + List 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 instantiateListeners(Class... classes) { + List listeners = new ArrayList<>(classes.length); for (Class listenerClass : classes) { try { listeners.add(BeanUtils.instantiateClass(listenerClass)); @@ -201,53 +259,6 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot return listeners; } - /** - * Get the default {@link TestExecutionListener} classes for this bootstrapper. - *

This method is invoked by {@link #getTestExecutionListeners()} and - * delegates to {@link #getDefaultTestExecutionListenerClassNames()} to - * retrieve the class names. - *

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> getDefaultTestExecutionListenerClasses() { - Set> defaultListenerClasses = new LinkedHashSet<>(); - ClassLoader cl = getClass().getClassLoader(); - for (String className : getDefaultTestExecutionListenerClassNames()) { - try { - defaultListenerClasses.add((Class) 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. - *

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. - *

This method is invoked by {@link #getDefaultTestExecutionListenerClasses()}. - * @return an unmodifiable list of names of default {@code TestExecutionListener} - * classes - * @see SpringFactoriesLoader#loadFactoryNames - */ - protected List getDefaultTestExecutionListenerClassNames() { - List 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} */