Skip to content

Commit

Permalink
Revise DynamicPropertyRegistrar support
Browse files Browse the repository at this point in the history
With this commit, a DynamicValuesPropertySource is now created and
registered with the Environment on demand whenever a
DefaultDynamicPropertyRegistry is created, which only occurs once in
DynamicPropertiesContextCustomizer and once in
DynamicPropertyRegistrarBeanInitializer.

In addition, DefaultDynamicPropertyRegistry now operates on the
valueSuppliers map stored in the singleton DynamicValuesPropertySource
registered with the Environment, which allows @⁠DynamicPropertySource
methods and DynamicPropertyRegistrar beans to transparently populate
the same DynamicValuesPropertySource.

See spring-projectsgh-33501
  • Loading branch information
sbrannen committed Sep 11, 2024
1 parent d3f28f1 commit 0f28d50
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,10 @@

package org.springframework.test.context.support;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;

import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.util.Assert;

Expand All @@ -37,43 +31,21 @@
*/
final class DefaultDynamicPropertyRegistry implements DynamicPropertyRegistry {

final Map<String, Supplier<Object>> valueSuppliers = Collections.synchronizedMap(new LinkedHashMap<>());
private final Map<String, Supplier<Object>> valueSuppliers;

private final ConfigurableEnvironment environment;

private final boolean lazilyRegisterPropertySource;

private final Lock propertySourcesLock = new ReentrantLock();


DefaultDynamicPropertyRegistry(ConfigurableEnvironment environment, boolean lazilyRegisterPropertySource) {
this.environment = environment;
this.lazilyRegisterPropertySource = lazilyRegisterPropertySource;
DefaultDynamicPropertyRegistry(ConfigurableEnvironment environment) {
DynamicValuesPropertySource dynamicValuesPropertySource =
DynamicValuesPropertySource.getOrCreate(environment);
this.valueSuppliers = dynamicValuesPropertySource.valueSuppliers;
}


@Override
public void add(String name, Supplier<Object> valueSupplier) {
Assert.hasText(name, "'name' must not be null or blank");
Assert.notNull(valueSupplier, "'valueSupplier' must not be null");
if (this.lazilyRegisterPropertySource) {
ensurePropertySourceIsRegistered();
}
this.valueSuppliers.put(name, valueSupplier);
}

private void ensurePropertySourceIsRegistered() {
MutablePropertySources propertySources = this.environment.getPropertySources();
this.propertySourcesLock.lock();
try {
PropertySource<?> ps = propertySources.get(DynamicValuesPropertySource.PROPERTY_SOURCE_NAME);
if (ps == null) {
propertySources.addFirst(new DynamicValuesPropertySource(this.valueSuppliers));
}
}
finally {
this.propertySourcesLock.unlock();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,20 @@
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

/**
* {@link ContextCustomizer} which supports
* {@link DynamicPropertySource @DynamicPropertySource} methods and registers a
* {@link DynamicPropertyRegistry} as a singleton bean in the container for use
* in {@code @Configuration} classes and {@code @Bean} methods.
* {@link org.springframework.test.context.DynamicPropertySource @DynamicPropertySource}
* methods and registers a {@link DynamicPropertyRegistrarBeanInitializer} in the
* container to eagerly initialize
* {@link org.springframework.test.context.DynamicPropertyRegistrar DynamicPropertyRegistrar}
* beans.
*
* @author Phillip Webb
* @author Sam Brannen
Expand Down Expand Up @@ -79,10 +79,7 @@ public void customizeContext(ConfigurableApplicationContext context, MergedConte

if (!this.methods.isEmpty()) {
ConfigurableEnvironment environment = context.getEnvironment();
DefaultDynamicPropertyRegistry dynamicPropertyRegistry =
new DefaultDynamicPropertyRegistry(environment, false);
MutablePropertySources propertySources = environment.getPropertySources();
propertySources.addFirst(new DynamicValuesPropertySource(dynamicPropertyRegistry.valueSuppliers));
DefaultDynamicPropertyRegistry dynamicPropertyRegistry = new DefaultDynamicPropertyRegistry(environment);
this.methods.forEach(method -> {
ReflectionUtils.makeAccessible(method);
ReflectionUtils.invokeMethod(method, null, dynamicPropertyRegistry);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ public void initialize(ListableBeanFactory beanFactory) {
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
beanFactory, DynamicPropertyRegistrar.class);
if (beanNames.length > 0) {
DefaultDynamicPropertyRegistry dynamicPropertyRegistry =
new DefaultDynamicPropertyRegistry(this.environment, true);

DefaultDynamicPropertyRegistry dynamicPropertyRegistry = new DefaultDynamicPropertyRegistry(this.environment);
for (String name : beanNames) {
if (logger.isDebugEnabled()) {
logger.debug("Eagerly initializing DynamicPropertyRegistrar bean '%s'".formatted(name));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@
package org.springframework.test.context.support;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;

import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.lang.Nullable;
import org.springframework.util.function.SupplierUtils;

Expand All @@ -36,9 +42,18 @@ class DynamicValuesPropertySource extends MapPropertySource {

static final String PROPERTY_SOURCE_NAME = "Dynamic Test Properties";

private static final Lock propertySourcesLock = new ReentrantLock();

final Map<String, Supplier<Object>> valueSuppliers;


DynamicValuesPropertySource() {
this(Collections.synchronizedMap(new LinkedHashMap<>()));
}

DynamicValuesPropertySource(Map<String, Supplier<Object>> valueSuppliers) {
super(PROPERTY_SOURCE_NAME, Collections.unmodifiableMap(valueSuppliers));
this.valueSuppliers = valueSuppliers;
}


Expand All @@ -48,4 +63,29 @@ public Object getProperty(String name) {
return SupplierUtils.resolve(super.getProperty(name));
}


static DynamicValuesPropertySource getOrCreate(ConfigurableEnvironment environment) {
propertySourcesLock.lock();
try {
MutablePropertySources propertySources = environment.getPropertySources();
PropertySource<?> ps = propertySources.get(PROPERTY_SOURCE_NAME);
if (ps instanceof DynamicValuesPropertySource dynamicValuesPropertySource) {
return dynamicValuesPropertySource;
}
else if (ps == null) {
DynamicValuesPropertySource dynamicValuesPropertySource = new DynamicValuesPropertySource();
propertySources.addFirst(dynamicValuesPropertySource);
return dynamicValuesPropertySource;
}
else {
throw new IllegalStateException(
"PropertySource with name '%s' must be a DynamicValuesPropertySource"
.formatted(PROPERTY_SOURCE_NAME));
}
}
finally {
propertySourcesLock.unlock();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@
* @see DynamicPropertySourceIntegrationTests
*/
@SpringJUnitConfig
@TestPropertySource(properties = "api.url: https://example.com/test")
@TestPropertySource(properties = "api.url.1: https://example.com/test")
class DynamicPropertyRegistrarIntegrationTests {

private static final String API_URL = "api.url";
private static final String API_URL_1 = "api.url.1";
private static final String API_URL_2 = "api.url.2";


@Test
void customDynamicPropertyRegistryCanExistInApplicationContext(
Expand All @@ -55,43 +57,49 @@ void customDynamicPropertyRegistryCanExistInApplicationContext(

@Test
void dynamicPropertySourceOverridesTestPropertySource(@Autowired ConfigurableEnvironment env) {
assertApiUrlIsDynamic(env.getProperty(API_URL));
assertApiUrlIsDynamic1(env.getProperty(API_URL_1));

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");
assertThat(propertySources.get("Inlined Test Properties").getProperty(API_URL_1)).isEqualTo("https://example.com/test");
assertThat(propertySources.get("Dynamic Test Properties").getProperty(API_URL_1)).isEqualTo("https://example.com/dynamic/1");
assertThat(propertySources.get("Dynamic Test Properties").getProperty(API_URL_2)).isEqualTo("https://example.com/dynamic/2");
}

@Test
void testReceivesDynamicProperty(@Value("${api.url}") String apiUrl) {
assertApiUrlIsDynamic(apiUrl);
void testReceivesDynamicProperties(@Value("${api.url.1}") String apiUrl1, @Value("${api.url.2}") String apiUrl2) {
assertApiUrlIsDynamic1(apiUrl1);
assertApiUrlIsDynamic2(apiUrl2);
}

@Test
void environmentInjectedServiceCanRetrieveDynamicProperty(@Autowired EnvironmentInjectedService service) {
assertApiUrlIsDynamic(service);
assertApiUrlIsDynamic1(service);
}

@Test
void constructorInjectedServiceReceivesDynamicProperty(@Autowired ConstructorInjectedService service) {
assertApiUrlIsDynamic(service);
assertApiUrlIsDynamic1(service);
}

@Test
void setterInjectedServiceReceivesDynamicProperty(@Autowired SetterInjectedService service) {
assertApiUrlIsDynamic(service);
assertApiUrlIsDynamic1(service);
}


private static void assertApiUrlIsDynamic(ApiUrlClient service) {
assertApiUrlIsDynamic(service.getApiUrl());
private static void assertApiUrlIsDynamic1(ApiUrlClient service) {
assertApiUrlIsDynamic1(service.getApiUrl());
}

private static void assertApiUrlIsDynamic1(String apiUrl) {
assertThat(apiUrl).isEqualTo("https://example.com/dynamic/1");
}

private static void assertApiUrlIsDynamic(String apiUrl) {
assertThat(apiUrl).isEqualTo("https://example.com/dynamic");
private static void assertApiUrlIsDynamic2(String apiUrl) {
assertThat(apiUrl).isEqualTo("https://example.com/dynamic/2");
}


Expand All @@ -109,8 +117,13 @@ ApiServer apiServer() {
// context which further ensures that the dynamic "api.url" property is
// available to all standard singleton beans.
@Bean
DynamicPropertyRegistrar apiServerProperties(ApiServer apiServer) {
return registry -> registry.add(API_URL, apiServer::getUrl);
DynamicPropertyRegistrar apiServerProperties1(ApiServer apiServer) {
return registry -> registry.add(API_URL_1, () -> apiServer.getUrl() + "/1");
}

@Bean
DynamicPropertyRegistrar apiServerProperties2(ApiServer apiServer) {
return registry -> registry.add(API_URL_2, () -> apiServer.getUrl() + "/2");
}

@Bean
Expand Down Expand Up @@ -138,7 +151,7 @@ static class EnvironmentInjectedService implements ApiUrlClient {

@Override
public String getApiUrl() {
return this.env.getProperty(API_URL);
return this.env.getProperty(API_URL_1);
}
}

Expand All @@ -147,7 +160,7 @@ static class ConstructorInjectedService implements ApiUrlClient {
private final String apiUrl;


ConstructorInjectedService(@Value("${api.url}") String apiUrl) {
ConstructorInjectedService(@Value("${api.url.1}") String apiUrl) {
this.apiUrl = apiUrl;
}

Expand All @@ -163,7 +176,7 @@ static class SetterInjectedService implements ApiUrlClient {


@Autowired
void setApiUrl(@Value("${api.url}") String apiUrl) {
void setApiUrl(@Value("${api.url.1}") String apiUrl) {
this.apiUrl = apiUrl;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
class DynamicPropertySourceIntegrationTests {

private static final String TEST_CONTAINER_IP = "test.container.ip";
private static final String MAGIC_WORD = "magic.word";

static {
System.setProperty(TEST_CONTAINER_IP, "system");
Expand Down Expand Up @@ -91,11 +92,11 @@ void dynamicPropertySourceOverridesTestPropertySourceAndSystemProperty(@Autowire
assertThat(propertySources.contains("Inlined Test Properties")).isTrue();
assertThat(propertySources.contains("systemProperties")).isTrue();
assertThat(propertySources.get("Dynamic Test Properties").getProperty(TEST_CONTAINER_IP)).isEqualTo("127.0.0.1");
// assertThat(propertySources.get("Dynamic Test Properties").getProperty("magic.word")).isEqualTo("enigma");
assertThat(propertySources.get("Dynamic Test Properties").getProperty(MAGIC_WORD)).isEqualTo("enigma");
assertThat(propertySources.get("Inlined Test Properties").getProperty(TEST_CONTAINER_IP)).isEqualTo("test");
assertThat(propertySources.get("systemProperties").getProperty(TEST_CONTAINER_IP)).isEqualTo("system");
assertThat(env.getProperty(TEST_CONTAINER_IP)).isEqualTo("127.0.0.1");
// assertThat(env.getProperty("magic.word")).isEqualTo("enigma");
assertThat(env.getProperty(MAGIC_WORD)).isEqualTo("enigma");
}

@Test
Expand All @@ -112,7 +113,7 @@ static class Config {

@Bean
DynamicPropertyRegistrar magicWordProperties() {
return registry -> registry.add("magic.word", () -> "engima");
return registry -> registry.add(MAGIC_WORD, () -> "enigma");
}

@Bean
Expand Down

0 comments on commit 0f28d50

Please sign in to comment.