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 index 932b1a2f2b..6d9a53e1bb 100644 --- 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 @@ -28,6 +28,10 @@ import org.springframework.test.context.SmartContextLoader; * {@linkplain #loadContextForAotRuntime AOT execution} for an integration test * managed by the Spring TestContext Framework. * + *

{@code AotContextLoader} is an extension of the {@link SmartContextLoader} + * SPI that allows a context loader to optionally provide ahead-of-time (AOT) + * support. + * *

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. @@ -47,7 +51,7 @@ public interface AotContextLoader extends SmartContextLoader { * 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)}. + * behavior identical to {@code loadContext(MergedContextConfiguration)}. * @param mergedConfig the merged context configuration to use to load the * application context * @return a new {@code GenericApplicationContext} 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 a6e1943193..e003123aa4 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 @@ -122,7 +122,7 @@ public class TestContextAotGenerator { generationContext.writeGeneratedContent(); } catch (Exception ex) { - logger.warn(LogMessage.format("Failed to generate AOT artifacts for test classes [%s]", + logger.warn(LogMessage.format("Failed to generate AOT artifacts for test classes %s", testClasses.stream().map(Class::getName).toList()), ex); } }); 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 24ea45a1f9..bcfea5500f 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 @@ -105,18 +105,19 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext context = loadContextInternal(mergedContextConfiguration); } if (logger.isDebugEnabled()) { - logger.debug(String.format("Storing ApplicationContext [%s] in cache under key [%s]", + logger.debug("Storing ApplicationContext [%s] in cache under key %s".formatted( System.identityHashCode(context), mergedContextConfiguration)); } this.contextCache.put(mergedContextConfiguration, context); } catch (Exception ex) { - throw new IllegalStateException("Failed to load ApplicationContext", ex); + throw new IllegalStateException( + "Failed to load ApplicationContext for " + mergedContextConfiguration, ex); } } else { if (logger.isDebugEnabled()) { - logger.debug(String.format("Retrieved ApplicationContext [%s] from cache with key [%s]", + logger.debug("Retrieved ApplicationContext [%s] from cache with key %s".formatted( System.identityHashCode(context), mergedContextConfiguration)); } } @@ -150,17 +151,16 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext protected ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration) throws Exception { - ContextLoader contextLoader = mergedContextConfiguration.getContextLoader(); - Assert.notNull(contextLoader, "Cannot load an ApplicationContext with a NULL 'contextLoader'. " + - "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy."); - + ContextLoader contextLoader = getContextLoader(mergedContextConfiguration); if (contextLoader instanceof SmartContextLoader smartContextLoader) { return smartContextLoader.loadContext(mergedContextConfiguration); } else { String[] locations = mergedContextConfiguration.getLocations(); - Assert.notNull(locations, "Cannot load an ApplicationContext with a NULL 'locations' array. " + - "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy."); + Assert.notNull(locations, """ + Cannot load an ApplicationContext with a NULL 'locations' array. \ + Consider annotating test class [%s] with @ContextConfiguration or \ + @ContextHierarchy.""".formatted(mergedContextConfiguration.getTestClass().getName())); return contextLoader.loadContext(locations); } } @@ -172,8 +172,8 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext Assert.state(contextInitializer != null, () -> "Failed to load AOT ApplicationContextInitializer for test class [%s]" .formatted(testClass.getName())); + ContextLoader contextLoader = getContextLoader(mergedConfig); logger.info(LogMessage.format("Loading ApplicationContext in AOT mode for %s", mergedConfig)); - ContextLoader contextLoader = mergedConfig.getContextLoader(); if (!((contextLoader instanceof AotContextLoader aotContextLoader) && (aotContextLoader.loadContextForAotRuntime(mergedConfig, contextInitializer) instanceof GenericApplicationContext gac))) { @@ -187,6 +187,15 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext return gac; } + private ContextLoader getContextLoader(MergedContextConfiguration mergedConfig) { + ContextLoader contextLoader = mergedConfig.getContextLoader(); + Assert.notNull(contextLoader, """ + Cannot load an ApplicationContext with a NULL 'contextLoader'. \ + Consider annotating test class [%s] with @ContextConfiguration or \ + @ContextHierarchy.""".formatted(mergedConfig.getTestClass().getName())); + return contextLoader; + } + /** * Determine if we are running in AOT mode for the supplied test class. */ 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 90b750ceb2..a9a9904578 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 @@ -60,12 +60,17 @@ import org.springframework.util.Assert; * (e.g., XML configuration files and Groovy scripts) or annotated classes, * but not both simultaneously. * - *

As of Spring 3.2, a test class may optionally declare neither path-based + *

As of Spring Framework 3.2, a test class may optionally declare neither path-based * resource locations nor annotated classes and instead declare only {@linkplain * ContextConfiguration#initializers application context initializers}. In such * cases, an attempt will still be made to detect defaults, but their absence will * not result in an exception. * + *

As of Spring Framework 6.0, this class implements {@link AotContextLoader}. + * Consequently, the candidate {@link #getXmlLoader()} and + * {@link #getAnnotationConfigLoader()} must also implement {@code AotContextLoader} + * in order to provide AOT support. + * * @author Sam Brannen * @author Phillip Webb * @since 3.2 @@ -195,144 +200,103 @@ public abstract class AbstractDelegatingSmartContextLoader implements AotContext * * @param mergedConfig the merged context configuration to use to load the application context * @return a new application context - * @throws IllegalArgumentException if the supplied merged configuration is {@code null} * @throws IllegalStateException if neither candidate loader is capable of loading an * {@code ApplicationContext} from the supplied merged context configuration */ @Override public final ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception { - return loadContext(mergedConfig, true); + SmartContextLoader loader = getContextLoader(mergedConfig); + if (logger.isDebugEnabled()) { + logger.debug("Delegating to %s to load context for %s".formatted(name(loader), mergedConfig)); + } + return loader.loadContext(mergedConfig); } /** * Delegates to an appropriate candidate {@code SmartContextLoader} to load * an {@link ApplicationContext} for AOT processing. *

Delegation is based on explicit knowledge of the implementations of the - * default loaders for {@linkplain #getXmlLoader() XML configuration files and - * Groovy scripts} and {@linkplain #getAnnotationConfigLoader() annotated classes}. - * Specifically, the delegation algorithm is as follows: - *

+ * default loaders. See {@link #loadContext(MergedContextConfiguration)} for + * details. * @param mergedConfig the merged context configuration to use to load the application context * @return a new application context * @throws IllegalArgumentException if the supplied merged configuration is {@code null} * @throws IllegalStateException if neither candidate loader is capable of loading an * {@code ApplicationContext} from the supplied merged context configuration * @since 6.0 + * @see AotContextLoader#loadContextForAotProcessing(MergedContextConfiguration) */ @Override 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); + AotContextLoader loader = getAotContextLoader(mergedConfig); + if (logger.isDebugEnabled()) { + logger.debug("Delegating to %s to load context for AOT processing for %s" + .formatted(name(loader), mergedConfig)); + } + return loader.loadContextForAotProcessing(mergedConfig); } /** * Delegates to an appropriate candidate {@code SmartContextLoader} to load - * an {@link ApplicationContext}. + * an {@link ApplicationContext} for AOT run-time execution. + *

Delegation is based on explicit knowledge of the implementations of the + * default loaders. See {@link #loadContext(MergedContextConfiguration)} for + * details. * @param mergedConfig the merged context configuration to use to load the application context - * @param refresh whether to refresh the {@code ApplicationContext} and register - * a JVM shutdown hook for it + * @param initializer the {@code ApplicationContextInitializer} that should + * be applied to the context in order to recreate bean definitions * @return a new application context - * @throws IllegalArgumentException if the supplied merged configuration is {@code null} * @throws IllegalStateException if neither candidate loader is capable of loading an * {@code ApplicationContext} from the supplied merged context configuration + * @since 6.0 + * @see AotContextLoader#loadContextForAotRuntime(MergedContextConfiguration, ApplicationContextInitializer) */ - private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, boolean refresh) throws Exception { - assertPreconditions(mergedConfig, "load"); + @Override + public final ApplicationContext loadContextForAotRuntime(MergedContextConfiguration mergedConfig, + ApplicationContextInitializer initializer) throws Exception { + + AotContextLoader loader = getAotContextLoader(mergedConfig); + if (logger.isDebugEnabled()) { + logger.debug("Delegating to %s to load context for AOT execution for %s" + .formatted(name(loader), mergedConfig)); + } + return loader.loadContextForAotRuntime(mergedConfig, initializer); + } + + private SmartContextLoader getContextLoader(MergedContextConfiguration mergedConfig) { + Assert.notNull(mergedConfig, "MergedContextConfiguration must not be null"); + Assert.state(!(mergedConfig.hasLocations() && mergedConfig.hasClasses()), () -> """ + Neither %s nor %s is able to load an ApplicationContext for %s: \ + declare either 'locations' or 'classes' but not both.""".formatted( + name(getXmlLoader()), name(getAnnotationConfigLoader()), mergedConfig)); 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 (supports(loader, mergedConfig)) { - return delegateLoading(loader, mergedConfig, refresh); + return loader; } } // 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 (!mergedConfig.getContextInitializerClasses().isEmpty() || !mergedConfig.getContextCustomizers().isEmpty()) { - return delegateLoading(getAnnotationConfigLoader(), mergedConfig, refresh); + // ACIs or customizers were declared, then delegate to the annotation config loader. + if (hasInitializersOrCustomizers(mergedConfig)) { + return getAnnotationConfigLoader(); } // else... - throw new IllegalStateException(String.format( - "Neither %s nor %s is able to load an ApplicationContext for %s.", - name(getXmlLoader()), name(getAnnotationConfigLoader()), mergedConfig)); - } - - private static void delegateProcessing(SmartContextLoader loader, ContextConfigurationAttributes configAttributes) { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Delegating to %s to process context configuration %s.", - name(loader), configAttributes)); - } - loader.processContextConfiguration(configAttributes); - } - - private static ApplicationContext delegateLoading( - SmartContextLoader loader, MergedContextConfiguration mergedConfig, boolean refresh) - throws Exception { - - if (logger.isDebugEnabled()) { - logger.debug(String.format("Delegating to %s to load context for %s.", name(loader), 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)); + "Neither %s nor %s is able to load an ApplicationContext for %s.".formatted( + name(getXmlLoader()), name(getAnnotationConfigLoader()), 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 AotContextLoader getAotContextLoader(MergedContextConfiguration mergedConfig) { + SmartContextLoader loader = getContextLoader(mergedConfig); + if (!(loader instanceof AotContextLoader aotContextLoader)) { + throw new IllegalStateException("%s must be an AotContextLoader".formatted(name(loader))); + } + return aotContextLoader; } private boolean supports(SmartContextLoader loader, MergedContextConfiguration mergedConfig) { @@ -344,6 +308,19 @@ public abstract class AbstractDelegatingSmartContextLoader implements AotContext } } + + private static void delegateProcessing(SmartContextLoader loader, ContextConfigurationAttributes configAttributes) { + if (logger.isDebugEnabled()) { + logger.debug("Delegating to %s to process context configuration %s.".formatted(name(loader), configAttributes)); + } + loader.processContextConfiguration(configAttributes); + } + + private static boolean hasInitializersOrCustomizers(MergedContextConfiguration mergedConfig) { + return !(mergedConfig.getContextInitializerClasses().isEmpty() && + mergedConfig.getContextCustomizers().isEmpty()); + } + private static String name(SmartContextLoader loader) { return loader.getClass().getSimpleName(); } 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 d518dff262..68f38553e7 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 @@ -16,6 +16,8 @@ package org.springframework.test.context.support; +import java.util.Arrays; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -28,7 +30,7 @@ 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; +import org.springframework.util.Assert; /** * Abstract, generic extension of {@link AbstractContextLoader} that loads a @@ -104,12 +106,12 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader */ @Override public final ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception { - return loadContext(mergedConfig, true); + return loadContext(mergedConfig, false); } /** - * Load a {@link GenericApplicationContext} for the supplied - * {@link MergedContextConfiguration}. + * Load a {@link GenericApplicationContext} for AOT build-time processing based + * on the supplied {@link MergedContextConfiguration}. *

In contrast to {@link #loadContext(MergedContextConfiguration)}, this * method does not * {@linkplain org.springframework.context.ConfigurableApplicationContext#refresh() @@ -120,23 +122,38 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader * @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 - * @see org.springframework.test.context.SmartContextLoader#loadContextForAotProcessing(MergedContextConfiguration) + * @see AotContextLoader#loadContextForAotProcessing(MergedContextConfiguration) */ @Override - public final ApplicationContext loadContextForAotProcessing( - MergedContextConfiguration mergedConfig) throws Exception { - - return loadContext(mergedConfig, false); + public final GenericApplicationContext loadContextForAotProcessing(MergedContextConfiguration mergedConfig) + throws Exception { + return loadContext(mergedConfig, true); } + /** + * Load a {@link GenericApplicationContext} for AOT run-time execution based on + * the supplied {@link MergedContextConfiguration} and + * {@link ApplicationContextInitializer}. + * @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 application context + * @throws Exception if context loading failed + * @since 6.0 + * @see AotContextLoader#loadContextForAotRuntime(MergedContextConfiguration, ApplicationContextInitializer) + */ @Override - public final ApplicationContext loadContextForAotRuntime(MergedContextConfiguration mergedConfig, + public final GenericApplicationContext loadContextForAotRuntime(MergedContextConfiguration mergedConfig, ApplicationContextInitializer initializer) throws Exception { + Assert.notNull(mergedConfig, "MergedContextConfiguration must not be null"); + Assert.notNull(initializer, "ApplicationContextInitializer must not be null"); + if (logger.isDebugEnabled()) { - logger.debug(String.format("Loading ApplicationContext for AOT runtime for merged context configuration [%s].", - mergedConfig)); + logger.debug("Loading ApplicationContext for AOT runtime for merged context configuration " + mergedConfig); } validateMergedContextConfiguration(mergedConfig); @@ -156,16 +173,17 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader * {@link MergedContextConfiguration}. * @param mergedConfig the merged context configuration to use to load the * application context - * @param refresh whether to refresh the {@code ApplicationContext} and register - * a JVM shutdown hook for it + * @param forAotProcessing {@code true} if the context is being loaded for + * AOT processing, meaning not to refresh the {@code ApplicationContext} or + * register a JVM shutdown hook for it * @return a new application context */ private final GenericApplicationContext loadContext( - MergedContextConfiguration mergedConfig, boolean refresh) throws Exception { + MergedContextConfiguration mergedConfig, boolean forAotProcessing) throws Exception { if (logger.isDebugEnabled()) { - logger.debug(String.format("Loading ApplicationContext for merged context configuration [%s].", - mergedConfig)); + logger.debug("Loading ApplicationContext %sfor merged context configuration %s" + .formatted((forAotProcessing ? "for AOT processing " : ""), mergedConfig)); } validateMergedContextConfiguration(mergedConfig); @@ -184,7 +202,7 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader customizeContext(context); customizeContext(context, mergedConfig); - if (refresh) { + if (!forAotProcessing) { context.refresh(); context.registerShutdownHook(); } @@ -236,14 +254,13 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader * @see org.springframework.test.context.ContextLoader#loadContext * @see GenericApplicationContext * @see #loadContext(MergedContextConfiguration) - * @deprecated as of Spring Framework 6.0, in favor of {@link #loadContextForAotProcessing(MergedContextConfiguration)} + * @deprecated as of Spring Framework 6.0, in favor of {@link #loadContext(MergedContextConfiguration)} */ @Deprecated @Override public final ConfigurableApplicationContext loadContext(String... locations) throws Exception { if (logger.isDebugEnabled()) { - logger.debug(String.format("Loading ApplicationContext for locations [%s].", - StringUtils.arrayToCommaDelimitedString(locations))); + logger.debug("Loading ApplicationContext for locations " + Arrays.toString(locations)); } GenericApplicationContext context = createContext(); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoader.java index 7b3f043253..cc2b7286eb 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -16,6 +16,8 @@ package org.springframework.test.context.support; +import java.util.Arrays; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -24,7 +26,6 @@ import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; import org.springframework.context.support.GenericApplicationContext; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.MergedContextConfiguration; -import org.springframework.util.ObjectUtils; /** * Concrete implementation of {@link AbstractGenericContextLoader} that loads @@ -155,10 +156,11 @@ public class AnnotationConfigContextLoader extends AbstractGenericContextLoader @Override protected void validateMergedContextConfiguration(MergedContextConfiguration mergedConfig) { if (mergedConfig.hasLocations()) { - String msg = String.format("Test class [%s] has been configured with @ContextConfiguration's 'locations' " + - "(or 'value') attribute %s, but %s does not support resource locations.", - mergedConfig.getTestClass().getName(), ObjectUtils.nullSafeToString(mergedConfig.getLocations()), - getClass().getSimpleName()); + String msg = """ + Test class [%s] has been configured with @ContextConfiguration's 'locations' \ + (or 'value') attribute %s, but %s does not support resource locations.""" + .formatted(mergedConfig.getTestClass().getName(), + Arrays.toString(mergedConfig.getLocations()), getClass().getSimpleName()); logger.error(msg); throw new IllegalStateException(msg); } @@ -181,7 +183,7 @@ public class AnnotationConfigContextLoader extends AbstractGenericContextLoader protected void loadBeanDefinitions(GenericApplicationContext context, MergedContextConfiguration mergedConfig) { Class[] componentClasses = mergedConfig.getClasses(); if (logger.isDebugEnabled()) { - logger.debug("Registering component classes: " + ObjectUtils.nullSafeToString(componentClasses)); + logger.debug("Registering component classes: " + Arrays.toString(componentClasses)); } new AnnotatedBeanDefinitionReader(context).register(componentClasses); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java index 2c2f1318a7..86ec4441be 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * 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. @@ -123,13 +123,14 @@ public class DefaultTestContext implements TestContext { public ApplicationContext getApplicationContext() { ApplicationContext context = this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration); if (context instanceof ConfigurableApplicationContext cac) { - Assert.state(cac.isActive(), () -> - "The ApplicationContext loaded for [" + this.mergedContextConfiguration + - "] is not active. This may be due to one of the following reasons: " + - "1) the context was closed programmatically by user code; " + - "2) the context was closed during parallel test execution either " + - "according to @DirtiesContext semantics or due to automatic eviction " + - "from the ContextCache due to a maximum cache size policy."); + Assert.state(cac.isActive(), () -> """ + The ApplicationContext loaded for %s is not active. \ + This may be due to one of the following reasons: \ + 1) the context was closed programmatically by user code; \ + 2) the context was closed during parallel test execution either \ + according to @DirtiesContext semantics or due to automatic eviction \ + from the ContextCache due to a maximum cache size policy.""" + .formatted(this.mergedContextConfiguration)); } return context; } @@ -206,7 +207,7 @@ public class DefaultTestContext implements TestContext { Assert.notNull(computeFunction, "Compute function must not be null"); Object value = this.attributes.computeIfAbsent(name, computeFunction); Assert.state(value != null, - () -> String.format("Compute function must not return null for attribute named '%s'", name)); + () -> "Compute function must not return null for attribute named '%s'".formatted(name)); return (T) value; } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java index c07ca5f8ee..0970994216 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/GenericXmlContextLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * 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. @@ -16,11 +16,12 @@ package org.springframework.test.context.support; +import java.util.Arrays; + import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.context.support.GenericApplicationContext; import org.springframework.test.context.MergedContextConfiguration; -import org.springframework.util.ObjectUtils; /** * Concrete implementation of {@link AbstractGenericContextLoader} that reads @@ -64,10 +65,10 @@ public class GenericXmlContextLoader extends AbstractGenericContextLoader { @Override protected void validateMergedContextConfiguration(MergedContextConfiguration mergedConfig) { if (mergedConfig.hasClasses()) { - String msg = String.format( - "Test class [%s] has been configured with @ContextConfiguration's 'classes' attribute %s, " - + "but %s does not support annotated classes.", mergedConfig.getTestClass().getName(), - ObjectUtils.nullSafeToString(mergedConfig.getClasses()), getClass().getSimpleName()); + String msg = """ + Test class [%s] has been configured with @ContextConfiguration's 'classes' attribute %s, \ + but %s does not support annotated classes.""".formatted(mergedConfig.getTestClass().getName(), + Arrays.toString(mergedConfig.getClasses()), getClass().getSimpleName()); logger.error(msg); throw new IllegalStateException(msg); } 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 0adad9d74b..f87e5423b3 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 @@ -64,57 +64,6 @@ public abstract class AbstractGenericWebContextLoader extends AbstractContextLoa 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 - /** * Load a {@link GenericWebApplicationContext} for the supplied * {@link MergedContextConfiguration}. @@ -149,26 +98,86 @@ public abstract class AbstractGenericWebContextLoader extends AbstractContextLoa * application context * @return a new web application context * @see org.springframework.test.context.SmartContextLoader#loadContext(MergedContextConfiguration) - * @see GenericWebApplicationContext */ @Override public final ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception { + return loadContext(mergedConfig, false); + } + + /** + * Load a {@link GenericWebApplicationContext} for AOT build-time processing based + * on 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 + * @throws Exception if context loading failed + * @since 6.0 + * @see AotContextLoader#loadContextForAotProcessing(MergedContextConfiguration) + */ + @Override + public final GenericWebApplicationContext loadContextForAotProcessing(MergedContextConfiguration mergedConfig) + throws Exception { return loadContext(mergedConfig, true); } + /** + * Load a {@link GenericWebApplicationContext} for AOT run-time execution based on + * the supplied {@link MergedContextConfiguration} and + * {@link ApplicationContextInitializer}. + * @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 web application context + * @throws Exception if context loading failed + * @since 6.0 + * @see AotContextLoader#loadContextForAotRuntime(MergedContextConfiguration, ApplicationContextInitializer) + */ + @Override + public final GenericWebApplicationContext loadContextForAotRuntime(MergedContextConfiguration mergedConfig, + ApplicationContextInitializer initializer) throws Exception { + + Assert.notNull(mergedConfig, "MergedContextConfiguration must not be null"); + Assert.notNull(initializer, "ApplicationContextInitializer must not be null"); + 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; + } + /** * Load a {@link GenericWebApplicationContext} for the supplied * {@link MergedContextConfiguration}. * @param mergedConfig the merged context configuration to use to load the * application context - * @param refresh whether to refresh the {@code ApplicationContext} and register - * a JVM shutdown hook for it + * @param forAotProcessing {@code true} if the context is being loaded for + * AOT processing, meaning not to refresh the {@code ApplicationContext} or + * register a JVM shutdown hook for it * @return a new web application context * @see org.springframework.test.context.SmartContextLoader#loadContext(MergedContextConfiguration) - * @see org.springframework.test.context.SmartContextLoader#loadContextForAotProcessing(MergedContextConfiguration) + * @see org.springframework.test.context.aot.AotContextLoader#loadContextForAotProcessing(MergedContextConfiguration) */ private final GenericWebApplicationContext loadContext( - MergedContextConfiguration mergedConfig, boolean refresh) throws Exception { + MergedContextConfiguration mergedConfig, boolean forAotProcessing) throws Exception { if (!(mergedConfig instanceof WebMergedContextConfiguration webMergedConfig)) { throw new IllegalArgumentException(""" @@ -178,8 +187,8 @@ public abstract class AbstractGenericWebContextLoader extends AbstractContextLoa } if (logger.isDebugEnabled()) { - logger.debug(String.format("Loading WebApplicationContext for merged context configuration %s.", - webMergedConfig)); + logger.debug("Loading WebApplicationContext %sfor merged context configuration %s" + .formatted((forAotProcessing ? "for AOT processing " : ""), mergedConfig)); } validateMergedContextConfiguration(webMergedConfig); @@ -197,7 +206,7 @@ public abstract class AbstractGenericWebContextLoader extends AbstractContextLoa AnnotationConfigUtils.registerAnnotationConfigProcessors(context); customizeContext(context, webMergedConfig); - if (refresh) { + if (!forAotProcessing) { context.refresh(); context.registerShutdownHook(); } @@ -343,8 +352,6 @@ public abstract class AbstractGenericWebContextLoader extends AbstractContextLoa } - // ContextLoader - /** * {@code AbstractGenericWebContextLoader} should be used as a * {@link org.springframework.test.context.SmartContextLoader SmartContextLoader}, diff --git a/spring-test/src/main/java/org/springframework/test/context/web/AnnotationConfigWebContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/AnnotationConfigWebContextLoader.java index 877d52109a..bd01d72792 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/AnnotationConfigWebContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/AnnotationConfigWebContextLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -16,6 +16,8 @@ package org.springframework.test.context.web; +import java.util.Arrays; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -23,7 +25,6 @@ import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.support.AnnotationConfigContextLoaderUtils; -import org.springframework.util.ObjectUtils; import org.springframework.web.context.support.GenericWebApplicationContext; /** @@ -159,7 +160,7 @@ public class AnnotationConfigWebContextLoader extends AbstractGenericWebContextL Class[] annotatedClasses = webMergedConfig.getClasses(); if (logger.isDebugEnabled()) { - logger.debug("Registering annotated classes: " + ObjectUtils.nullSafeToString(annotatedClasses)); + logger.debug("Registering annotated classes: " + Arrays.toString(annotatedClasses)); } new AnnotatedBeanDefinitionReader(context).register(annotatedClasses); } @@ -173,10 +174,11 @@ public class AnnotationConfigWebContextLoader extends AbstractGenericWebContextL @Override protected void validateMergedContextConfiguration(WebMergedContextConfiguration webMergedConfig) { if (webMergedConfig.hasLocations()) { - String msg = String.format("Test class [%s] has been configured with @ContextConfiguration's 'locations' " + - "(or 'value') attribute %s, but %s does not support resource locations.", - webMergedConfig.getTestClass().getName(), - ObjectUtils.nullSafeToString(webMergedConfig.getLocations()), getClass().getSimpleName()); + String msg = """ + Test class [%s] has been configured with @ContextConfiguration's 'locations' \ + (or 'value') attribute %s, but %s does not support resource locations.""" + .formatted(webMergedConfig.getTestClass().getName(), + Arrays.toString(webMergedConfig.getLocations()), getClass().getSimpleName()); logger.error(msg); throw new IllegalStateException(msg); } diff --git a/spring-test/src/main/java/org/springframework/test/context/web/GenericXmlWebContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/GenericXmlWebContextLoader.java index bf7e38063b..65cebd2ac6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/GenericXmlWebContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/GenericXmlWebContextLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,9 +16,10 @@ package org.springframework.test.context.web; +import java.util.Arrays; + import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.test.context.MergedContextConfiguration; -import org.springframework.util.ObjectUtils; import org.springframework.web.context.support.GenericWebApplicationContext; /** @@ -65,10 +66,11 @@ public class GenericXmlWebContextLoader extends AbstractGenericWebContextLoader @Override protected void validateMergedContextConfiguration(WebMergedContextConfiguration webMergedConfig) { if (webMergedConfig.hasClasses()) { - String msg = String.format( - "Test class [%s] has been configured with @ContextConfiguration's 'classes' attribute %s, " - + "but %s does not support annotated classes.", webMergedConfig.getTestClass().getName(), - ObjectUtils.nullSafeToString(webMergedConfig.getClasses()), getClass().getSimpleName()); + String msg = """ + Test class [%s] has been configured with @ContextConfiguration's 'classes' \ + attribute %s, but %s does not support annotated classes.""" + .formatted(webMergedConfig.getTestClass().getName(), + Arrays.toString(webMergedConfig.getClasses()), getClass().getSimpleName()); logger.error(msg); throw new IllegalStateException(msg); } diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java index a1663ef8f5..be21a0747f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java @@ -29,16 +29,18 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** - * {@code WebMergedContextConfiguration} encapsulates the merged - * context configuration declared on a test class and all of its superclasses - * via {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration}, - * {@link WebAppConfiguration @WebAppConfiguration}, and - * {@link org.springframework.test.context.ActiveProfiles @ActiveProfiles}. + * {@code WebMergedContextConfiguration} encapsulates the merged context + * configuration declared on a test class and all of its superclasses and + * enclosing classes via + * {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration}, + * {@link WebAppConfiguration @WebAppConfiguration}, + * {@link org.springframework.test.context.ActiveProfiles @ActiveProfiles}, and + * {@link org.springframework.test.context.TestPropertySource @TestPropertySource}. * *

{@code WebMergedContextConfiguration} extends the contract of - * {@link MergedContextConfiguration} by adding support for the {@link + * {@link MergedContextConfiguration} by adding support for the {@linkplain * #getResourceBasePath() resource base path} configured via {@code @WebAppConfiguration}. - * This allows the {@link org.springframework.test.context.TestContext TestContext} + * This allows the {@link org.springframework.test.context.cache.ContextCache ContextCache} * to properly cache the corresponding {@link * org.springframework.web.context.WebApplicationContext WebApplicationContext} * that was loaded using properties of this {@code WebMergedContextConfiguration}. @@ -69,7 +71,7 @@ public class WebMergedContextConfiguration extends MergedContextConfiguration { */ public WebMergedContextConfiguration(MergedContextConfiguration mergedConfig, String resourceBasePath) { super(mergedConfig); - this.resourceBasePath = !StringUtils.hasText(resourceBasePath) ? "" : resourceBasePath; + this.resourceBasePath = (StringUtils.hasText(resourceBasePath) ? resourceBasePath : ""); } /**