Polish Javadoc and internals of the TestContext framework

This commit is contained in:
Sam Brannen 2022-08-26 12:21:28 +02:00
parent c32ff17821
commit e53c7ae6f5
12 changed files with 255 additions and 231 deletions

View File

@ -28,6 +28,10 @@ import org.springframework.test.context.SmartContextLoader;
* {@linkplain #loadContextForAotRuntime AOT execution} for an integration test
* managed by the Spring TestContext Framework.
*
* <p>{@code AotContextLoader} is an extension of the {@link SmartContextLoader}
* SPI that allows a context loader to optionally provide ahead-of-time (AOT)
* support.
*
* <p>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}

View File

@ -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);
}
});

View File

@ -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.
*/

View File

@ -60,12 +60,17 @@ import org.springframework.util.Assert;
* (e.g., XML configuration files and Groovy scripts) or annotated classes,
* but not both simultaneously.
*
* <p>As of Spring 3.2, a test class may optionally declare neither path-based
* <p>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.
*
* <p>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
* </ul>
* @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.
* <p>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:
* <ul>
* <li>If the resource locations in the supplied {@code MergedContextConfiguration}
* are not empty and the annotated classes are empty,
* the XML-based loader will load the {@code ApplicationContext}.</li>
* <li>If the annotated classes in the supplied {@code MergedContextConfiguration}
* are not empty and the resource locations are empty,
* the annotation-based loader will load the {@code ApplicationContext}.</li>
* </ul>
* 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<ConfigurableApplicationContext> 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.
* <p>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<ConfigurableApplicationContext> 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();
}

View File

@ -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}.
* <p>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<ConfigurableApplicationContext> 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();

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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}.
* <p>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<ConfigurableApplicationContext> 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}.
* <p>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<ConfigurableApplicationContext> 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},

View File

@ -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);
}

View File

@ -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);
}

View File

@ -29,16 +29,18 @@ import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* {@code WebMergedContextConfiguration} encapsulates the <em>merged</em>
* 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 <em>merged</em> 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}.
*
* <p>{@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 : "");
}
/**