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 d7086c3ae8..57ab6d7019 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
@@ -101,18 +101,4 @@ public interface CacheAwareContextLoaderDelegate {
*/
void closeContext(MergedContextConfiguration mergedContextConfiguration, @Nullable HierarchyMode hierarchyMode);
- /**
- * Set the {@link ApplicationContextFailureProcessor} to use.
- *
The default implementation ignores the supplied processor.
- *
Concrete implementations should override this method to store a reference
- * to the supplied processor and use it to process {@link ContextLoadException
- * ContextLoadExceptions} thrown from context loaders in
- * {@link #loadContext(MergedContextConfiguration)}.
- * @param contextFailureProcessor the context failure processor to use
- * @since 6.0
- */
- default void setContextFailureProcessor(ApplicationContextFailureProcessor contextFailureProcessor) {
- // no-op
- }
-
}
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 fea89f7462..f0a94eb71d 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,6 +16,10 @@
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;
import org.apache.commons.logging.LogFactory;
@@ -23,6 +27,7 @@ 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;
@@ -44,6 +49,11 @@ import org.springframework.util.Assert;
* invoke the {@link #DefaultCacheAwareContextLoaderDelegate(ContextCache)}
* 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.
+ *
* @author Sam Brannen
* @since 4.1
*/
@@ -51,6 +61,9 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
private static final Log logger = LogFactory.getLog(DefaultCacheAwareContextLoaderDelegate.class);
+ private static List contextFailureProcessors =
+ getApplicationContextFailureProcessors();
+
/**
* Default static cache of Spring application contexts.
*/
@@ -60,9 +73,6 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
private final ContextCache contextCache;
- @Nullable
- private ApplicationContextFailureProcessor contextFailureProcessor;
-
/**
* Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using
@@ -117,14 +127,14 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
Throwable cause = ex;
if (ex instanceof ContextLoadException cle) {
cause = cle.getCause();
- if (this.contextFailureProcessor != null) {
+ for (ApplicationContextFailureProcessor contextFailureProcessor : contextFailureProcessors) {
try {
- this.contextFailureProcessor.processLoadFailure(cle.getApplicationContext(), cause);
+ contextFailureProcessor.processLoadFailure(cle.getApplicationContext(), cause);
}
catch (Throwable throwable) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring exception thrown from ApplicationContextFailureProcessor [%s]: %s"
- .formatted(this.contextFailureProcessor, throwable));
+ .formatted(contextFailureProcessor, throwable));
}
}
}
@@ -153,12 +163,6 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
}
}
- @Override
- public void setContextFailureProcessor(ApplicationContextFailureProcessor contextFailureProcessor) {
- this.contextFailureProcessor = contextFailureProcessor;
- }
-
-
/**
* Get the {@link ContextCache} used by this context loader delegate.
*/
@@ -236,6 +240,7 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
* unmodified.
* This allows for transparent {@link org.springframework.test.context.cache.ContextCache ContextCache}
* support for AOT-optimized application contexts.
+ * @since 6.0
*/
@SuppressWarnings("unchecked")
private MergedContextConfiguration replaceIfNecessary(MergedContextConfiguration mergedConfig) {
@@ -248,4 +253,71 @@ 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 getApplicationContextFailureProcessors() {
+ 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 1519df9cd8..40b1f16081 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
@@ -35,7 +35,6 @@ 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.ApplicationContextFailureProcessor;
import org.springframework.test.context.BootstrapContext;
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
import org.springframework.test.context.ContextConfiguration;
@@ -523,29 +522,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
* @see #getApplicationContextFailureProcessor()
*/
protected CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate() {
- CacheAwareContextLoaderDelegate delegate = getBootstrapContext().getCacheAwareContextLoaderDelegate();
- ApplicationContextFailureProcessor contextFailureProcessor = getApplicationContextFailureProcessor();
- if (contextFailureProcessor != null) {
- delegate.setContextFailureProcessor(contextFailureProcessor);
- }
- return delegate;
- }
-
- /**
- * Get the {@link ApplicationContextFailureProcessor} to use.
- * The default implementation returns {@code null}.
- *
Concrete subclasses may choose to override this method to provide an
- * {@code ApplicationContextFailureProcessor} that will be supplied to the
- * configured {@code CacheAwareContextLoaderDelegate} in
- * {@link #getCacheAwareContextLoaderDelegate()}.
- * @return the context failure processor to use, or {@code null} if no processor
- * should be used
- * @since 6.0
- * @see #getCacheAwareContextLoaderDelegate()
- */
- @Nullable
- protected ApplicationContextFailureProcessor getApplicationContextFailureProcessor() {
- return null;
+ return getBootstrapContext().getCacheAwareContextLoaderDelegate();
}
/**
diff --git a/spring-test/src/test/java/org/springframework/test/context/failures/ContextLoadFailureTests.java b/spring-test/src/test/java/org/springframework/test/context/failures/ContextLoadFailureTests.java
index bb40c7cf34..c7331dbbc4 100644
--- a/spring-test/src/test/java/org/springframework/test/context/failures/ContextLoadFailureTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/failures/ContextLoadFailureTests.java
@@ -16,9 +16,6 @@
package org.springframework.test.context.failures;
-import java.util.ArrayList;
-import java.util.List;
-
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -30,11 +27,9 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
-import org.springframework.test.context.ApplicationContextFailureProcessor;
-import org.springframework.test.context.BootstrapWith;
+import org.springframework.test.context.failures.TrackingApplicationContextFailureProcessor.LoadFailure;
import org.springframework.test.context.junit.jupiter.FailingTestCase;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
-import org.springframework.test.context.support.DefaultTestContextBootstrapper;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
@@ -47,18 +42,15 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass
*/
class ContextLoadFailureTests {
- static List loadFailures = new ArrayList<>();
-
-
@BeforeEach
@AfterEach
void clearFailures() {
- loadFailures.clear();
+ TrackingApplicationContextFailureProcessor.loadFailures.clear();
}
@Test
void customBootstrapperAppliesApplicationContextFailureProcessor() {
- assertThat(loadFailures).isEmpty();
+ assertThat(TrackingApplicationContextFailureProcessor.loadFailures).isEmpty();
EngineTestKit.engine("junit-jupiter")
.selectors(selectClass(ExplosiveContextTestCase.class))//
@@ -66,8 +58,8 @@ class ContextLoadFailureTests {
.testEvents()
.assertStatistics(stats -> stats.started(1).succeeded(0).failed(1));
- assertThat(loadFailures).hasSize(1);
- LoadFailure loadFailure = loadFailures.get(0);
+ assertThat(TrackingApplicationContextFailureProcessor.loadFailures).hasSize(1);
+ LoadFailure loadFailure = TrackingApplicationContextFailureProcessor.loadFailures.get(0);
assertThat(loadFailure.context()).isExactlyInstanceOf(GenericApplicationContext.class);
assertThat(loadFailure.exception())
.isInstanceOf(BeanCreationException.class)
@@ -78,7 +70,6 @@ class ContextLoadFailureTests {
@FailingTestCase
@SpringJUnitConfig
- @BootstrapWith(CustomTestContextBootstrapper.class)
static class ExplosiveContextTestCase {
@Test
@@ -96,14 +87,4 @@ class ContextLoadFailureTests {
}
}
- static class CustomTestContextBootstrapper extends DefaultTestContextBootstrapper {
-
- @Override
- protected ApplicationContextFailureProcessor getApplicationContextFailureProcessor() {
- return (context, exception) -> loadFailures.add(new LoadFailure(context, exception));
- }
- }
-
- record LoadFailure(ApplicationContext context, Throwable exception) {}
-
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/failures/TrackingApplicationContextFailureProcessor.java b/spring-test/src/test/java/org/springframework/test/context/failures/TrackingApplicationContextFailureProcessor.java
new file mode 100644
index 0000000000..45421e0c63
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/failures/TrackingApplicationContextFailureProcessor.java
@@ -0,0 +1,46 @@
+/*
+ * 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.failures;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.test.context.ApplicationContextFailureProcessor;
+
+/**
+ * Demo {@link ApplicationContextFailureProcessor} for tests that tracks
+ * {@linkplain LoadFailure load failures} that can be queried via
+ * {@link #loadFailures}.
+ *
+ * @author Sam Brannen
+ * @since 6.0
+ */
+public class TrackingApplicationContextFailureProcessor implements ApplicationContextFailureProcessor {
+
+ public static List loadFailures = Collections.synchronizedList(new ArrayList<>());
+
+
+ @Override
+ public void processLoadFailure(ApplicationContext context, Throwable exception) {
+ loadFailures.add(new LoadFailure(context, exception));
+ }
+
+ public record LoadFailure(ApplicationContext context, Throwable exception) {}
+
+}
diff --git a/spring-test/src/test/resources/META-INF/spring.factories b/spring-test/src/test/resources/META-INF/spring.factories
index b26a9de824..cd50c180c2 100644
--- a/spring-test/src/test/resources/META-INF/spring.factories
+++ b/spring-test/src/test/resources/META-INF/spring.factories
@@ -1,6 +1,10 @@
-# Test configuration file containing a non-existent default TestExecutionListener and a demo ContextCustomizerFactory.
+# Test configuration file containing a non-existent default TestExecutionListener,
+# a demo ContextCustomizerFactory, and a demo ApplicationContextFailureProcessor.
org.springframework.test.context.TestExecutionListener = org.example.FooListener
org.springframework.test.context.ContextCustomizerFactory =\
org.springframework.test.context.aot.samples.basic.ImportsContextCustomizerFactory
+
+org.springframework.test.context.ApplicationContextFailureProcessor =\
+ org.springframework.test.context.failures.TrackingApplicationContextFailureProcessor