Skip to content

Commit 6bdf0bc

Browse files
committed
Introduce ApplicationContextFailureProcessor SPI in the TCF
This commit introduces an ApplicationContextFailureProcessor SPI in the Spring TestContext Framework that allows third parties to process failures that occur while a SmartContextLoader attempts to load an ApplicationContext. SmartContextLoader implementations must introduce a try-catch block around the loading code and throw a ContextLoadException that wraps the failed ApplicationContext and the cause of the failure. Extensions of AbstractTestContextBootstrapper can configure an ApplicationContextFailureProcessor by overriding the new protected getApplicationContextFailureProcessor() method. DefaultCacheAwareContextLoaderDelegate unwraps any ContextLoadException and delegates to the configured ApplicationContextFailureProcessor for processing. Closes gh-28826
1 parent 19f795a commit 6bdf0bc

11 files changed

+391
-53
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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;
18+
19+
import org.springframework.context.ApplicationContext;
20+
21+
/**
22+
* Strategy for components that process failures related to application contexts
23+
* within the <em>Spring TestContext Framework</em>.
24+
*
25+
* @author Sam Brannen
26+
* @since 6.0
27+
* @see ContextLoadException
28+
*/
29+
public interface ApplicationContextFailureProcessor {
30+
31+
/**
32+
* Invoked when a failure was encountered while attempting to load an
33+
* {@link ApplicationContext}.
34+
* <p>Implementations of this method must not throw any exceptions. Consequently,
35+
* any exception thrown by an implementation of this method will be ignored.
36+
* @param context the application context that did not load successfully
37+
* @param exception the exception caught while loading the application context
38+
*/
39+
void processLoadFailure(ApplicationContext context, Throwable exception);
40+
41+
}

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

+15
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ default boolean isContextLoaded(MergedContextConfiguration mergedContextConfigur
7575
* the application context
7676
* @see #isContextLoaded
7777
* @see #closeContext
78+
* @see #setContextFailureProcessor
7879
*/
7980
ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration);
8081

@@ -100,4 +101,18 @@ default boolean isContextLoaded(MergedContextConfiguration mergedContextConfigur
100101
*/
101102
void closeContext(MergedContextConfiguration mergedContextConfiguration, @Nullable HierarchyMode hierarchyMode);
102103

104+
/**
105+
* Set the {@link ApplicationContextFailureProcessor} to use.
106+
* <p>The default implementation ignores the supplied processor.
107+
* <p>Concrete implementations should override this method to store a reference
108+
* to the supplied processor and use it to process {@link ContextLoadException
109+
* ContextLoadExceptions} thrown from context loaders in
110+
* {@link #loadContext(MergedContextConfiguration)}.
111+
* @param contextFailureProcessor the context failure processor to use
112+
* @since 6.0
113+
*/
114+
default void setContextFailureProcessor(ApplicationContextFailureProcessor contextFailureProcessor) {
115+
// no-op
116+
}
117+
103118
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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;
18+
19+
import org.springframework.context.ApplicationContext;
20+
21+
/**
22+
* Exception thrown when an error occurs while a {@link SmartContextLoader}
23+
* attempts to load an {@link ApplicationContext}.
24+
*
25+
* <p>This exception provides access to the {@linkplain #getApplicationContext()
26+
* application context} that failed to load as well as the {@linkplain #getCause()
27+
* exception} caught while attempting to load that context.
28+
*
29+
* @author Sam Brannen
30+
* @since 6.0
31+
* @see SmartContextLoader#loadContext(MergedContextConfiguration)
32+
*/
33+
@SuppressWarnings("serial")
34+
public class ContextLoadException extends Exception {
35+
36+
private final ApplicationContext applicationContext;
37+
38+
39+
/**
40+
* Create a new {@code ContextLoadException} for the supplied
41+
* {@link ApplicationContext} and {@link Exception}.
42+
* @param applicationContext the application context that failed to load
43+
* @param cause the exception caught while attempting to load that context
44+
*/
45+
public ContextLoadException(ApplicationContext applicationContext, Exception cause) {
46+
super(cause);
47+
this.applicationContext = applicationContext;
48+
}
49+
50+
51+
/**
52+
* Get the {@code ApplicationContext} that failed to load.
53+
* <p>Clients must not retain a long-lived reference to the context returned
54+
* from this method.
55+
*/
56+
public ApplicationContext getApplicationContext() {
57+
return this.applicationContext;
58+
}
59+
60+
}

Diff for: spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,23 @@ public interface SmartContextLoader extends ContextLoader {
126126
* closed on JVM shutdown. This allows for freeing of external resources held
127127
* by beans within the context &mdash; for example, temporary files.</li>
128128
* </ul>
129+
* <p>As of Spring Framework 6.0, any exception thrown while attempting to
130+
* load an {@code ApplicationContext} should be wrapped in a
131+
* {@link ContextLoadException}. Concrete implementations should therefore
132+
* contain a try-catch block similar to the following.
133+
* <pre style="code">
134+
* ApplicationContext context = // create context
135+
* try {
136+
* // configure and refresh context
137+
* }
138+
* catch (Exception ex) {
139+
* throw new ContextLoadException(context, ex);
140+
* }
141+
* </pre>
129142
* @param mergedConfig the merged context configuration to use to load the
130143
* application context
131144
* @return a new application context
132-
* @throws Exception if context loading failed
145+
* @throws ContextLoadException if context loading failed
133146
* @see #processContextConfiguration(ContextConfigurationAttributes)
134147
* @see #loadContextForAotProcessing(MergedContextConfiguration)
135148
* @see org.springframework.context.annotation.AnnotationConfigUtils#registerAnnotationConfigProcessors(org.springframework.beans.factory.support.BeanDefinitionRegistry)

Diff for: spring-test/src/main/java/org/springframework/test/context/aot/AotContextLoader.java

+27-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.springframework.context.ApplicationContext;
2020
import org.springframework.context.ApplicationContextInitializer;
2121
import org.springframework.context.ConfigurableApplicationContext;
22+
import org.springframework.test.context.ContextLoadException;
2223
import org.springframework.test.context.MergedContextConfiguration;
2324
import org.springframework.test.context.SmartContextLoader;
2425

@@ -52,10 +53,22 @@ public interface AotContextLoader extends SmartContextLoader {
5253
* {@linkplain org.springframework.context.ConfigurableApplicationContext#registerShutdownHook()
5354
* register a JVM shutdown hook} for it. Otherwise, this method should implement
5455
* behavior identical to {@code loadContext(MergedContextConfiguration)}.
56+
* <p>Any exception thrown while attempting to load an {@code ApplicationContext}
57+
* should be wrapped in a {@link ContextLoadException}. Concrete implementations
58+
* should therefore contain a try-catch block similar to the following.
59+
* <pre style="code">
60+
* GenericApplicationContext context = // create context
61+
* try {
62+
* // configure context
63+
* }
64+
* catch (Exception ex) {
65+
* throw new ContextLoadException(context, ex);
66+
* }
67+
* </pre>
5568
* @param mergedConfig the merged context configuration to use to load the
5669
* application context
5770
* @return a new {@code GenericApplicationContext}
58-
* @throws Exception if context loading failed
71+
* @throws ContextLoadException if context loading failed
5972
* @see #loadContextForAotRuntime(MergedContextConfiguration, ApplicationContextInitializer)
6073
*/
6174
ApplicationContext loadContextForAotProcessing(MergedContextConfiguration mergedConfig) throws Exception;
@@ -67,12 +80,24 @@ public interface AotContextLoader extends SmartContextLoader {
6780
* <p>This method must instantiate, initialize, and
6881
* {@linkplain org.springframework.context.ConfigurableApplicationContext#refresh()
6982
* refresh} the {@code ApplicationContext}.
83+
* <p>Any exception thrown while attempting to load an {@code ApplicationContext}
84+
* should be wrapped in a {@link ContextLoadException}. Concrete implementations
85+
* should therefore contain a try-catch block similar to the following.
86+
* <pre style="code">
87+
* GenericApplicationContext context = // create context
88+
* try {
89+
* // configure and refresh context
90+
* }
91+
* catch (Exception ex) {
92+
* throw new ContextLoadException(context, ex);
93+
* }
94+
* </pre>
7095
* @param mergedConfig the merged context configuration to use to load the
7196
* application context
7297
* @param initializer the {@code ApplicationContextInitializer} that should
7398
* be applied to the context in order to recreate bean definitions
7499
* @return a new {@code GenericApplicationContext}
75-
* @throws Exception if context loading failed
100+
* @throws ContextLoadException if context loading failed
76101
* @see #loadContextForAotProcessing(MergedContextConfiguration)
77102
*/
78103
ApplicationContext loadContextForAotRuntime(MergedContextConfiguration mergedConfig,

Diff for: spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.core.log.LogMessage;
4040
import org.springframework.javapoet.ClassName;
4141
import org.springframework.test.context.BootstrapUtils;
42+
import org.springframework.test.context.ContextLoadException;
4243
import org.springframework.test.context.ContextLoader;
4344
import org.springframework.test.context.MergedContextConfiguration;
4445
import org.springframework.test.context.SmartContextLoader;
@@ -217,9 +218,10 @@ private GenericApplicationContext loadContextForAotProcessing(
217218
}
218219
}
219220
catch (Exception ex) {
221+
Throwable cause = (ex instanceof ContextLoadException cle ? cle.getCause() : ex);
220222
throw new TestContextAotException(
221223
"Failed to load ApplicationContext for AOT processing for test class [%s]"
222-
.formatted(testClass.getName()), ex);
224+
.formatted(testClass.getName()), cause);
223225
}
224226
}
225227
throw new TestContextAotException("""

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

+27-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import org.springframework.core.log.LogMessage;
2727
import org.springframework.lang.Nullable;
2828
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
29+
import org.springframework.test.context.ApplicationContextFailureProcessor;
2930
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
31+
import org.springframework.test.context.ContextLoadException;
3032
import org.springframework.test.context.ContextLoader;
3133
import org.springframework.test.context.MergedContextConfiguration;
3234
import org.springframework.test.context.SmartContextLoader;
@@ -59,6 +61,9 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
5961

6062
private final ContextCache contextCache;
6163

64+
@Nullable
65+
private ApplicationContextFailureProcessor contextFailureProcessor;
66+
6267

6368
/**
6469
* Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using
@@ -110,8 +115,23 @@ public ApplicationContext loadContext(MergedContextConfiguration mergedContextCo
110115
this.contextCache.put(mergedContextConfiguration, context);
111116
}
112117
catch (Exception ex) {
118+
Throwable cause = ex;
119+
if (ex instanceof ContextLoadException cle) {
120+
cause = cle.getCause();
121+
if (this.contextFailureProcessor != null) {
122+
try {
123+
this.contextFailureProcessor.processLoadFailure(cle.getApplicationContext(), cause);
124+
}
125+
catch (Throwable throwable) {
126+
if (logger.isDebugEnabled()) {
127+
logger.debug("Ignoring exception thrown from ApplicationContextFailureProcessor [%s]: %s"
128+
.formatted(this.contextFailureProcessor, throwable));
129+
}
130+
}
131+
}
132+
}
113133
throw new IllegalStateException(
114-
"Failed to load ApplicationContext for " + mergedContextConfiguration, ex);
134+
"Failed to load ApplicationContext for " + mergedContextConfiguration, cause);
115135
}
116136
}
117137
else {
@@ -134,6 +154,12 @@ public void closeContext(MergedContextConfiguration mergedContextConfiguration,
134154
}
135155
}
136156

157+
@Override
158+
public void setContextFailureProcessor(ApplicationContextFailureProcessor contextFailureProcessor) {
159+
this.contextFailureProcessor = contextFailureProcessor;
160+
}
161+
162+
137163
/**
138164
* Get the {@link ContextCache} used by this context loader delegate.
139165
*/

Diff for: spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java

+35-24
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.context.ConfigurableApplicationContext;
2929
import org.springframework.context.annotation.AnnotationConfigUtils;
3030
import org.springframework.context.support.GenericApplicationContext;
31+
import org.springframework.test.context.ContextLoadException;
3132
import org.springframework.test.context.MergedContextConfiguration;
3233
import org.springframework.test.context.aot.AotContextLoader;
3334
import org.springframework.util.Assert;
@@ -159,13 +160,18 @@ public final GenericApplicationContext loadContextForAotRuntime(MergedContextCon
159160
validateMergedContextConfiguration(mergedConfig);
160161

161162
GenericApplicationContext context = createContext();
162-
prepareContext(context);
163-
prepareContext(context, mergedConfig);
164-
initializer.initialize(context);
165-
customizeContext(context);
166-
customizeContext(context, mergedConfig);
167-
context.refresh();
168-
return context;
163+
try {
164+
prepareContext(context);
165+
prepareContext(context, mergedConfig);
166+
initializer.initialize(context);
167+
customizeContext(context);
168+
customizeContext(context, mergedConfig);
169+
context.refresh();
170+
return context;
171+
}
172+
catch (Exception ex) {
173+
throw new ContextLoadException(context, ex);
174+
}
169175
}
170176

171177
/**
@@ -189,25 +195,30 @@ private final GenericApplicationContext loadContext(
189195
validateMergedContextConfiguration(mergedConfig);
190196

191197
GenericApplicationContext context = createContext();
192-
ApplicationContext parent = mergedConfig.getParentApplicationContext();
193-
if (parent != null) {
194-
context.setParent(parent);
198+
try {
199+
ApplicationContext parent = mergedConfig.getParentApplicationContext();
200+
if (parent != null) {
201+
context.setParent(parent);
202+
}
203+
204+
prepareContext(context);
205+
prepareContext(context, mergedConfig);
206+
customizeBeanFactory(context.getDefaultListableBeanFactory());
207+
loadBeanDefinitions(context, mergedConfig);
208+
AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
209+
customizeContext(context);
210+
customizeContext(context, mergedConfig);
211+
212+
if (!forAotProcessing) {
213+
context.refresh();
214+
context.registerShutdownHook();
215+
}
216+
217+
return context;
195218
}
196-
197-
prepareContext(context);
198-
prepareContext(context, mergedConfig);
199-
customizeBeanFactory(context.getDefaultListableBeanFactory());
200-
loadBeanDefinitions(context, mergedConfig);
201-
AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
202-
customizeContext(context);
203-
customizeContext(context, mergedConfig);
204-
205-
if (!forAotProcessing) {
206-
context.refresh();
207-
context.registerShutdownHook();
219+
catch (Exception ex) {
220+
throw new ContextLoadException(context, ex);
208221
}
209-
210-
return context;
211222
}
212223

213224
/**

0 commit comments

Comments
 (0)