Skip to content

Commit

Permalink
Introduce ApplicationContextFailureProcessor SPI in the TCF
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed Oct 18, 2022
1 parent 19f795a commit 6bdf0bc
Show file tree
Hide file tree
Showing 11 changed files with 391 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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;

import org.springframework.context.ApplicationContext;

/**
* Strategy for components that process failures related to application contexts
* within the <em>Spring TestContext Framework</em>.
*
* @author Sam Brannen
* @since 6.0
* @see ContextLoadException
*/
public interface ApplicationContextFailureProcessor {

/**
* Invoked when a failure was encountered while attempting to load an
* {@link ApplicationContext}.
* <p>Implementations of this method must not throw any exceptions. Consequently,
* any exception thrown by an implementation of this method will be ignored.
* @param context the application context that did not load successfully
* @param exception the exception caught while loading the application context
*/
void processLoadFailure(ApplicationContext context, Throwable exception);

}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ default boolean isContextLoaded(MergedContextConfiguration mergedContextConfigur
* the application context
* @see #isContextLoaded
* @see #closeContext
* @see #setContextFailureProcessor
*/
ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration);

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

/**
* Set the {@link ApplicationContextFailureProcessor} to use.
* <p>The default implementation ignores the supplied processor.
* <p>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
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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;

import org.springframework.context.ApplicationContext;

/**
* Exception thrown when an error occurs while a {@link SmartContextLoader}
* attempts to load an {@link ApplicationContext}.
*
* <p>This exception provides access to the {@linkplain #getApplicationContext()
* application context} that failed to load as well as the {@linkplain #getCause()
* exception} caught while attempting to load that context.
*
* @author Sam Brannen
* @since 6.0
* @see SmartContextLoader#loadContext(MergedContextConfiguration)
*/
@SuppressWarnings("serial")
public class ContextLoadException extends Exception {

private final ApplicationContext applicationContext;


/**
* Create a new {@code ContextLoadException} for the supplied
* {@link ApplicationContext} and {@link Exception}.
* @param applicationContext the application context that failed to load
* @param cause the exception caught while attempting to load that context
*/
public ContextLoadException(ApplicationContext applicationContext, Exception cause) {
super(cause);
this.applicationContext = applicationContext;
}


/**
* Get the {@code ApplicationContext} that failed to load.
* <p>Clients must not retain a long-lived reference to the context returned
* from this method.
*/
public ApplicationContext getApplicationContext() {
return this.applicationContext;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,23 @@ public interface SmartContextLoader extends ContextLoader {
* closed on JVM shutdown. This allows for freeing of external resources held
* by beans within the context &mdash; for example, temporary files.</li>
* </ul>
* <p>As of Spring Framework 6.0, any exception thrown while attempting to
* load an {@code ApplicationContext} should be wrapped in a
* {@link ContextLoadException}. Concrete implementations should therefore
* contain a try-catch block similar to the following.
* <pre style="code">
* ApplicationContext context = // create context
* try {
* // configure and refresh context
* }
* catch (Exception ex) {
* throw new ContextLoadException(context, ex);
* }
* </pre>
* @param mergedConfig the merged context configuration to use to load the
* application context
* @return a new application context
* @throws Exception if context loading failed
* @throws ContextLoadException if context loading failed
* @see #processContextConfiguration(ContextConfigurationAttributes)
* @see #loadContextForAotProcessing(MergedContextConfiguration)
* @see org.springframework.context.annotation.AnnotationConfigUtils#registerAnnotationConfigProcessors(org.springframework.beans.factory.support.BeanDefinitionRegistry)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextLoadException;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.SmartContextLoader;

Expand Down Expand Up @@ -52,10 +53,22 @@ public interface AotContextLoader extends SmartContextLoader {
* {@linkplain org.springframework.context.ConfigurableApplicationContext#registerShutdownHook()
* register a JVM shutdown hook} for it. Otherwise, this method should implement
* behavior identical to {@code loadContext(MergedContextConfiguration)}.
* <p>Any exception thrown while attempting to load an {@code ApplicationContext}
* should be wrapped in a {@link ContextLoadException}. Concrete implementations
* should therefore contain a try-catch block similar to the following.
* <pre style="code">
* GenericApplicationContext context = // create context
* try {
* // configure context
* }
* catch (Exception ex) {
* throw new ContextLoadException(context, ex);
* }
* </pre>
* @param mergedConfig the merged context configuration to use to load the
* application context
* @return a new {@code GenericApplicationContext}
* @throws Exception if context loading failed
* @throws ContextLoadException if context loading failed
* @see #loadContextForAotRuntime(MergedContextConfiguration, ApplicationContextInitializer)
*/
ApplicationContext loadContextForAotProcessing(MergedContextConfiguration mergedConfig) throws Exception;
Expand All @@ -67,12 +80,24 @@ public interface AotContextLoader extends SmartContextLoader {
* <p>This method must instantiate, initialize, and
* {@linkplain org.springframework.context.ConfigurableApplicationContext#refresh()
* refresh} the {@code ApplicationContext}.
* <p>Any exception thrown while attempting to load an {@code ApplicationContext}
* should be wrapped in a {@link ContextLoadException}. Concrete implementations
* should therefore contain a try-catch block similar to the following.
* <pre style="code">
* GenericApplicationContext context = // create context
* try {
* // configure and refresh context
* }
* catch (Exception ex) {
* throw new ContextLoadException(context, ex);
* }
* </pre>
* @param mergedConfig the merged context configuration to use to load the
* application context
* @param initializer the {@code ApplicationContextInitializer} that should
* be applied to the context in order to recreate bean definitions
* @return a new {@code GenericApplicationContext}
* @throws Exception if context loading failed
* @throws ContextLoadException if context loading failed
* @see #loadContextForAotProcessing(MergedContextConfiguration)
*/
ApplicationContext loadContextForAotRuntime(MergedContextConfiguration mergedConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.springframework.core.log.LogMessage;
import org.springframework.javapoet.ClassName;
import org.springframework.test.context.BootstrapUtils;
import org.springframework.test.context.ContextLoadException;
import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.SmartContextLoader;
Expand Down Expand Up @@ -217,9 +218,10 @@ private GenericApplicationContext loadContextForAotProcessing(
}
}
catch (Exception ex) {
Throwable cause = (ex instanceof ContextLoadException cle ? cle.getCause() : ex);
throw new TestContextAotException(
"Failed to load ApplicationContext for AOT processing for test class [%s]"
.formatted(testClass.getName()), ex);
.formatted(testClass.getName()), cause);
}
}
throw new TestContextAotException("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
import org.springframework.core.log.LogMessage;
import org.springframework.lang.Nullable;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.ApplicationContextFailureProcessor;
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
import org.springframework.test.context.ContextLoadException;
import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.SmartContextLoader;
Expand Down Expand Up @@ -59,6 +61,9 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext

private final ContextCache contextCache;

@Nullable
private ApplicationContextFailureProcessor contextFailureProcessor;


/**
* Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using
Expand Down Expand Up @@ -110,8 +115,23 @@ public ApplicationContext loadContext(MergedContextConfiguration mergedContextCo
this.contextCache.put(mergedContextConfiguration, context);
}
catch (Exception ex) {
Throwable cause = ex;
if (ex instanceof ContextLoadException cle) {
cause = cle.getCause();
if (this.contextFailureProcessor != null) {
try {
this.contextFailureProcessor.processLoadFailure(cle.getApplicationContext(), cause);
}
catch (Throwable throwable) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring exception thrown from ApplicationContextFailureProcessor [%s]: %s"
.formatted(this.contextFailureProcessor, throwable));
}
}
}
}
throw new IllegalStateException(
"Failed to load ApplicationContext for " + mergedContextConfiguration, ex);
"Failed to load ApplicationContext for " + mergedContextConfiguration, cause);
}
}
else {
Expand All @@ -134,6 +154,12 @@ public void closeContext(MergedContextConfiguration mergedContextConfiguration,
}
}

@Override
public void setContextFailureProcessor(ApplicationContextFailureProcessor contextFailureProcessor) {
this.contextFailureProcessor = contextFailureProcessor;
}


/**
* Get the {@link ContextCache} used by this context loader delegate.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.test.context.ContextLoadException;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.aot.AotContextLoader;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -159,13 +160,18 @@ public final GenericApplicationContext loadContextForAotRuntime(MergedContextCon
validateMergedContextConfiguration(mergedConfig);

GenericApplicationContext context = createContext();
prepareContext(context);
prepareContext(context, mergedConfig);
initializer.initialize(context);
customizeContext(context);
customizeContext(context, mergedConfig);
context.refresh();
return context;
try {
prepareContext(context);
prepareContext(context, mergedConfig);
initializer.initialize(context);
customizeContext(context);
customizeContext(context, mergedConfig);
context.refresh();
return context;
}
catch (Exception ex) {
throw new ContextLoadException(context, ex);
}
}

/**
Expand All @@ -189,25 +195,30 @@ private final GenericApplicationContext loadContext(
validateMergedContextConfiguration(mergedConfig);

GenericApplicationContext context = createContext();
ApplicationContext parent = mergedConfig.getParentApplicationContext();
if (parent != null) {
context.setParent(parent);
try {
ApplicationContext parent = mergedConfig.getParentApplicationContext();
if (parent != null) {
context.setParent(parent);
}

prepareContext(context);
prepareContext(context, mergedConfig);
customizeBeanFactory(context.getDefaultListableBeanFactory());
loadBeanDefinitions(context, mergedConfig);
AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
customizeContext(context);
customizeContext(context, mergedConfig);

if (!forAotProcessing) {
context.refresh();
context.registerShutdownHook();
}

return context;
}

prepareContext(context);
prepareContext(context, mergedConfig);
customizeBeanFactory(context.getDefaultListableBeanFactory());
loadBeanDefinitions(context, mergedConfig);
AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
customizeContext(context);
customizeContext(context, mergedConfig);

if (!forAotProcessing) {
context.refresh();
context.registerShutdownHook();
catch (Exception ex) {
throw new ContextLoadException(context, ex);
}

return context;
}

/**
Expand Down
Loading

0 comments on commit 6bdf0bc

Please sign in to comment.