Skip to content

Commit d3f28f1

Browse files
committed
Introduce DynamicPropertyRegistrar support
This is a PROOF OF CONCEPT which introduces DynamicPropertyRegistrar support as a replacement for registering a DynamicPropertyRegistry as a bean in the ApplicationContext. It appears that most things work as expected; however, in this POC there are two instances of DefaultDynamicPropertyRegistry that prevent the use of @⁠DynamicPropertySource and DynamicPropertyRegistrar in the same ApplicationContext, since the two features end up writing to two different valueSuppliers maps. The commented-out "magic.word" assertions in DynamicPropertySourceIntegrationTests therefore currently fail. See spring-projectsgh-33501
1 parent 78028cd commit d3f28f1

9 files changed

+194
-88
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context;
18+
19+
/**
20+
* Registrar that is used to add properties with dynamically resolved values to
21+
* the {@code Environment} via a {@link DynamicPropertyRegistry}.
22+
*
23+
* @author Sam Brannen
24+
* @since 6.2
25+
* @see DynamicPropertySource
26+
* @see DynamicPropertyRegistry
27+
*/
28+
@FunctionalInterface
29+
public interface DynamicPropertyRegistrar {
30+
31+
void accept(DynamicPropertyRegistry registry);
32+
33+
}

spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,12 @@
4646
* @since 5.2.5
4747
* @see DynamicPropertiesContextCustomizerFactory
4848
* @see DefaultDynamicPropertyRegistry
49-
* @see DynamicPropertySourceBeanInitializer
49+
* @see DynamicPropertyRegistrarBeanInitializer
5050
*/
5151
class DynamicPropertiesContextCustomizer implements ContextCustomizer {
5252

53-
private static final String DYNAMIC_PROPERTY_REGISTRY_BEAN_NAME =
54-
DynamicPropertiesContextCustomizer.class.getName() + ".dynamicPropertyRegistry";
55-
56-
private static final String DYNAMIC_PROPERTY_SOURCE_BEAN_INITIALIZER_BEAN_NAME =
57-
DynamicPropertiesContextCustomizer.class.getName() + ".dynamicPropertySourceBeanInitializer";
53+
private static final String DYNAMIC_PROPERTY_REGISTRAR_BEAN_INITIALIZER_BEAN_NAME =
54+
DynamicPropertiesContextCustomizer.class.getName() + ".dynamicPropertyRegistrarBeanInitializer";
5855

5956

6057
private final Set<Method> methods;
@@ -68,24 +65,22 @@ class DynamicPropertiesContextCustomizer implements ContextCustomizer {
6865

6966
@Override
7067
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
71-
ConfigurableEnvironment environment = context.getEnvironment();
7268
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
7369
if (!(beanFactory instanceof BeanDefinitionRegistry beanDefinitionRegistry)) {
7470
throw new IllegalStateException("BeanFactory must be a BeanDefinitionRegistry");
7571
}
7672

77-
DefaultDynamicPropertyRegistry dynamicPropertyRegistry =
78-
new DefaultDynamicPropertyRegistry(environment, this.methods.isEmpty());
79-
beanFactory.registerSingleton(DYNAMIC_PROPERTY_REGISTRY_BEAN_NAME, dynamicPropertyRegistry);
80-
81-
if (!beanDefinitionRegistry.containsBeanDefinition(DYNAMIC_PROPERTY_SOURCE_BEAN_INITIALIZER_BEAN_NAME)) {
82-
BeanDefinition beanDefinition = new RootBeanDefinition(DynamicPropertySourceBeanInitializer.class);
73+
if (!beanDefinitionRegistry.containsBeanDefinition(DYNAMIC_PROPERTY_REGISTRAR_BEAN_INITIALIZER_BEAN_NAME)) {
74+
BeanDefinition beanDefinition = new RootBeanDefinition(DynamicPropertyRegistrarBeanInitializer.class);
8375
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
8476
beanDefinitionRegistry.registerBeanDefinition(
85-
DYNAMIC_PROPERTY_SOURCE_BEAN_INITIALIZER_BEAN_NAME, beanDefinition);
77+
DYNAMIC_PROPERTY_REGISTRAR_BEAN_INITIALIZER_BEAN_NAME, beanDefinition);
8678
}
8779

8880
if (!this.methods.isEmpty()) {
81+
ConfigurableEnvironment environment = context.getEnvironment();
82+
DefaultDynamicPropertyRegistry dynamicPropertyRegistry =
83+
new DefaultDynamicPropertyRegistry(environment, false);
8984
MutablePropertySources propertySources = environment.getPropertySources();
9085
propertySources.addFirst(new DynamicValuesPropertySource(dynamicPropertyRegistry.valueSuppliers));
9186
this.methods.forEach(method -> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.support;
18+
19+
import org.apache.commons.logging.Log;
20+
import org.apache.commons.logging.LogFactory;
21+
22+
import org.springframework.beans.factory.BeanFactoryInitializer;
23+
import org.springframework.beans.factory.BeanFactoryUtils;
24+
import org.springframework.beans.factory.ListableBeanFactory;
25+
import org.springframework.context.EnvironmentAware;
26+
import org.springframework.core.env.ConfigurableEnvironment;
27+
import org.springframework.core.env.Environment;
28+
import org.springframework.lang.Nullable;
29+
import org.springframework.test.context.DynamicPropertyRegistrar;
30+
31+
/**
32+
* Internal component which eagerly initializes {@link DynamicPropertyRegistrar}
33+
* beans.
34+
*
35+
* @author Sam Brannen
36+
* @since 6.2
37+
*/
38+
class DynamicPropertyRegistrarBeanInitializer implements BeanFactoryInitializer<ListableBeanFactory>, EnvironmentAware {
39+
40+
private static final Log logger = LogFactory.getLog(DynamicPropertyRegistrarBeanInitializer.class);
41+
42+
43+
@Nullable
44+
private ConfigurableEnvironment environment;
45+
46+
47+
@Override
48+
public void setEnvironment(Environment environment) {
49+
if (!(environment instanceof ConfigurableEnvironment configurableEnvironment)) {
50+
throw new IllegalArgumentException("Environment must be a ConfigurableEnvironment");
51+
}
52+
this.environment = configurableEnvironment;
53+
}
54+
55+
@Override
56+
public void initialize(ListableBeanFactory beanFactory) {
57+
if (this.environment == null) {
58+
throw new IllegalStateException("Environment is required");
59+
}
60+
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
61+
beanFactory, DynamicPropertyRegistrar.class);
62+
if (beanNames.length > 0) {
63+
DefaultDynamicPropertyRegistry dynamicPropertyRegistry =
64+
new DefaultDynamicPropertyRegistry(this.environment, true);
65+
66+
for (String name : beanNames) {
67+
if (logger.isDebugEnabled()) {
68+
logger.debug("Eagerly initializing DynamicPropertyRegistrar bean '%s'".formatted(name));
69+
}
70+
DynamicPropertyRegistrar registrar = beanFactory.getBean(name, DynamicPropertyRegistrar.class);
71+
registrar.accept(dynamicPropertyRegistry);
72+
}
73+
}
74+
}
75+
76+
}

spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertySourceBeanInitializer.java

Lines changed: 0 additions & 48 deletions
This file was deleted.

spring-test/src/test/java/org/springframework/test/context/DynamicPropertiesTestSuite.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
@Suite
4242
@SelectClasses(
4343
value = {
44-
DynamicPropertyRegistryIntegrationTests.class,
44+
DynamicPropertyRegistrarIntegrationTests.class,
4545
DynamicPropertySourceIntegrationTests.class
4646
},
4747
names = {
Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,29 @@
2929
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
3030

3131
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
3233

3334
/**
34-
* Integration tests for {@link DynamicPropertyRegistry} bean support.
35+
* Integration tests for {@link DynamicPropertyRegistrar} bean support.
3536
*
3637
* @author Sam Brannen
3738
* @since 6.2
3839
* @see DynamicPropertySourceIntegrationTests
3940
*/
4041
@SpringJUnitConfig
4142
@TestPropertySource(properties = "api.url: https://example.com/test")
42-
class DynamicPropertyRegistryIntegrationTests {
43+
class DynamicPropertyRegistrarIntegrationTests {
4344

4445
private static final String API_URL = "api.url";
4546

47+
@Test
48+
void customDynamicPropertyRegistryCanExistInApplicationContext(
49+
@Autowired DynamicPropertyRegistry dynamicPropertyRegistry) {
50+
51+
assertThatRuntimeException()
52+
.isThrownBy(() -> dynamicPropertyRegistry.add("test", () -> "test"))
53+
.withMessage("Boom!");
54+
}
4655

4756
@Test
4857
void dynamicPropertySourceOverridesTestPropertySource(@Autowired ConfigurableEnvironment env) {
@@ -90,16 +99,25 @@ private static void assertApiUrlIsDynamic(String apiUrl) {
9099
@Import({ EnvironmentInjectedService.class, ConstructorInjectedService.class, SetterInjectedService.class })
91100
static class Config {
92101

93-
// Annotating this @Bean method with @DynamicPropertySource ensures that
94-
// this bean will be instantiated before any other singleton beans in the
102+
@Bean
103+
ApiServer apiServer() {
104+
return new ApiServer();
105+
}
106+
107+
// Accepting ApiServer as a method argument ensures that the apiServer
108+
// bean will be instantiated before any other singleton beans in the
95109
// context which further ensures that the dynamic "api.url" property is
96110
// available to all standard singleton beans.
97111
@Bean
98-
@DynamicPropertySource
99-
ApiServer apiServer(DynamicPropertyRegistry registry) {
100-
ApiServer apiServer = new ApiServer();
101-
registry.add(API_URL, apiServer::getUrl);
102-
return apiServer;
112+
DynamicPropertyRegistrar apiServerProperties(ApiServer apiServer) {
113+
return registry -> registry.add(API_URL, apiServer::getUrl);
114+
}
115+
116+
@Bean
117+
DynamicPropertyRegistry dynamicPropertyRegistry() {
118+
return (name, valueSupplier) -> {
119+
throw new RuntimeException("Boom!");
120+
};
103121
}
104122

105123
}

spring-test/src/test/java/org/springframework/test/context/DynamicPropertySourceIntegrationTests.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,24 @@
2323

2424
import org.springframework.beans.factory.annotation.Autowired;
2525
import org.springframework.beans.factory.annotation.Value;
26+
import org.springframework.context.annotation.Bean;
2627
import org.springframework.context.annotation.Configuration;
2728
import org.springframework.context.annotation.Import;
2829
import org.springframework.core.env.ConfigurableEnvironment;
2930
import org.springframework.core.env.MutablePropertySources;
3031
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
3132

3233
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
3335
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
3436

3537
/**
36-
* Integration tests for {@link DynamicPropertySource @DynamicPropertySource}.
38+
* Integration tests for {@link DynamicPropertySource @DynamicPropertySource} and
39+
* {@link DynamicPropertyRegistrar} bean support.
3740
*
3841
* @author Phillip Webb
3942
* @author Sam Brannen
40-
* @see DynamicPropertyRegistryIntegrationTests
43+
* @see DynamicPropertyRegistrarIntegrationTests
4144
*/
4245
@SpringJUnitConfig
4346
@TestPropertySource(properties = "test.container.ip: test")
@@ -54,8 +57,12 @@ class DynamicPropertySourceIntegrationTests {
5457
static final DemoContainer container = new DemoContainer();
5558

5659
@DynamicPropertySource
57-
static void containerProperties(DynamicPropertyRegistry registry) {
60+
static void containerPropertiesIpAddress(DynamicPropertyRegistry registry) {
5861
registry.add(TEST_CONTAINER_IP, container::getIpAddress);
62+
}
63+
64+
@DynamicPropertySource
65+
static void containerPropertiesPort(DynamicPropertyRegistry registry) {
5966
registry.add("test.container.port", container::getPort);
6067
}
6168

@@ -65,6 +72,16 @@ void clearSystemProperty() {
6572
System.clearProperty(TEST_CONTAINER_IP);
6673
}
6774

75+
@Test
76+
@DisplayName("A custom DynamicPropertyRegistry bean can exist in the ApplicationContext")
77+
void customDynamicPropertyRegistryCanExistInApplicationContext(
78+
@Autowired DynamicPropertyRegistry dynamicPropertyRegistry) {
79+
80+
assertThatRuntimeException()
81+
.isThrownBy(() -> dynamicPropertyRegistry.add("test", () -> "test"))
82+
.withMessage("Boom!");
83+
}
84+
6885
@Test
6986
@DisplayName("@DynamicPropertySource overrides @TestPropertySource and JVM system property")
7087
void dynamicPropertySourceOverridesTestPropertySourceAndSystemProperty(@Autowired ConfigurableEnvironment env) {
@@ -74,9 +91,11 @@ void dynamicPropertySourceOverridesTestPropertySourceAndSystemProperty(@Autowire
7491
assertThat(propertySources.contains("Inlined Test Properties")).isTrue();
7592
assertThat(propertySources.contains("systemProperties")).isTrue();
7693
assertThat(propertySources.get("Dynamic Test Properties").getProperty(TEST_CONTAINER_IP)).isEqualTo("127.0.0.1");
94+
// assertThat(propertySources.get("Dynamic Test Properties").getProperty("magic.word")).isEqualTo("enigma");
7795
assertThat(propertySources.get("Inlined Test Properties").getProperty(TEST_CONTAINER_IP)).isEqualTo("test");
7896
assertThat(propertySources.get("systemProperties").getProperty(TEST_CONTAINER_IP)).isEqualTo("system");
7997
assertThat(env.getProperty(TEST_CONTAINER_IP)).isEqualTo("127.0.0.1");
98+
// assertThat(env.getProperty("magic.word")).isEqualTo("enigma");
8099
}
81100

82101
@Test
@@ -90,6 +109,19 @@ void serviceHasInjectedValues(@Autowired Service service) {
90109
@Configuration
91110
@Import(Service.class)
92111
static class Config {
112+
113+
@Bean
114+
DynamicPropertyRegistrar magicWordProperties() {
115+
return registry -> registry.add("magic.word", () -> "engima");
116+
}
117+
118+
@Bean
119+
DynamicPropertyRegistry dynamicPropertyRegistry() {
120+
return (name, valueSupplier) -> {
121+
throw new RuntimeException("Boom!");
122+
};
123+
}
124+
93125
}
94126

95127
static class Service {

0 commit comments

Comments
 (0)