Introduce configurable refresh support in SmartContextLoader SPI

Prior to this commit, the contract of the loadContext() method in the
SmartContextLoader SPI required that the ApplicationContext be returned
in a fully refreshed state with a JVM shutdown hook registered for it.

However, in order to support AOT processing within the Spring
TestContext Framework (TCF), we need a way to signal to
SmartContextLoader implementations that they should load the test's
ApplicationContext without refreshing it or registering a JVM shutdown
hook.

To address this issue, this commit:

- Introduces a new loadContext(MergedContextConfiguration, boolean)
  method. The boolean `refresh` flag controls whether the returned
  ApplicationContext should be refreshed and have a JVM shutdown hook
  registered for it.

- Deprecates the existing loadContext(MergedContextConfiguration)
  method in favor of loadContext(MergedContextConfiguration, boolean).

- Removes all use of the deprecated method within the spring-test
  module, excluding the exception mentioned below.

Note that loadContext(MergedContextConfiguration, boolean) is
implemented as an interface `default` method which delegates to the
deprecated loadContext(MergedContextConfiguration) method for backward
compatibility. When migrating a SmartContextLoader to Spring Framework
6.0, implementations that directly implement the SmartContextLoader SPI
(instead of extending AbstractGenericContextLoader or
AbstractGenericWebContextLoader) will need to override the new
loadContext(MergedContextConfiguration, boolean) method in order to
honor the `refresh` flag for AOT processing support. See the
implementation in AbstractGenericContextLoader for an example of how
this can be achieved.

Closes gh-28906
This commit is contained in:
Sam Brannen
2022-08-03 11:54:52 +03:00
parent dcad3beeb9
commit 903e9f2a02
15 changed files with 373 additions and 189 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 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.
@@ -28,11 +28,11 @@ import org.springframework.context.annotation.Configuration;
*/
public class AnnotatedFooConfigInnerClassTestCase {
@Configuration
@Configuration(proxyBeanMethods = false)
static class FooConfig {
@Bean
public String foo() {
String foo() {
return "foo";
}
}

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,8 +16,11 @@
package org.springframework.test.context.support;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.MergedContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
@@ -31,24 +34,54 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*/
class AnnotationConfigContextLoaderTests {
private final AnnotationConfigContextLoader contextLoader = new AnnotationConfigContextLoader();
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
private final AnnotationConfigContextLoader contextLoader = new AnnotationConfigContextLoader();
/**
* @since 4.0.4
*/
@Test
void configMustNotContainLocations() throws Exception {
void loadContextWithConfigContainingLocationsResultsInException() {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(getClass(),
new String[] { "config.xml" }, EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, contextLoader);
assertThatIllegalStateException().isThrownBy(() ->
contextLoader.loadContext(mergedConfig))
assertThatIllegalStateException()
.isThrownBy(() -> contextLoader.loadContext(mergedConfig, true))
.withMessageContaining("does not support resource locations");
}
/**
* @since 6.0
*/
@Test
void loadContextHonorsRefreshTrue() throws Exception {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(
AnnotatedFooConfigInnerClassTestCase.class, EMPTY_STRING_ARRAY,
new Class<?>[] {AnnotatedFooConfigInnerClassTestCase.FooConfig.class},
EMPTY_STRING_ARRAY, contextLoader);
ConfigurableApplicationContext context = contextLoader.loadContext(mergedConfig, true);
assertThat(context).isNotNull();
assertThat(context.isActive()).as("ApplicationContext is active").isTrue();
assertThat(context.getBean(String.class)).isEqualTo("foo");
}
/**
* @since 6.0
*/
@Test
void loadContextHonorsRefreshFalse() throws Exception {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(
AnnotatedFooConfigInnerClassTestCase.class, EMPTY_STRING_ARRAY,
new Class<?>[] {AnnotatedFooConfigInnerClassTestCase.FooConfig.class},
EMPTY_STRING_ARRAY, contextLoader);
ConfigurableApplicationContext context = contextLoader.loadContext(mergedConfig, false);
assertThat(context).isNotNull();
assertThat(context.isActive()).as("ApplicationContext is active").isFalse();
assertThat(Arrays.stream(context.getBeanDefinitionNames())).anyMatch(name -> name.contains("FooConfig"));
}
@Test
void detectDefaultConfigurationClassesForAnnotatedInnerClass() {
Class<?>[] configClasses = contextLoader.detectDefaultConfigurationClasses(ContextConfigurationInnerClassTestCase.class);

View File

@@ -53,7 +53,7 @@ class CustomizedGenericXmlContextLoaderTests {
MergedContextConfiguration mergedConfig =
new MergedContextConfiguration(getClass(), null, null, null, null);
customLoader.loadContext(mergedConfig);
customLoader.loadContext(mergedConfig, true);
assertThat(customizeInvoked).as("customizeContext() should have been invoked").isTrue();
}

View File

@@ -16,6 +16,9 @@
package org.springframework.test.context.support;
import java.util.Arrays;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
@@ -25,7 +28,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.util.ObjectUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -45,135 +47,167 @@ class DelegatingSmartContextLoaderTests {
private final DelegatingSmartContextLoader loader = new DelegatingSmartContextLoader();
@Nested
class SmartContextLoaderSpiTests {
@Test
void processContextConfigurationWithDefaultXmlConfigGeneration() {
ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(
XmlTestCase.class, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, true, null, true, ContextLoader.class);
loader.processContextConfiguration(configAttributes);
assertThat(configAttributes.getLocations()).hasSize(1);
assertThat(configAttributes.getClasses()).isEmpty();
}
@Test
void processContextConfigurationWithDefaultConfigurationClassGeneration() {
ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(
ConfigClassTestCase.class, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, true, null, true, ContextLoader.class);
loader.processContextConfiguration(configAttributes);
assertThat(configAttributes.getClasses()).hasSize(1);
assertThat(configAttributes.getLocations()).isEmpty();
}
@Test
void processContextConfigurationWithDefaultXmlConfigAndConfigurationClassGeneration() {
ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(
ImproperDuplicateDefaultXmlAndConfigClassTestCase.class, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY,
true, null, true, ContextLoader.class);
assertThatIllegalStateException()
.isThrownBy(() -> loader.processContextConfiguration(configAttributes))
.withMessageContaining("both default locations AND default configuration classes were detected");
}
@Test
void processContextConfigurationWithLocation() {
String[] locations = new String[] {"classpath:/foo.xml"};
ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(
getClass(), locations, EMPTY_CLASS_ARRAY, true, null, true, ContextLoader.class);
loader.processContextConfiguration(configAttributes);
assertThat(configAttributes.getLocations()).isEqualTo(locations);
assertThat(configAttributes.getClasses()).isEmpty();
}
@Test
void processContextConfigurationWithConfigurationClass() {
Class<?>[] classes = new Class<?>[] {getClass()};
ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(
getClass(), EMPTY_STRING_ARRAY, classes, true, null, true, ContextLoader.class);
loader.processContextConfiguration(configAttributes);
assertThat(configAttributes.getClasses()).isEqualTo(classes);
assertThat(configAttributes.getLocations()).isEmpty();
}
@Test
void loadContextWithNullConfig() throws Exception {
assertThatIllegalArgumentException().isThrownBy(() -> loader.loadContext(null, true));
}
@Test
void loadContextWithoutLocationsAndConfigurationClasses() throws Exception {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(
getClass(), EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader);
assertThatIllegalStateException()
.isThrownBy(() -> loader.loadContext(mergedConfig, true))
.withMessageStartingWith("Neither")
.withMessageContaining("was able to load an ApplicationContext from");
}
/**
* @since 4.1
*/
@Test
void loadContextWithLocationsAndConfigurationClasses() throws Exception {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(getClass(),
new String[] {"test.xml"}, new Class<?>[] {getClass()}, EMPTY_STRING_ARRAY, loader);
assertThatIllegalStateException()
.isThrownBy(() -> loader.loadContext(mergedConfig, true))
.withMessageStartingWith("Neither")
.withMessageContaining("declare either 'locations' or 'classes' but not both.");
}
@Test
void loadContextWithXmlConfig() throws Exception {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(
XmlTestCase.class,
new String[] {"classpath:/org/springframework/test/context/support/DelegatingSmartContextLoaderTests$XmlTestCase-context.xml"},
EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader);
assertApplicationContextLoadsAndContainsFooString(mergedConfig);
}
@Test
void loadContextWithConfigurationClass() throws Exception {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(ConfigClassTestCase.class,
EMPTY_STRING_ARRAY, new Class<?>[] {ConfigClassTestCase.Config.class}, EMPTY_STRING_ARRAY, loader);
assertApplicationContextLoadsAndContainsFooString(mergedConfig);
}
private void assertApplicationContextLoadsAndContainsFooString(MergedContextConfiguration mergedConfig)
throws Exception {
ApplicationContext applicationContext = loader.loadContext(mergedConfig, true);
assertThat(applicationContext).isInstanceOf(ConfigurableApplicationContext.class);
assertThat(applicationContext.getBean(String.class)).isEqualTo("foo");
ConfigurableApplicationContext cac = (ConfigurableApplicationContext) applicationContext;
cac.close();
}
/**
* @since 6.0
*/
@Test
void loadContextWithXmlConfigWithoutRefresh() throws Exception {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(
XmlTestCase.class,
new String[] {"classpath:/org/springframework/test/context/support/DelegatingSmartContextLoaderTests$XmlTestCase-context.xml"},
EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader);
assertApplicationContextLoadsWithoutRefresh(mergedConfig, "foo");
}
/**
* @since 6.0
*/
@Test
void loadContextWithConfigurationClassWithoutRefresh() throws Exception {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(ConfigClassTestCase.class,
EMPTY_STRING_ARRAY, new Class<?>[] {ConfigClassTestCase.Config.class}, EMPTY_STRING_ARRAY, loader);
assertApplicationContextLoadsWithoutRefresh(mergedConfig, "ConfigClassTestCase.Config");
}
private void assertApplicationContextLoadsWithoutRefresh(MergedContextConfiguration mergedConfig,
String expectedBeanDefName) throws Exception {
ApplicationContext context = loader.loadContext(mergedConfig, false);
assertThat(context).isInstanceOf(ConfigurableApplicationContext.class);
ConfigurableApplicationContext cac = (ConfigurableApplicationContext) context;
assertThat(cac.isActive()).as("ApplicationContext is active").isFalse();
assertThat(Arrays.stream(context.getBeanDefinitionNames())).anyMatch(name -> name.contains(expectedBeanDefName));
cac.close();
}
private static void assertEmpty(Object[] array) {
assertThat(ObjectUtils.isEmpty(array)).isTrue();
}
// --- SmartContextLoader - processContextConfiguration() ------------------
@Nested
class ContextLoaderSpiTests {
@Test
void processLocations() {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> loader.processLocations(getClass(), EMPTY_STRING_ARRAY));
}
@Test
void loadContextFromLocations() {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> loader.loadContext(EMPTY_STRING_ARRAY));
}
@Test
void processContextConfigurationWithDefaultXmlConfigGeneration() {
ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(
XmlTestCase.class, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, true, null, true, ContextLoader.class);
loader.processContextConfiguration(configAttributes);
assertThat(configAttributes.getLocations().length).isEqualTo(1);
assertEmpty(configAttributes.getClasses());
}
@Test
void processContextConfigurationWithDefaultConfigurationClassGeneration() {
ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(
ConfigClassTestCase.class, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, true, null, true, ContextLoader.class);
loader.processContextConfiguration(configAttributes);
assertThat(configAttributes.getClasses().length).isEqualTo(1);
assertEmpty(configAttributes.getLocations());
}
@Test
void processContextConfigurationWithDefaultXmlConfigAndConfigurationClassGeneration() {
ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(
ImproperDuplicateDefaultXmlAndConfigClassTestCase.class, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY,
true, null, true, ContextLoader.class);
assertThatIllegalStateException().isThrownBy(() ->
loader.processContextConfiguration(configAttributes))
.withMessageContaining("both default locations AND default configuration classes were detected");
}
@Test
void processContextConfigurationWithLocation() {
String[] locations = new String[] {"classpath:/foo.xml"};
ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(
getClass(), locations, EMPTY_CLASS_ARRAY, true, null, true, ContextLoader.class);
loader.processContextConfiguration(configAttributes);
assertThat(configAttributes.getLocations()).isEqualTo(locations);
assertEmpty(configAttributes.getClasses());
}
@Test
void processContextConfigurationWithConfigurationClass() {
Class<?>[] classes = new Class<?>[] {getClass()};
ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(
getClass(), EMPTY_STRING_ARRAY, classes, true, null, true, ContextLoader.class);
loader.processContextConfiguration(configAttributes);
assertThat(configAttributes.getClasses()).isEqualTo(classes);
assertEmpty(configAttributes.getLocations());
}
// --- SmartContextLoader - loadContext() ----------------------------------
@Test
void loadContextWithNullConfig() throws Exception {
assertThatIllegalArgumentException().isThrownBy(() ->
loader.loadContext((MergedContextConfiguration) null));
}
@Test
void loadContextWithoutLocationsAndConfigurationClasses() throws Exception {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(
getClass(), EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader);
assertThatIllegalStateException().isThrownBy(() ->
loader.loadContext(mergedConfig))
.withMessageStartingWith("Neither")
.withMessageContaining("was able to load an ApplicationContext from");
}
/**
* @since 4.1
*/
@Test
void loadContextWithLocationsAndConfigurationClasses() throws Exception {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(getClass(),
new String[] {"test.xml"}, new Class<?>[] {getClass()}, EMPTY_STRING_ARRAY, loader);
assertThatIllegalStateException().isThrownBy(() ->
loader.loadContext(mergedConfig))
.withMessageStartingWith("Neither")
.withMessageContaining("declare either 'locations' or 'classes' but not both.");
}
private void assertApplicationContextLoadsAndContainsFooString(MergedContextConfiguration mergedConfig)
throws Exception {
ApplicationContext applicationContext = loader.loadContext(mergedConfig);
assertThat(applicationContext).isNotNull();
assertThat(applicationContext.getBean(String.class)).isEqualTo("foo");
boolean condition = applicationContext instanceof ConfigurableApplicationContext;
assertThat(condition).isTrue();
((ConfigurableApplicationContext) applicationContext).close();
}
@Test
void loadContextWithXmlConfig() throws Exception {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(
XmlTestCase.class,
new String[] {"classpath:/org/springframework/test/context/support/DelegatingSmartContextLoaderTests$XmlTestCase-context.xml"},
EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader);
assertApplicationContextLoadsAndContainsFooString(mergedConfig);
}
@Test
void loadContextWithConfigurationClass() throws Exception {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(ConfigClassTestCase.class,
EMPTY_STRING_ARRAY, new Class<?>[] {ConfigClassTestCase.Config.class}, EMPTY_STRING_ARRAY, loader);
assertApplicationContextLoadsAndContainsFooString(mergedConfig);
}
// --- ContextLoader -------------------------------------------------------
@Test
void processLocations() {
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() ->
loader.processLocations(getClass(), EMPTY_STRING_ARRAY));
}
@Test
void loadContextFromLocations() {
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() ->
loader.loadContext(EMPTY_STRING_ARRAY));
}
// -------------------------------------------------------------------------
static class XmlTestCase {
}

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.
@@ -40,7 +40,7 @@ class GenericXmlContextLoaderTests {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY,
new Class<?>[] { getClass() }, EMPTY_STRING_ARRAY, loader);
assertThatIllegalStateException()
.isThrownBy(() -> loader.loadContext(mergedConfig))
.isThrownBy(() -> loader.loadContext(mergedConfig, true))
.withMessageContaining("does not support annotated classes");
}

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.
@@ -39,7 +39,7 @@ class AnnotationConfigWebContextLoaderTests {
new String[] { "config.xml" }, EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY,
EMPTY_STRING_ARRAY, "resource/path", loader, null, null);
assertThatIllegalStateException()
.isThrownBy(() -> loader.loadContext(mergedConfig))
.isThrownBy(() -> loader.loadContext(mergedConfig, true))
.withMessageContaining("does not support resource locations");
}

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.
@@ -38,7 +38,7 @@ class GenericXmlWebContextLoaderTests {
new Class<?>[] { getClass() }, null, EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY,
"resource/path", loader, null, null);
assertThatIllegalStateException()
.isThrownBy(() -> loader.loadContext(mergedConfig))
.isThrownBy(() -> loader.loadContext(mergedConfig, true))
.withMessageContaining("does not support annotated classes");
}