diff --git a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java
index 580b8fa2ff2..0a5827713ae 100644
--- a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java
+++ b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java
@@ -71,7 +71,7 @@ public interface CacheAwareContextLoaderDelegate {
* {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader}
* mechanism, catch any exception thrown by the {@link ContextLoader}, and
* delegate to each of the configured failure processors to process the context
- * load failure if the thrown exception is an instance of {@link ContextLoadException}.
+ * load failure if the exception is an instance of {@link ContextLoadException}.
*
The cache statistics should be logged by invoking
* {@link org.springframework.test.context.cache.ContextCache#logStatistics()}.
* @param mergedContextConfiguration the merged context configuration to use
diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java
index 0e1d421381b..c41719720f9 100644
--- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java
+++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java
@@ -16,8 +16,6 @@
package org.springframework.test.context.cache;
-import java.lang.reflect.InvocationTargetException;
-import java.util.Collection;
import java.util.List;
import org.apache.commons.logging.Log;
@@ -27,7 +25,6 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
-import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.lang.Nullable;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.ApplicationContextFailureProcessor;
@@ -39,6 +36,7 @@ import org.springframework.test.context.SmartContextLoader;
import org.springframework.test.context.aot.AotContextLoader;
import org.springframework.test.context.aot.AotTestContextInitializers;
import org.springframework.test.context.aot.TestContextAotException;
+import org.springframework.test.context.util.TestContextSpringFactoriesUtils;
import org.springframework.util.Assert;
/**
@@ -50,9 +48,9 @@ import org.springframework.util.Assert;
* and provide a custom {@link ContextCache} implementation.
*
*
As of Spring Framework 6.0, this class loads {@link ApplicationContextFailureProcessor}
- * implementations via the {@link SpringFactoriesLoader} mechanism and delegates to
- * them in {@link #loadContext(MergedContextConfiguration)} to process context
- * load failures.
+ * implementations via the {@link org.springframework.core.io.support.SpringFactoriesLoader
+ * SpringFactoriesLoader} mechanism and delegates to them in
+ * {@link #loadContext(MergedContextConfiguration)} to process context load failures.
*
* @author Sam Brannen
* @since 4.1
@@ -67,8 +65,8 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
*/
static final ContextCache defaultContextCache = new DefaultContextCache();
- private List contextFailureProcessors =
- loadApplicationContextFailureProcessors();
+ private List contextFailureProcessors = TestContextSpringFactoriesUtils
+ .loadFactoryImplementations(ApplicationContextFailureProcessor.class);
private final AotTestContextInitializers aotTestContextInitializers = new AotTestContextInitializers();
@@ -254,71 +252,4 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
return mergedConfig;
}
- /**
- * Get the {@link ApplicationContextFailureProcessor} implementations to use,
- * loaded via the {@link SpringFactoriesLoader} mechanism.
- * @return the context failure processors to use
- * @since 6.0
- */
- private static List loadApplicationContextFailureProcessors() {
- SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(
- DefaultCacheAwareContextLoaderDelegate.class.getClassLoader());
- List processors = loader.load(ApplicationContextFailureProcessor.class,
- DefaultCacheAwareContextLoaderDelegate::handleInstantiationFailure);
- if (logger.isTraceEnabled()) {
- logger.trace("Loaded default ApplicationContextFailureProcessor implementations from location [%s]: %s"
- .formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames(processors)));
- }
- else if (logger.isDebugEnabled()) {
- logger.debug("Loaded default ApplicationContextFailureProcessor implementations from location [%s]: %s"
- .formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classSimpleNames(processors)));
- }
- return processors;
- }
-
- private static void handleInstantiationFailure(
- Class> factoryType, String factoryImplementationName, Throwable failure) {
-
- Throwable ex = (failure instanceof InvocationTargetException ite ?
- ite.getTargetException() : failure);
- if (ex instanceof ClassNotFoundException || ex instanceof NoClassDefFoundError) {
- logSkippedComponent(factoryType, factoryImplementationName, ex);
- }
- else if (ex instanceof LinkageError) {
- if (logger.isDebugEnabled()) {
- logger.debug("""
- Could not load %1$s [%2$s]. Specify custom %1$s classes or make the default %1$s classes \
- available.""".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
- }
- }
- else {
- if (ex instanceof RuntimeException runtimeException) {
- throw runtimeException;
- }
- if (ex instanceof Error error) {
- throw error;
- }
- throw new IllegalStateException(
- "Failed to load %s [%s].".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
- }
- }
-
- private static void logSkippedComponent(Class> factoryType, String factoryImplementationName, Throwable ex) {
- if (logger.isDebugEnabled()) {
- logger.debug("""
- Skipping candidate %1$s [%2$s] due to a missing dependency. \
- Specify custom %1$s classes or make the default %1$s classes \
- and their required dependencies available. Offending class: [%3$s]"""
- .formatted(factoryType.getSimpleName(), factoryImplementationName, ex.getMessage()));
- }
- }
-
- private static List classNames(Collection> components) {
- return components.stream().map(Object::getClass).map(Class::getName).toList();
- }
-
- private static List classSimpleNames(Collection> components) {
- return components.stream().map(Object::getClass).map(Class::getSimpleName).toList();
- }
-
}
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 40b1f160810..ba0855ff5d7 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
@@ -16,7 +16,6 @@
package org.springframework.test.context.support;
-import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -32,8 +31,6 @@ import org.apache.commons.logging.LogFactory;
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;
@@ -52,6 +49,7 @@ import org.springframework.test.context.TestContextBootstrapper;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.TestExecutionListeners.MergeMode;
+import org.springframework.test.context.util.TestContextSpringFactoriesUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@@ -189,30 +187,15 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
/**
* Get the default {@link TestExecutionListener TestExecutionListeners} for
* this bootstrapper.
+ * The default implementation delegates to
+ * {@link TestContextSpringFactoriesUtils#loadFactoryImplementations(Class)}.
*
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() {
- SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(getClass().getClassLoader());
- List listeners =
- loader.load(TestExecutionListener.class, this::handleInstantiationFailure);
- if (logger.isTraceEnabled()) {
- logger.trace("Loaded default TestExecutionListener implementations from location [%s]: %s"
- .formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames(listeners)));
- }
- return Collections.unmodifiableList(listeners);
+ return TestContextSpringFactoriesUtils.loadFactoryImplementations(TestExecutionListener.class);
}
@SuppressWarnings("unchecked")
@@ -225,7 +208,14 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
catch (BeanInstantiationException ex) {
Throwable cause = ex.getCause();
if (cause instanceof ClassNotFoundException || cause instanceof NoClassDefFoundError) {
- logSkippedComponent(TestExecutionListener.class, listenerClass.getName(), cause);
+ if (logger.isDebugEnabled()) {
+ logger.debug("""
+ Skipping candidate %1$s [%2$s] due to a missing dependency. \
+ Specify custom %1$s classes or make the default %1$s classes \
+ and their required dependencies available. Offending class: [%3$s]"""
+ .formatted(TestExecutionListener.class.getSimpleName(), listenerClass.getName(),
+ cause.getMessage()));
+ }
}
else {
throw ex;
@@ -409,21 +399,12 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
/**
* Get the {@link ContextCustomizerFactory} instances for this bootstrapper.
- * The default implementation uses the {@link SpringFactoriesLoader} mechanism
- * for loading factories configured in all {@code META-INF/spring.factories}
- * files on the classpath.
+ *
The default implementation delegates to
+ * {@link TestContextSpringFactoriesUtils#loadFactoryImplementations(Class)}.
* @since 4.3
- * @see SpringFactoriesLoader#loadFactories
*/
protected List getContextCustomizerFactories() {
- SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(getClass().getClassLoader());
- List factories =
- loader.load(ContextCustomizerFactory.class, this::handleInstantiationFailure);
- if (logger.isTraceEnabled()) {
- logger.trace("Loaded ContextCustomizerFactory implementations from location [%s]: %s"
- .formatted(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames(factories)));
- }
- return factories;
+ return TestContextSpringFactoriesUtils.loadFactoryImplementations(ContextCustomizerFactory.class);
}
/**
@@ -552,53 +533,10 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
}
- private void handleInstantiationFailure(
- Class> factoryType, String factoryImplementationName, Throwable failure) {
-
- Throwable ex = (failure instanceof InvocationTargetException ite ?
- ite.getTargetException() : failure);
- if (ex instanceof ClassNotFoundException || ex instanceof NoClassDefFoundError) {
- logSkippedComponent(factoryType, factoryImplementationName, ex);
- }
- else if (ex instanceof LinkageError) {
- if (logger.isDebugEnabled()) {
- logger.debug("""
- Could not load %1$s [%2$s]. Specify custom %1$s classes or make the default %1$s classes \
- available.""".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
- }
- }
- else {
- if (ex instanceof RuntimeException runtimeException) {
- throw runtimeException;
- }
- if (ex instanceof Error error) {
- throw error;
- }
- throw new IllegalStateException(
- "Failed to load %s [%s].".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
- }
- }
-
- private void logSkippedComponent(Class> factoryType, String factoryImplementationName, Throwable ex) {
- // TestExecutionListener/ContextCustomizerFactory not applicable due to a missing dependency
- if (logger.isDebugEnabled()) {
- logger.debug("""
- Skipping candidate %1$s [%2$s] due to a missing dependency. \
- Specify custom %1$s classes or make the default %1$s classes \
- and their required dependencies available. Offending class: [%3$s]"""
- .formatted(factoryType.getSimpleName(), factoryImplementationName, ex.getMessage()));
- }
- }
-
-
private static List classSimpleNames(Collection> components) {
return components.stream().map(Object::getClass).map(Class::getSimpleName).toList();
}
- private static List classNames(Collection> components) {
- return components.stream().map(Object::getClass).map(Class::getName).toList();
- }
-
private static boolean areAllEmpty(Collection>... collections) {
return Arrays.stream(collections).allMatch(Collection::isEmpty);
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/util/TestContextFailureHandler.java b/spring-test/src/main/java/org/springframework/test/context/util/TestContextFailureHandler.java
new file mode 100644
index 00000000000..0fbe8e4795b
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/util/TestContextFailureHandler.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.util;
+
+import java.lang.reflect.InvocationTargetException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
+
+/**
+ * Spring factories {@link FailureHandler} used within the Spring TestContext
+ * Framework.
+ *
+ * @author Sam Brannen
+ * @since 6.0
+ */
+class TestContextFailureHandler implements FailureHandler {
+
+ private final Log logger = LogFactory.getLog(TestContextSpringFactoriesUtils.class);
+
+ @Override
+ public void handleFailure(Class> factoryType, String factoryImplementationName, Throwable failure) {
+ Throwable ex = (failure instanceof InvocationTargetException ite ? ite.getTargetException() : failure);
+ if (ex instanceof ClassNotFoundException || ex instanceof NoClassDefFoundError) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("""
+ Skipping candidate %1$s [%2$s] due to a missing dependency. \
+ Specify custom %1$s classes or make the default %1$s classes \
+ and their required dependencies available. Offending class: [%3$s]"""
+ .formatted(factoryType.getSimpleName(), factoryImplementationName, ex.getMessage()));
+ }
+ }
+ else if (ex instanceof LinkageError) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("""
+ Could not load %1$s [%2$s]. Specify custom %1$s classes or make the default %1$s classes \
+ available.""".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
+ }
+ }
+ else {
+ if (ex instanceof RuntimeException runtimeException) {
+ throw runtimeException;
+ }
+ if (ex instanceof Error error) {
+ throw error;
+ }
+ throw new IllegalStateException(
+ "Failed to load %s [%s]".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/context/util/TestContextSpringFactoriesUtils.java b/spring-test/src/main/java/org/springframework/test/context/util/TestContextSpringFactoriesUtils.java
new file mode 100644
index 00000000000..7f67cf6e407
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/util/TestContextSpringFactoriesUtils.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.util;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.io.support.SpringFactoriesLoader;
+import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
+
+import static org.springframework.core.io.support.SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION;
+
+/**
+ * Collection of utilities for working with {@link SpringFactoriesLoader} within
+ * the Spring TestContext Framework.
+ *
+ * Primarily intended for use within the TestContext framework.
+ *
+ * @author Sam Brannen
+ * @since 6.0
+ */
+public abstract class TestContextSpringFactoriesUtils {
+
+ private static final Log logger = LogFactory.getLog(TestContextSpringFactoriesUtils.class);
+
+
+ private TestContextSpringFactoriesUtils() {
+ // no-op
+ }
+
+
+ /**
+ * Load factory implementations of the given type via the
+ * {@link SpringFactoriesLoader} mechanism.
+ *
This method utilizes a custom {@link FailureHandler} and DEBUG/TRACE logging
+ * that are specific to the needs of the Spring TestContext Framework.
+ *
Specifically, this method looks up and instantiates all {@code factoryType}
+ * entries configured in all {@code META-INF/spring.factories} files on the classpath.
+ *
If a particular factory implementation 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}.
+ * @param the factory type
+ * @param factoryType the interface or abstract class representing the factory
+ * @return an unmodifiable list of factory implementations
+ * @see SpringFactoriesLoader#forDefaultResourceLocation(ClassLoader)
+ * @see SpringFactoriesLoader#load(Class, org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler)
+ */
+ public static List loadFactoryImplementations(Class factoryType) {
+ SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(
+ TestContextSpringFactoriesUtils.class.getClassLoader());
+ List implementations = loader.load(factoryType, new TestContextFailureHandler());
+ if (logger.isTraceEnabled()) {
+ logger.trace("Loaded %s implementations from location [%s]: %s"
+ .formatted(factoryType.getSimpleName(), FACTORIES_RESOURCE_LOCATION, classNames(implementations)));
+ }
+ return Collections.unmodifiableList(implementations);
+ }
+
+ private static List classNames(Collection> components) {
+ return components.stream().map(Object::getClass).map(Class::getName).toList();
+ }
+
+}