Skip to content

Commit d1b65f6

Browse files
committed
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
1 parent 24c4614 commit d1b65f6

File tree

1 file changed

+71
-60
lines changed

1 file changed

+71
-60
lines changed

spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java

Lines changed: 71 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.List;
2525
import java.util.Map;
2626
import java.util.Set;
27+
import java.util.stream.Collectors;
2728

2829
import org.apache.commons.logging.Log;
2930
import org.apache.commons.logging.LogFactory;
@@ -32,6 +33,7 @@
3233
import org.springframework.beans.BeanUtils;
3334
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
3435
import org.springframework.core.io.support.SpringFactoriesLoader;
36+
import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
3537
import org.springframework.lang.Nullable;
3638
import org.springframework.test.context.BootstrapContext;
3739
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
@@ -112,7 +114,7 @@ public TestContext buildTestContext() {
112114
public final List<TestExecutionListener> getTestExecutionListeners() {
113115
Class<?> clazz = getBootstrapContext().getTestClass();
114116
Class<TestExecutionListeners> annotationType = TestExecutionListeners.class;
115-
List<Class<? extends TestExecutionListener>> classesList = new ArrayList<>();
117+
List<TestExecutionListener> listeners = new ArrayList<>(8);
116118
boolean usingDefaults = false;
117119

118120
AnnotationDescriptor<TestExecutionListeners> descriptor =
@@ -125,7 +127,7 @@ public final List<TestExecutionListener> getTestExecutionListeners() {
125127
clazz.getName()));
126128
}
127129
usingDefaults = true;
128-
classesList.addAll(getDefaultTestExecutionListenerClasses());
130+
listeners.addAll(getDefaultTestExecutionListeners());
129131
}
130132
else {
131133
// Traverse the class hierarchy...
@@ -149,24 +151,27 @@ public final List<TestExecutionListener> getTestExecutionListeners() {
149151
"@TestExecutionListeners for class [%s].", descriptor.getRootDeclaringClass().getName()));
150152
}
151153
usingDefaults = true;
152-
classesList.addAll(getDefaultTestExecutionListenerClasses());
154+
listeners.addAll(getDefaultTestExecutionListeners());
153155
}
154156

155-
classesList.addAll(0, Arrays.asList(testExecutionListeners.listeners()));
157+
listeners.addAll(0, instantiateListeners(testExecutionListeners.listeners()));
156158

157159
descriptor = (inheritListeners ? parentDescriptor : null);
158160
}
159161
}
160162

161-
Collection<Class<? extends TestExecutionListener>> classesToUse = classesList;
162-
// Remove possible duplicates if we loaded default listeners.
163163
if (usingDefaults) {
164-
classesToUse = new LinkedHashSet<>(classesList);
165-
}
164+
// Remove possible duplicates if we loaded default listeners.
165+
List<TestExecutionListener> uniqueListeners = new ArrayList<>(listeners.size());
166+
listeners.forEach(listener -> {
167+
Class<? extends TestExecutionListener> listenerClass = listener.getClass();
168+
if (uniqueListeners.stream().map(Object::getClass).noneMatch(listenerClass::equals)) {
169+
uniqueListeners.add(listener);
170+
}
171+
});
172+
listeners = uniqueListeners;
166173

167-
List<TestExecutionListener> listeners = instantiateListeners(classesToUse);
168-
// Sort by Ordered/@Order if we loaded default listeners.
169-
if (usingDefaults) {
174+
// Sort by Ordered/@Order if we loaded default listeners.
170175
AnnotationAwareOrderComparator.sort(listeners);
171176
}
172177

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

179-
private List<TestExecutionListener> instantiateListeners(Collection<Class<? extends TestExecutionListener>> classes) {
180-
List<TestExecutionListener> listeners = new ArrayList<>(classes.size());
184+
/**
185+
* Get the default {@link TestExecutionListener TestExecutionListeners} for
186+
* this bootstrapper.
187+
* <p>This method is invoked by {@link #getTestExecutionListeners()}.
188+
* <p>The default implementation looks up and instantiates all
189+
* {@code org.springframework.test.context.TestExecutionListener} entries
190+
* configured in all {@code META-INF/spring.factories} files on the classpath.
191+
* <p>If a particular listener cannot be loaded due to a {@link LinkageError}
192+
* or {@link ClassNotFoundException}, a {@code DEBUG} message will be logged,
193+
* but the associated exception will not be rethrown. A {@link RuntimeException}
194+
* or any other {@link Error} will be rethrown. Any other exception will be
195+
* thrown wrapped in an {@link IllegalStateException}.
196+
* @return an <em>unmodifiable</em> list of default {@code TestExecutionListener}
197+
* instances
198+
* @since 6.0
199+
* @see SpringFactoriesLoader#forDefaultResourceLocation()
200+
* @see SpringFactoriesLoader#load(Class, FailureHandler)
201+
*/
202+
protected List<TestExecutionListener> getDefaultTestExecutionListeners() {
203+
FailureHandler failureHandler = (factoryType, factoryImplementationName, ex) -> {
204+
if (ex instanceof LinkageError || ex instanceof ClassNotFoundException) {
205+
if (logger.isDebugEnabled()) {
206+
logger.debug("Could not load default TestExecutionListener [" + factoryImplementationName +
207+
"]. Specify custom listener classes or make the default listener classes available.", ex);
208+
}
209+
}
210+
else {
211+
if (ex instanceof RuntimeException runtimeException) {
212+
throw runtimeException;
213+
}
214+
if (ex instanceof Error error) {
215+
throw error;
216+
}
217+
throw new IllegalStateException("Failed to load default TestExecutionListener [" +
218+
factoryImplementationName + "].", ex);
219+
}
220+
};
221+
222+
List<TestExecutionListener> listeners = SpringFactoriesLoader.forDefaultResourceLocation()
223+
.load(TestExecutionListener.class, failureHandler);
224+
225+
if (logger.isInfoEnabled()) {
226+
List<String> classNames = listeners.stream()
227+
.map(listener -> listener.getClass().getName())
228+
.collect(Collectors.toList());
229+
logger.info(String.format("Loaded default TestExecutionListener implementations from location [%s]: %s",
230+
SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames));
231+
}
232+
233+
return Collections.unmodifiableList(listeners);
234+
}
235+
236+
@SuppressWarnings("unchecked")
237+
private List<TestExecutionListener> instantiateListeners(Class<? extends TestExecutionListener>... classes) {
238+
List<TestExecutionListener> listeners = new ArrayList<>(classes.length);
181239
for (Class<? extends TestExecutionListener> listenerClass : classes) {
182240
try {
183241
listeners.add(BeanUtils.instantiateClass(listenerClass));
@@ -201,53 +259,6 @@ private List<TestExecutionListener> instantiateListeners(Collection<Class<? exte
201259
return listeners;
202260
}
203261

204-
/**
205-
* Get the default {@link TestExecutionListener} classes for this bootstrapper.
206-
* <p>This method is invoked by {@link #getTestExecutionListeners()} and
207-
* delegates to {@link #getDefaultTestExecutionListenerClassNames()} to
208-
* retrieve the class names.
209-
* <p>If a particular class cannot be loaded, a {@code DEBUG} message will
210-
* be logged, but the associated exception will not be rethrown.
211-
*/
212-
@SuppressWarnings("unchecked")
213-
protected Set<Class<? extends TestExecutionListener>> getDefaultTestExecutionListenerClasses() {
214-
Set<Class<? extends TestExecutionListener>> defaultListenerClasses = new LinkedHashSet<>();
215-
ClassLoader cl = getClass().getClassLoader();
216-
for (String className : getDefaultTestExecutionListenerClassNames()) {
217-
try {
218-
defaultListenerClasses.add((Class<? extends TestExecutionListener>) ClassUtils.forName(className, cl));
219-
}
220-
catch (Throwable ex) {
221-
if (logger.isDebugEnabled()) {
222-
logger.debug("Could not load default TestExecutionListener class [" + className +
223-
"]. Specify custom listener classes or make the default listener classes available.", ex);
224-
}
225-
}
226-
}
227-
return defaultListenerClasses;
228-
}
229-
230-
/**
231-
* Get the names of the default {@link TestExecutionListener} classes for
232-
* this bootstrapper.
233-
* <p>The default implementation looks up all
234-
* {@code org.springframework.test.context.TestExecutionListener} entries
235-
* configured in all {@code META-INF/spring.factories} files on the classpath.
236-
* <p>This method is invoked by {@link #getDefaultTestExecutionListenerClasses()}.
237-
* @return an <em>unmodifiable</em> list of names of default {@code TestExecutionListener}
238-
* classes
239-
* @see SpringFactoriesLoader#loadFactoryNames
240-
*/
241-
protected List<String> getDefaultTestExecutionListenerClassNames() {
242-
List<String> classNames =
243-
SpringFactoriesLoader.loadFactoryNames(TestExecutionListener.class, getClass().getClassLoader());
244-
if (logger.isInfoEnabled()) {
245-
logger.info(String.format("Loaded default TestExecutionListener class names from location [%s]: %s",
246-
SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames));
247-
}
248-
return Collections.unmodifiableList(classNames);
249-
}
250-
251262
/**
252263
* {@inheritDoc}
253264
*/

0 commit comments

Comments
 (0)