diff --git a/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java index 9dc234f2f52..09091083cf8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/SmartContextLoader.java @@ -137,32 +137,6 @@ public interface SmartContextLoader extends ContextLoader { */ ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception; - /** - * Load a new {@linkplain ApplicationContext} based on the supplied - * {@link MergedContextConfiguration}, configure the context, and return the - * context. - *

In contrast to {@link #loadContext(MergedContextConfiguration)}, this - * method must not - * {@linkplain org.springframework.context.ConfigurableApplicationContext#refresh() - * refresh} the {@code ApplicationContext} or - * {@linkplain org.springframework.context.ConfigurableApplicationContext#registerShutdownHook() - * register a JVM shutdown hook} for it. Otherwise, this method should implement - * behavior identical to {@link #loadContext(MergedContextConfiguration)}. - *

The default implementation throws an {@link UnsupportedOperationException}. - * Concrete implementations must therefore override this method in order to - * support AOT (ahead of time) processing. - * @param mergedConfig the merged context configuration to use to load the - * application context - * @return a new application context - * @throws Exception if context loading failed - * @since 6.0 - */ - default ApplicationContext loadContextForAotProcessing(MergedContextConfiguration mergedConfig) throws Exception { - throw new UnsupportedOperationException( - "%s does not support loadContextForAotProcessing(MergedContextConfiguration)" - .formatted(getClass().getName())); - } - /** * {@code SmartContextLoader} does not support deprecated {@link ContextLoader} methods. * Call {@link #processContextConfiguration(ContextConfigurationAttributes)} instead. diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotContextLoader.java new file mode 100644 index 00000000000..932b1a2f2ba --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotContextLoader.java @@ -0,0 +1,77 @@ +/* + * 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.aot; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.SmartContextLoader; + +/** + * Strategy interface for loading an {@link ApplicationContext} for build-time + * {@linkplain #loadContextForAotProcessing AOT processing} as well as run-time + * {@linkplain #loadContextForAotRuntime AOT execution} for an integration test + * managed by the Spring TestContext Framework. + * + *

As of Spring Framework 6.0, AOT infrastructure requires that an {@code AotContextLoader} + * create a {@link org.springframework.context.support.GenericApplicationContext + * GenericApplicationContext} for both build-time processing and run-time execution. + * + * @author Sam Brannen + * @since 6.0 + */ +public interface AotContextLoader extends SmartContextLoader { + + /** + * Load a new {@link ApplicationContext} for AOT build-time processing based + * on the supplied {@link MergedContextConfiguration}, configure the context, + * and return the context. + *

In contrast to {@link #loadContext(MergedContextConfiguration)}, this + * method must not + * {@linkplain org.springframework.context.ConfigurableApplicationContext#refresh() + * refresh} the {@code ApplicationContext} or + * {@linkplain org.springframework.context.ConfigurableApplicationContext#registerShutdownHook() + * register a JVM shutdown hook} for it. Otherwise, this method should implement + * behavior identical to {@link #loadContext(MergedContextConfiguration)}. + * @param mergedConfig the merged context configuration to use to load the + * application context + * @return a new {@code GenericApplicationContext} + * @throws Exception if context loading failed + * @see #loadContextForAotRuntime(MergedContextConfiguration, ApplicationContextInitializer) + */ + ApplicationContext loadContextForAotProcessing(MergedContextConfiguration mergedConfig) throws Exception; + + /** + * Load a new {@link ApplicationContext} for AOT run-time execution based on + * the supplied {@link MergedContextConfiguration} and + * {@link ApplicationContextInitializer}. + *

This method must instantiate, initialize, and + * {@linkplain org.springframework.context.ConfigurableApplicationContext#refresh() + * refresh} the {@code ApplicationContext}. + * @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 + * @see #loadContextForAotProcessing(MergedContextConfiguration) + */ + ApplicationContext loadContextForAotRuntime(MergedContextConfiguration mergedConfig, + ApplicationContextInitializer initializer) throws Exception; + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java index b4c8626efa1..2fd247c1a47 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java @@ -166,9 +166,9 @@ class TestContextAotGenerator { Consider annotating test class [%s] with @ContextConfiguration or \ @ContextHierarchy.""".formatted(testClass.getCanonicalName())); - if (contextLoader instanceof SmartContextLoader smartContextLoader) { + if (contextLoader instanceof AotContextLoader aotContextLoader) { try { - ApplicationContext context = smartContextLoader.loadContextForAotProcessing(mergedConfig); + ApplicationContext context = aotContextLoader.loadContextForAotProcessing(mergedConfig); if (context instanceof GenericApplicationContext gac) { return gac; } @@ -181,7 +181,7 @@ class TestContextAotGenerator { } throw new TestContextAotException(""" Cannot generate AOT artifacts for test class [%s]. The configured \ - ContextLoader [%s] must be a SmartContextLoader and must create a \ + ContextLoader [%s] must be an AotContextLoader and must create a \ GenericApplicationContext.""".formatted(testClass.getCanonicalName(), contextLoader.getClass().getName())); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java index 817550d8530..90b750ceb28 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java @@ -20,11 +20,14 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.SmartContextLoader; +import org.springframework.test.context.aot.AotContextLoader; import org.springframework.util.Assert; /** @@ -68,7 +71,7 @@ import org.springframework.util.Assert; * @since 3.2 * @see SmartContextLoader */ -public abstract class AbstractDelegatingSmartContextLoader implements SmartContextLoader { +public abstract class AbstractDelegatingSmartContextLoader implements AotContextLoader { private static final Log logger = LogFactory.getLog(AbstractDelegatingSmartContextLoader.class); @@ -224,10 +227,16 @@ public abstract class AbstractDelegatingSmartContextLoader implements SmartConte * @since 6.0 */ @Override - public ApplicationContext loadContextForAotProcessing(MergedContextConfiguration mergedConfig) throws Exception { + public final ApplicationContext loadContextForAotProcessing(MergedContextConfiguration mergedConfig) throws Exception { return loadContext(mergedConfig, false); } + @Override + public final ApplicationContext loadContextForAotRuntime(MergedContextConfiguration mergedConfig, + ApplicationContextInitializer initializer) throws Exception { + return getAotContextLoader(mergedConfig, "load").loadContextForAotRuntime(mergedConfig, initializer); + } + /** * Delegates to an appropriate candidate {@code SmartContextLoader} to load * an {@link ApplicationContext}. @@ -240,12 +249,7 @@ public abstract class AbstractDelegatingSmartContextLoader implements SmartConte * {@code ApplicationContext} from the supplied merged context configuration */ private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, boolean refresh) throws Exception { - Assert.notNull(mergedConfig, "MergedContextConfiguration must not be null"); - - Assert.state(!(mergedConfig.hasLocations() && mergedConfig.hasClasses()), () -> String.format( - "Neither %s nor %s supports loading an ApplicationContext from %s: " + - "declare either 'locations' or 'classes' but not both.", name(getXmlLoader()), - name(getAnnotationConfigLoader()), mergedConfig)); + assertPreconditions(mergedConfig, "load"); SmartContextLoader[] candidates = {getXmlLoader(), getAnnotationConfigLoader()}; for (SmartContextLoader loader : candidates) { @@ -265,7 +269,7 @@ public abstract class AbstractDelegatingSmartContextLoader implements SmartConte // else... throw new IllegalStateException(String.format( - "Neither %s nor %s was able to load an ApplicationContext from %s.", + "Neither %s nor %s is able to load an ApplicationContext for %s.", name(getXmlLoader()), name(getAnnotationConfigLoader()), mergedConfig)); } @@ -282,9 +286,53 @@ public abstract class AbstractDelegatingSmartContextLoader implements SmartConte throws Exception { if (logger.isDebugEnabled()) { - logger.debug(String.format("Delegating to %s to load context from %s.", name(loader), mergedConfig)); + logger.debug(String.format("Delegating to %s to load context for %s.", name(loader), mergedConfig)); } - return (refresh ? loader.loadContext(mergedConfig) : loader.loadContextForAotProcessing(mergedConfig)); + if (refresh) { + return loader.loadContext(mergedConfig); + } + else { + if (loader instanceof AotContextLoader aotContextLoader) { + return aotContextLoader.loadContextForAotProcessing(mergedConfig); + } + throw new IllegalStateException( + "%s must implement AotContextLoader to load the ApplicationContext for %s." + .formatted(name(loader), mergedConfig)); + } + } + + private AotContextLoader getAotContextLoader(MergedContextConfiguration mergedConfig, String action) { + assertPreconditions(mergedConfig, action); + + SmartContextLoader[] candidates = {getXmlLoader(), getAnnotationConfigLoader()}; + for (SmartContextLoader loader : candidates) { + // Determine if each loader can load a context from the mergedConfig. If it + // can, let it; otherwise, keep iterating. + if (loader instanceof AotContextLoader aotContextLoader && supports(loader, mergedConfig)) { + return aotContextLoader; + } + } + + // If neither of the candidates supports the mergedConfig based on resources but + // ACIs or customizers were declared, then delegate to the annotation config + // loader. + if (getAnnotationConfigLoader() instanceof AotContextLoader aotContextLoader && + (!mergedConfig.getContextInitializerClasses().isEmpty() || + !mergedConfig.getContextCustomizers().isEmpty())) { + return aotContextLoader; + } + + throw new IllegalStateException( + "Neither %s nor %s is able to %s the ApplicationContext for %s.".formatted( + name(getXmlLoader()), name(getAnnotationConfigLoader()), action, mergedConfig)); + } + + private void assertPreconditions(MergedContextConfiguration mergedConfig, String action) { + Assert.notNull(mergedConfig, "MergedContextConfiguration must not be null"); + Assert.state(!(mergedConfig.hasLocations() && mergedConfig.hasClasses()), () -> """ + Neither %s nor %s is able to %s an ApplicationContext for %s: \ + declare either 'locations' or 'classes' but not both.""".formatted( + name(getXmlLoader()), name(getAnnotationConfigLoader()), action, mergedConfig)); } private boolean supports(SmartContextLoader loader, MergedContextConfiguration mergedConfig) { diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java index 138827a82c2..d518dff2629 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java @@ -22,10 +22,12 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.support.GenericApplicationContext; import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.aot.AotContextLoader; import org.springframework.util.StringUtils; /** @@ -56,7 +58,7 @@ import org.springframework.util.StringUtils; * @since 2.5 * @see #loadContext(MergedContextConfiguration) */ -public abstract class AbstractGenericContextLoader extends AbstractContextLoader { +public abstract class AbstractGenericContextLoader extends AbstractContextLoader implements AotContextLoader { protected static final Log logger = LogFactory.getLog(AbstractGenericContextLoader.class); @@ -128,6 +130,27 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader return loadContext(mergedConfig, false); } + @Override + public final ApplicationContext loadContextForAotRuntime(MergedContextConfiguration mergedConfig, + ApplicationContextInitializer initializer) throws Exception { + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Loading ApplicationContext for AOT runtime for merged context configuration [%s].", + mergedConfig)); + } + + validateMergedContextConfiguration(mergedConfig); + + GenericApplicationContext context = createContext(); + prepareContext(context); + prepareContext(context, mergedConfig); + initializer.initialize(context); + customizeContext(context); + customizeContext(context, mergedConfig); + context.refresh(); + return context; + } + /** * Load a {@link GenericApplicationContext} for the supplied * {@link MergedContextConfiguration}. diff --git a/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java index 97fc135c3c8..0adad9d74b5 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java @@ -22,6 +22,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.core.io.DefaultResourceLoader; @@ -29,6 +30,7 @@ import org.springframework.core.io.FileSystemResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.mock.web.MockServletContext; import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.aot.AotContextLoader; import org.springframework.test.context.support.AbstractContextLoader; import org.springframework.util.Assert; import org.springframework.web.context.WebApplicationContext; @@ -57,11 +59,60 @@ import org.springframework.web.context.support.GenericWebApplicationContext; * @see #loadContext(MergedContextConfiguration) * @see #loadContext(String...) */ -public abstract class AbstractGenericWebContextLoader extends AbstractContextLoader { +public abstract class AbstractGenericWebContextLoader extends AbstractContextLoader implements AotContextLoader { protected static final Log logger = LogFactory.getLog(AbstractGenericWebContextLoader.class); + // AotContextLoader + + /** + * Load a {@link GenericWebApplicationContext} for the supplied + * {@link MergedContextConfiguration}. + *

In contrast to {@link #loadContext(MergedContextConfiguration)}, this + * method does not + * {@linkplain org.springframework.context.ConfigurableApplicationContext#refresh() + * refresh} the {@code ApplicationContext} or + * {@linkplain org.springframework.context.ConfigurableApplicationContext#registerShutdownHook() + * register a JVM shutdown hook} for it. Otherwise, this method implements + * behavior identical to {@link #loadContext(MergedContextConfiguration)}. + * @param mergedConfig the merged context configuration to use to load the + * application context + * @return a new web application context + * @since 6.0 + * @see org.springframework.test.context.aot.AotContextLoader#loadContextForAotProcessing(MergedContextConfiguration) + * @see GenericWebApplicationContext + */ + @Override + public final ApplicationContext loadContextForAotProcessing( + MergedContextConfiguration mergedConfig) throws Exception { + + return loadContext(mergedConfig, false); + } + + @Override + public final ApplicationContext loadContextForAotRuntime(MergedContextConfiguration mergedConfig, + ApplicationContextInitializer initializer) throws Exception { + + if (!(mergedConfig instanceof WebMergedContextConfiguration webMergedConfig)) { + throw new IllegalArgumentException(""" + Cannot load WebApplicationContext from non-web merged context configuration %s. \ + Consider annotating your test class with @WebAppConfiguration.""" + .formatted(mergedConfig)); + } + + validateMergedContextConfiguration(webMergedConfig); + + GenericWebApplicationContext context = createContext(); + configureWebResources(context, webMergedConfig); + prepareContext(context, webMergedConfig); + initializer.initialize(context); + customizeContext(context, webMergedConfig); + context.refresh(); + return context; + } + + // SmartContextLoader /** @@ -105,30 +156,6 @@ public abstract class AbstractGenericWebContextLoader extends AbstractContextLoa return loadContext(mergedConfig, true); } - /** - * Load a {@link GenericWebApplicationContext} for the supplied - * {@link MergedContextConfiguration}. - *

In contrast to {@link #loadContext(MergedContextConfiguration)}, this - * method does not - * {@linkplain org.springframework.context.ConfigurableApplicationContext#refresh() - * refresh} the {@code ApplicationContext} or - * {@linkplain org.springframework.context.ConfigurableApplicationContext#registerShutdownHook() - * register a JVM shutdown hook} for it. Otherwise, this method implements - * behavior identical to {@link #loadContext(MergedContextConfiguration)}. - * @param mergedConfig the merged context configuration to use to load the - * application context - * @return a new web application context - * @since 6.0 - * @see org.springframework.test.context.SmartContextLoader#loadContextForAotProcessing(MergedContextConfiguration) - * @see GenericWebApplicationContext - */ - @Override - public final ApplicationContext loadContextForAotProcessing( - MergedContextConfiguration mergedConfig) throws Exception { - - return loadContext(mergedConfig, false); - } - /** * Load a {@link GenericWebApplicationContext} for the supplied * {@link MergedContextConfiguration}. diff --git a/spring-test/src/test/java/org/springframework/test/context/support/DelegatingSmartContextLoaderTests.java b/spring-test/src/test/java/org/springframework/test/context/support/DelegatingSmartContextLoaderTests.java index cca4567596d..cc33d62a442 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/DelegatingSmartContextLoaderTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/DelegatingSmartContextLoaderTests.java @@ -110,7 +110,7 @@ class DelegatingSmartContextLoaderTests { assertThatIllegalStateException() .isThrownBy(() -> loader.loadContext(mergedConfig)) .withMessageStartingWith("Neither") - .withMessageContaining("was able to load an ApplicationContext from"); + .withMessageContaining("is able to load an ApplicationContext for"); } /**