Skip to content

Commit c21bd66

Browse files
committed
Introduce TestContextSpringFactoriesUtils to reduce code duplication
1 parent a4d36a8 commit c21bd66

File tree

5 files changed

+173
-153
lines changed

5 files changed

+173
-153
lines changed

spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ default boolean isContextLoaded(MergedContextConfiguration mergedContextConfigur
7171
* {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader}
7272
* mechanism, catch any exception thrown by the {@link ContextLoader}, and
7373
* delegate to each of the configured failure processors to process the context
74-
* load failure if the thrown exception is an instance of {@link ContextLoadException}.
74+
* load failure if the exception is an instance of {@link ContextLoadException}.
7575
* <p>The cache statistics should be logged by invoking
7676
* {@link org.springframework.test.context.cache.ContextCache#logStatistics()}.
7777
* @param mergedContextConfiguration the merged context configuration to use

spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java

Lines changed: 6 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
package org.springframework.test.context.cache;
1818

19-
import java.lang.reflect.InvocationTargetException;
20-
import java.util.Collection;
2119
import java.util.List;
2220

2321
import org.apache.commons.logging.Log;
@@ -27,7 +25,6 @@
2725
import org.springframework.context.ApplicationContextInitializer;
2826
import org.springframework.context.ConfigurableApplicationContext;
2927
import org.springframework.context.support.GenericApplicationContext;
30-
import org.springframework.core.io.support.SpringFactoriesLoader;
3128
import org.springframework.lang.Nullable;
3229
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
3330
import org.springframework.test.context.ApplicationContextFailureProcessor;
@@ -39,6 +36,7 @@
3936
import org.springframework.test.context.aot.AotContextLoader;
4037
import org.springframework.test.context.aot.AotTestContextInitializers;
4138
import org.springframework.test.context.aot.TestContextAotException;
39+
import org.springframework.test.context.util.TestContextSpringFactoriesUtils;
4240
import org.springframework.util.Assert;
4341

4442
/**
@@ -50,9 +48,9 @@
5048
* and provide a custom {@link ContextCache} implementation.
5149
*
5250
* <p>As of Spring Framework 6.0, this class loads {@link ApplicationContextFailureProcessor}
53-
* implementations via the {@link SpringFactoriesLoader} mechanism and delegates to
54-
* them in {@link #loadContext(MergedContextConfiguration)} to process context
55-
* load failures.
51+
* implementations via the {@link org.springframework.core.io.support.SpringFactoriesLoader
52+
* SpringFactoriesLoader} mechanism and delegates to them in
53+
* {@link #loadContext(MergedContextConfiguration)} to process context load failures.
5654
*
5755
* @author Sam Brannen
5856
* @since 4.1
@@ -67,8 +65,8 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
6765
*/
6866
static final ContextCache defaultContextCache = new DefaultContextCache();
6967

70-
private List<ApplicationContextFailureProcessor> contextFailureProcessors =
71-
loadApplicationContextFailureProcessors();
68+
private List<ApplicationContextFailureProcessor> contextFailureProcessors = TestContextSpringFactoriesUtils
69+
.loadFactoryImplementations(ApplicationContextFailureProcessor.class);
7270

7371
private final AotTestContextInitializers aotTestContextInitializers = new AotTestContextInitializers();
7472

@@ -254,71 +252,4 @@ private MergedContextConfiguration replaceIfNecessary(MergedContextConfiguration
254252
return mergedConfig;
255253
}
256254

257-
/**
258-
* Get the {@link ApplicationContextFailureProcessor} implementations to use,
259-
* loaded via the {@link SpringFactoriesLoader} mechanism.
260-
* @return the context failure processors to use
261-
* @since 6.0
262-
*/
263-
private static List<ApplicationContextFailureProcessor> loadApplicationContextFailureProcessors() {
264-
SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(
265-
DefaultCacheAwareContextLoaderDelegate.class.getClassLoader());
266-
List<ApplicationContextFailureProcessor> processors = loader.load(ApplicationContextFailureProcessor.class,
267-
DefaultCacheAwareContextLoaderDelegate::handleInstantiationFailure);
268-
if (logger.isTraceEnabled()) {
269-
logger.trace("Loaded default ApplicationContextFailureProcessor implementations from location [%s]: %s"
270-
.formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames(processors)));
271-
}
272-
else if (logger.isDebugEnabled()) {
273-
logger.debug("Loaded default ApplicationContextFailureProcessor implementations from location [%s]: %s"
274-
.formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classSimpleNames(processors)));
275-
}
276-
return processors;
277-
}
278-
279-
private static void handleInstantiationFailure(
280-
Class<?> factoryType, String factoryImplementationName, Throwable failure) {
281-
282-
Throwable ex = (failure instanceof InvocationTargetException ite ?
283-
ite.getTargetException() : failure);
284-
if (ex instanceof ClassNotFoundException || ex instanceof NoClassDefFoundError) {
285-
logSkippedComponent(factoryType, factoryImplementationName, ex);
286-
}
287-
else if (ex instanceof LinkageError) {
288-
if (logger.isDebugEnabled()) {
289-
logger.debug("""
290-
Could not load %1$s [%2$s]. Specify custom %1$s classes or make the default %1$s classes \
291-
available.""".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
292-
}
293-
}
294-
else {
295-
if (ex instanceof RuntimeException runtimeException) {
296-
throw runtimeException;
297-
}
298-
if (ex instanceof Error error) {
299-
throw error;
300-
}
301-
throw new IllegalStateException(
302-
"Failed to load %s [%s].".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
303-
}
304-
}
305-
306-
private static void logSkippedComponent(Class<?> factoryType, String factoryImplementationName, Throwable ex) {
307-
if (logger.isDebugEnabled()) {
308-
logger.debug("""
309-
Skipping candidate %1$s [%2$s] due to a missing dependency. \
310-
Specify custom %1$s classes or make the default %1$s classes \
311-
and their required dependencies available. Offending class: [%3$s]"""
312-
.formatted(factoryType.getSimpleName(), factoryImplementationName, ex.getMessage()));
313-
}
314-
}
315-
316-
private static List<String> classNames(Collection<?> components) {
317-
return components.stream().map(Object::getClass).map(Class::getName).toList();
318-
}
319-
320-
private static List<String> classSimpleNames(Collection<?> components) {
321-
return components.stream().map(Object::getClass).map(Class::getSimpleName).toList();
322-
}
323-
324255
}

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

Lines changed: 15 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.test.context.support;
1818

19-
import java.lang.reflect.InvocationTargetException;
2019
import java.util.ArrayList;
2120
import java.util.Arrays;
2221
import java.util.Collection;
@@ -32,8 +31,6 @@
3231
import org.springframework.beans.BeanInstantiationException;
3332
import org.springframework.beans.BeanUtils;
3433
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
35-
import org.springframework.core.io.support.SpringFactoriesLoader;
36-
import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
3734
import org.springframework.lang.Nullable;
3835
import org.springframework.test.context.BootstrapContext;
3936
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
@@ -52,6 +49,7 @@
5249
import org.springframework.test.context.TestExecutionListener;
5350
import org.springframework.test.context.TestExecutionListeners;
5451
import org.springframework.test.context.TestExecutionListeners.MergeMode;
52+
import org.springframework.test.context.util.TestContextSpringFactoriesUtils;
5553
import org.springframework.util.Assert;
5654
import org.springframework.util.ClassUtils;
5755
import org.springframework.util.StringUtils;
@@ -189,30 +187,15 @@ else if (logger.isDebugEnabled()) {
189187
/**
190188
* Get the default {@link TestExecutionListener TestExecutionListeners} for
191189
* this bootstrapper.
190+
* <p>The default implementation delegates to
191+
* {@link TestContextSpringFactoriesUtils#loadFactoryImplementations(Class)}.
192192
* <p>This method is invoked by {@link #getTestExecutionListeners()}.
193-
* <p>The default implementation looks up and instantiates all
194-
* {@code org.springframework.test.context.TestExecutionListener} entries
195-
* configured in all {@code META-INF/spring.factories} files on the classpath.
196-
* <p>If a particular listener cannot be loaded due to a {@link LinkageError}
197-
* or {@link ClassNotFoundException}, a {@code DEBUG} message will be logged,
198-
* but the associated exception will not be rethrown. A {@link RuntimeException}
199-
* or any other {@link Error} will be rethrown. Any other exception will be
200-
* thrown wrapped in an {@link IllegalStateException}.
201193
* @return an <em>unmodifiable</em> list of default {@code TestExecutionListener}
202194
* instances
203195
* @since 6.0
204-
* @see SpringFactoriesLoader#forDefaultResourceLocation()
205-
* @see SpringFactoriesLoader#load(Class, FailureHandler)
206196
*/
207197
protected List<TestExecutionListener> getDefaultTestExecutionListeners() {
208-
SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(getClass().getClassLoader());
209-
List<TestExecutionListener> listeners =
210-
loader.load(TestExecutionListener.class, this::handleInstantiationFailure);
211-
if (logger.isTraceEnabled()) {
212-
logger.trace("Loaded default TestExecutionListener implementations from location [%s]: %s"
213-
.formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames(listeners)));
214-
}
215-
return Collections.unmodifiableList(listeners);
198+
return TestContextSpringFactoriesUtils.loadFactoryImplementations(TestExecutionListener.class);
216199
}
217200

218201
@SuppressWarnings("unchecked")
@@ -225,7 +208,14 @@ private List<TestExecutionListener> instantiateListeners(Class<? extends TestExe
225208
catch (BeanInstantiationException ex) {
226209
Throwable cause = ex.getCause();
227210
if (cause instanceof ClassNotFoundException || cause instanceof NoClassDefFoundError) {
228-
logSkippedComponent(TestExecutionListener.class, listenerClass.getName(), cause);
211+
if (logger.isDebugEnabled()) {
212+
logger.debug("""
213+
Skipping candidate %1$s [%2$s] due to a missing dependency. \
214+
Specify custom %1$s classes or make the default %1$s classes \
215+
and their required dependencies available. Offending class: [%3$s]"""
216+
.formatted(TestExecutionListener.class.getSimpleName(), listenerClass.getName(),
217+
cause.getMessage()));
218+
}
229219
}
230220
else {
231221
throw ex;
@@ -409,21 +399,12 @@ else if (logger.isDebugEnabled()) {
409399

410400
/**
411401
* Get the {@link ContextCustomizerFactory} instances for this bootstrapper.
412-
* <p>The default implementation uses the {@link SpringFactoriesLoader} mechanism
413-
* for loading factories configured in all {@code META-INF/spring.factories}
414-
* files on the classpath.
402+
* <p>The default implementation delegates to
403+
* {@link TestContextSpringFactoriesUtils#loadFactoryImplementations(Class)}.
415404
* @since 4.3
416-
* @see SpringFactoriesLoader#loadFactories
417405
*/
418406
protected List<ContextCustomizerFactory> getContextCustomizerFactories() {
419-
SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(getClass().getClassLoader());
420-
List<ContextCustomizerFactory> factories =
421-
loader.load(ContextCustomizerFactory.class, this::handleInstantiationFailure);
422-
if (logger.isTraceEnabled()) {
423-
logger.trace("Loaded ContextCustomizerFactory implementations from location [%s]: %s"
424-
.formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames(factories)));
425-
}
426-
return factories;
407+
return TestContextSpringFactoriesUtils.loadFactoryImplementations(ContextCustomizerFactory.class);
427408
}
428409

429410
/**
@@ -552,53 +533,10 @@ protected MergedContextConfiguration processMergedContextConfiguration(MergedCon
552533
}
553534

554535

555-
private void handleInstantiationFailure(
556-
Class<?> factoryType, String factoryImplementationName, Throwable failure) {
557-
558-
Throwable ex = (failure instanceof InvocationTargetException ite ?
559-
ite.getTargetException() : failure);
560-
if (ex instanceof ClassNotFoundException || ex instanceof NoClassDefFoundError) {
561-
logSkippedComponent(factoryType, factoryImplementationName, ex);
562-
}
563-
else if (ex instanceof LinkageError) {
564-
if (logger.isDebugEnabled()) {
565-
logger.debug("""
566-
Could not load %1$s [%2$s]. Specify custom %1$s classes or make the default %1$s classes \
567-
available.""".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
568-
}
569-
}
570-
else {
571-
if (ex instanceof RuntimeException runtimeException) {
572-
throw runtimeException;
573-
}
574-
if (ex instanceof Error error) {
575-
throw error;
576-
}
577-
throw new IllegalStateException(
578-
"Failed to load %s [%s].".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
579-
}
580-
}
581-
582-
private void logSkippedComponent(Class<?> factoryType, String factoryImplementationName, Throwable ex) {
583-
// TestExecutionListener/ContextCustomizerFactory not applicable due to a missing dependency
584-
if (logger.isDebugEnabled()) {
585-
logger.debug("""
586-
Skipping candidate %1$s [%2$s] due to a missing dependency. \
587-
Specify custom %1$s classes or make the default %1$s classes \
588-
and their required dependencies available. Offending class: [%3$s]"""
589-
.formatted(factoryType.getSimpleName(), factoryImplementationName, ex.getMessage()));
590-
}
591-
}
592-
593-
594536
private static List<String> classSimpleNames(Collection<?> components) {
595537
return components.stream().map(Object::getClass).map(Class::getSimpleName).toList();
596538
}
597539

598-
private static List<String> classNames(Collection<?> components) {
599-
return components.stream().map(Object::getClass).map(Class::getName).toList();
600-
}
601-
602540
private static boolean areAllEmpty(Collection<?>... collections) {
603541
return Arrays.stream(collections).allMatch(Collection::isEmpty);
604542
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.util;
18+
19+
import java.lang.reflect.InvocationTargetException;
20+
21+
import org.apache.commons.logging.Log;
22+
import org.apache.commons.logging.LogFactory;
23+
24+
import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
25+
26+
/**
27+
* Spring factories {@link FailureHandler} used within the <em>Spring TestContext
28+
* Framework</em>.
29+
*
30+
* @author Sam Brannen
31+
* @since 6.0
32+
*/
33+
class TestContextFailureHandler implements FailureHandler {
34+
35+
private final Log logger = LogFactory.getLog(TestContextSpringFactoriesUtils.class);
36+
37+
@Override
38+
public void handleFailure(Class<?> factoryType, String factoryImplementationName, Throwable failure) {
39+
Throwable ex = (failure instanceof InvocationTargetException ite ? ite.getTargetException() : failure);
40+
if (ex instanceof ClassNotFoundException || ex instanceof NoClassDefFoundError) {
41+
if (logger.isDebugEnabled()) {
42+
logger.debug("""
43+
Skipping candidate %1$s [%2$s] due to a missing dependency. \
44+
Specify custom %1$s classes or make the default %1$s classes \
45+
and their required dependencies available. Offending class: [%3$s]"""
46+
.formatted(factoryType.getSimpleName(), factoryImplementationName, ex.getMessage()));
47+
}
48+
}
49+
else if (ex instanceof LinkageError) {
50+
if (logger.isDebugEnabled()) {
51+
logger.debug("""
52+
Could not load %1$s [%2$s]. Specify custom %1$s classes or make the default %1$s classes \
53+
available.""".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
54+
}
55+
}
56+
else {
57+
if (ex instanceof RuntimeException runtimeException) {
58+
throw runtimeException;
59+
}
60+
if (ex instanceof Error error) {
61+
throw error;
62+
}
63+
throw new IllegalStateException(
64+
"Failed to load %s [%s]".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
65+
}
66+
}
67+
68+
}

0 commit comments

Comments
 (0)