Register DynamicPropertyRegistry as singleton bean in test ApplicationContext

Prior to this commit, DynamicPropertyRegistry could only be used with a
static @⁠DynamicPropertySource method in an integration test class;
however, it can also be useful to be able to register a "dynamic
property" from within a test's ApplicationContext -- for example, in a
@⁠Bean method in a @⁠Configuration class that is specific to testing
scenarios.

To support such use cases, this commit updates the dynamic property
source infrastructure so that a DynamicPropertyRegistry is always
registered as a singleton bean in a test's ApplicationContext. This
allows DynamicPropertyRegistry to be autowired into a @⁠Configuration
class or supplied to a @⁠Bean method as an argument as shown in the
following example.

@⁠Bean
@⁠DynamicPropertySource
ApiServer apiServer(DynamicPropertyRegistry registry) {
	ApiServer apiServer = new ApiServer();
	registry.add("api.url", apiServer::getUrl);
	return apiServer;
}

Note that the use of @⁠DynamicPropertySource on the @⁠Bean method is
optional and results in the corresponding bean being eagerly
initialized so that other singleton beans in the context can be given
access to the dynamic properties sourced from that bean when those
other beans are initialized.

Side note: DynamicPropertySourceBeanInitializer temporarily implements
LoadTimeWeaverAware since doing so is currently the only way to have a
component eagerly initialized before the
ConfigurableListableBeanFactory.preInstantiateSingletons() phase.
However, we plan to introduce a first-class callback to support such
use cases in the future.

Closes gh-32271
This commit is contained in:
Sam Brannen
2024-02-07 16:24:35 +01:00
parent 7d8279afec
commit 6cdb34410b
15 changed files with 507 additions and 89 deletions

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link DynamicPropertyRegistry} bean support.
*
* @author Sam Brannen
* @since 6.2
* @see DynamicPropertySourceIntegrationTests
*/
@SpringJUnitConfig
@TestPropertySource(properties = "api.url: https://example.com/test")
class DynamicPropertyRegistryIntegrationTests {
private static final String API_URL = "api.url";
@Test
void dynamicPropertySourceOverridesTestPropertySource(@Autowired ConfigurableEnvironment env) {
assertApiUrlIsDynamic(env.getProperty(API_URL));
MutablePropertySources propertySources = env.getPropertySources();
assertThat(propertySources.size()).isGreaterThanOrEqualTo(4);
assertThat(propertySources.contains("Inlined Test Properties")).isTrue();
assertThat(propertySources.contains("Dynamic Test Properties")).isTrue();
assertThat(propertySources.get("Inlined Test Properties").getProperty(API_URL)).isEqualTo("https://example.com/test");
assertThat(propertySources.get("Dynamic Test Properties").getProperty(API_URL)).isEqualTo("https://example.com/dynamic");
}
@Test
void testReceivesDynamicProperty(@Value("${api.url}") String apiUrl) {
assertApiUrlIsDynamic(apiUrl);
}
@Test
void environmentInjectedServiceCanRetrieveDynamicProperty(@Autowired EnvironmentInjectedService service) {
assertApiUrlIsDynamic(service);
}
@Test
void constructorInjectedServiceReceivesDynamicProperty(@Autowired ConstructorInjectedService service) {
assertApiUrlIsDynamic(service);
}
@Test
void setterInjectedServiceReceivesDynamicProperty(@Autowired SetterInjectedService service) {
assertApiUrlIsDynamic(service);
}
private static void assertApiUrlIsDynamic(ApiUrlClient service) {
assertApiUrlIsDynamic(service.getApiUrl());
}
private static void assertApiUrlIsDynamic(String apiUrl) {
assertThat(apiUrl).isEqualTo("https://example.com/dynamic");
}
@Configuration
@Import({ EnvironmentInjectedService.class, ConstructorInjectedService.class, SetterInjectedService.class })
static class Config {
// Annotating this @Bean method with @DynamicPropertySource ensures that
// this bean will be instantiated before any other singleton beans in the
// context which further ensures that the dynamic "api.url" property is
// available to all standard singleton beans.
@Bean
@DynamicPropertySource
ApiServer apiServer(DynamicPropertyRegistry registry) {
ApiServer apiServer = new ApiServer();
registry.add(API_URL, apiServer::getUrl);
return apiServer;
}
}
interface ApiUrlClient {
String getApiUrl();
}
static class EnvironmentInjectedService implements ApiUrlClient {
private final Environment env;
EnvironmentInjectedService(Environment env) {
this.env = env;
}
@Override
public String getApiUrl() {
return this.env.getProperty(API_URL);
}
}
static class ConstructorInjectedService implements ApiUrlClient {
private final String apiUrl;
ConstructorInjectedService(@Value("${api.url}") String apiUrl) {
this.apiUrl = apiUrl;
}
@Override
public String getApiUrl() {
return this.apiUrl;
}
}
static class SetterInjectedService implements ApiUrlClient {
private String apiUrl;
@Autowired
void setApiUrl(@Value("${api.url}") String apiUrl) {
this.apiUrl = apiUrl;
}
@Override
public String getApiUrl() {
return this.apiUrl;
}
}
static class ApiServer {
String getUrl() {
return "https://example.com/dynamic";
}
}
}

View File

@@ -37,6 +37,7 @@ import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
*
* @author Phillip Webb
* @author Sam Brannen
* @see DynamicPropertyRegistryIntegrationTests
*/
@SpringJUnitConfig
@TestPropertySource(properties = "test.container.ip: test")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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,6 +40,7 @@ abstract class AbstractAotTests {
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests__TestContext001_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext001_BeanDefinitions.java",
// BasicSpringJupiterSharedConfigTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext002_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext002_BeanDefinitions.java",
@@ -50,6 +51,7 @@ abstract class AbstractAotTests {
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext002_ManagementBeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext002_BeanDefinitions.java",
// BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests.
// BasicSpringJupiterTests.NestedTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext003_BeanDefinitions.java",
@@ -61,24 +63,28 @@ abstract class AbstractAotTests {
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext003_ManagementBeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext003_BeanDefinitions.java",
// BasicSpringTestNGTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext004_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext004_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext004_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext004_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext004_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext004_BeanDefinitions.java",
// BasicSpringVintageTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext005_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext005_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext005_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext005_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext005_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext005_BeanDefinitions.java",
// DisabledInAotRuntimeMethodLevelTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext006_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext006_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/DisabledInAotRuntimeMethodLevelTests__TestContext006_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/DisabledInAotRuntimeMethodLevelTests__TestContext006_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/DisabledInAotRuntimeMethodLevelTests__TestContext006_BeanFactoryRegistrations.java"
"org/springframework/test/context/aot/samples/basic/DisabledInAotRuntimeMethodLevelTests__TestContext006_BeanFactoryRegistrations.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext006_BeanDefinitions.java"
};
Stream<Class<?>> scan() {

View File

@@ -395,6 +395,7 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests {
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext001_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext001_ManagementBeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext001_BeanDefinitions.java",
// BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests.
// BasicSpringJupiterTests.NestedTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext002_BeanDefinitions.java",
@@ -406,24 +407,28 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests {
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext002_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext002_ManagementBeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext002_BeanDefinitions.java",
// BasicSpringTestNGTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext003_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext003_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext003_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext003_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext003_BeanDefinitions.java",
// BasicSpringVintageTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext004_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext004_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext004_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext004_BeanFactoryRegistrations.java",
"org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext004_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext004_BeanDefinitions.java",
// SqlScriptsSpringJupiterTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext005_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext005_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext005_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext005_BeanFactoryRegistrations.java",
"org/springframework/test/context/jdbc/EmptyDatabaseConfig__TestContext005_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext005_BeanDefinitions.java",
// WebSpringJupiterTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext006_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext006_BeanDefinitions.java",
@@ -432,12 +437,14 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests {
"org/springframework/test/context/aot/samples/web/WebTestConfiguration__TestContext006_BeanDefinitions.java",
"org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext006_Autowiring.java",
"org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext006_BeanDefinitions.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext006_BeanDefinitions.java",
// XmlSpringJupiterTests
"org/springframework/context/event/DefaultEventListenerFactory__TestContext007_BeanDefinitions.java",
"org/springframework/context/event/EventListenerMethodProcessor__TestContext007_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/common/DefaultMessageService__TestContext007_BeanDefinitions.java",
"org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext007_ApplicationContextInitializer.java",
"org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext007_BeanFactoryRegistrations.java"
"org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext007_BeanFactoryRegistrations.java",
"org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext007_BeanDefinitions.java",
};
}

View File

@@ -36,7 +36,6 @@ import org.springframework.test.context.web.WebDelegatingSmartContextLoader;
import org.springframework.test.context.web.WebMergedContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link BootstrapTestUtils} involving {@link MergedContextConfiguration}.
@@ -59,10 +58,14 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt
*/
@Test
void buildMergedConfigWithContextConfigurationWithoutLocationsClassesOrInitializers() {
assertThatIllegalStateException().isThrownBy(() ->
buildMergedContextConfiguration(MissingContextAttributesTestCase.class))
.withMessageStartingWith("DelegatingSmartContextLoader was unable to detect defaults, "
+ "and no ApplicationContextInitializers or ContextCustomizers were declared for context configuration attributes");
Class<?> testClass = MissingContextAttributesTestCase.class;
MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass);
assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, DelegatingSmartContextLoader.class);
assertThat(mergedConfig.getContextCustomizers())
.map(Object::getClass)
.map(Class::getSimpleName)
.containsOnly("DynamicPropertiesContextCustomizer");
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 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,10 +40,11 @@ class DynamicPropertiesContextCustomizerFactoryTests {
private final List<ContextConfigurationAttributes> configAttributes = Collections.emptyList();
@Test
void createContextCustomizerWhenNoAnnotatedMethodsReturnsNull() {
void createContextCustomizerWhenNoAnnotatedMethodsReturnsCustomizerWithEmptyMethods() {
DynamicPropertiesContextCustomizer customizer = this.factory.createContextCustomizer(
NoDynamicPropertySource.class, this.configAttributes);
assertThat(customizer).isNull();
assertThat(customizer).isNotNull();
assertThat(customizer.getMethods()).isEmpty();
}
@Test

View File

@@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
class DynamicValuesPropertySourceTests {
private final DynamicValuesPropertySource source = new DynamicValuesPropertySource("test",
private final DynamicValuesPropertySource source = new DynamicValuesPropertySource(
Map.of("a", () -> "A", "b", () -> "B"));