Skip to content

Commit

Permalink
Introduce DynamicPropertyRegistrar support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed Sep 11, 2024
1 parent 78028cd commit d3f28f1
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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;

/**
* Registrar that is used to add properties with dynamically resolved values to
* the {@code Environment} via a {@link DynamicPropertyRegistry}.
*
* @author Sam Brannen
* @since 6.2
* @see DynamicPropertySource
* @see DynamicPropertyRegistry
*/
@FunctionalInterface
public interface DynamicPropertyRegistrar {

void accept(DynamicPropertyRegistry registry);

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,12 @@
* @since 5.2.5
* @see DynamicPropertiesContextCustomizerFactory
* @see DefaultDynamicPropertyRegistry
* @see DynamicPropertySourceBeanInitializer
* @see DynamicPropertyRegistrarBeanInitializer
*/
class DynamicPropertiesContextCustomizer implements ContextCustomizer {

private static final String DYNAMIC_PROPERTY_REGISTRY_BEAN_NAME =
DynamicPropertiesContextCustomizer.class.getName() + ".dynamicPropertyRegistry";

private static final String DYNAMIC_PROPERTY_SOURCE_BEAN_INITIALIZER_BEAN_NAME =
DynamicPropertiesContextCustomizer.class.getName() + ".dynamicPropertySourceBeanInitializer";
private static final String DYNAMIC_PROPERTY_REGISTRAR_BEAN_INITIALIZER_BEAN_NAME =
DynamicPropertiesContextCustomizer.class.getName() + ".dynamicPropertyRegistrarBeanInitializer";


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

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

DefaultDynamicPropertyRegistry dynamicPropertyRegistry =
new DefaultDynamicPropertyRegistry(environment, this.methods.isEmpty());
beanFactory.registerSingleton(DYNAMIC_PROPERTY_REGISTRY_BEAN_NAME, dynamicPropertyRegistry);

if (!beanDefinitionRegistry.containsBeanDefinition(DYNAMIC_PROPERTY_SOURCE_BEAN_INITIALIZER_BEAN_NAME)) {
BeanDefinition beanDefinition = new RootBeanDefinition(DynamicPropertySourceBeanInitializer.class);
if (!beanDefinitionRegistry.containsBeanDefinition(DYNAMIC_PROPERTY_REGISTRAR_BEAN_INITIALIZER_BEAN_NAME)) {
BeanDefinition beanDefinition = new RootBeanDefinition(DynamicPropertyRegistrarBeanInitializer.class);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
beanDefinitionRegistry.registerBeanDefinition(
DYNAMIC_PROPERTY_SOURCE_BEAN_INITIALIZER_BEAN_NAME, beanDefinition);
DYNAMIC_PROPERTY_REGISTRAR_BEAN_INITIALIZER_BEAN_NAME, beanDefinition);
}

if (!this.methods.isEmpty()) {
ConfigurableEnvironment environment = context.getEnvironment();
DefaultDynamicPropertyRegistry dynamicPropertyRegistry =
new DefaultDynamicPropertyRegistry(environment, false);
MutablePropertySources propertySources = environment.getPropertySources();
propertySources.addFirst(new DynamicValuesPropertySource(dynamicPropertyRegistry.valueSuppliers));
this.methods.forEach(method -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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.support;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.BeanFactoryInitializer;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.lang.Nullable;
import org.springframework.test.context.DynamicPropertyRegistrar;

/**
* Internal component which eagerly initializes {@link DynamicPropertyRegistrar}
* beans.
*
* @author Sam Brannen
* @since 6.2
*/
class DynamicPropertyRegistrarBeanInitializer implements BeanFactoryInitializer<ListableBeanFactory>, EnvironmentAware {

private static final Log logger = LogFactory.getLog(DynamicPropertyRegistrarBeanInitializer.class);


@Nullable
private ConfigurableEnvironment environment;


@Override
public void setEnvironment(Environment environment) {
if (!(environment instanceof ConfigurableEnvironment configurableEnvironment)) {
throw new IllegalArgumentException("Environment must be a ConfigurableEnvironment");
}
this.environment = configurableEnvironment;
}

@Override
public void initialize(ListableBeanFactory beanFactory) {
if (this.environment == null) {
throw new IllegalStateException("Environment is required");
}
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
beanFactory, DynamicPropertyRegistrar.class);
if (beanNames.length > 0) {
DefaultDynamicPropertyRegistry dynamicPropertyRegistry =
new DefaultDynamicPropertyRegistry(this.environment, true);

for (String name : beanNames) {
if (logger.isDebugEnabled()) {
logger.debug("Eagerly initializing DynamicPropertyRegistrar bean '%s'".formatted(name));
}
DynamicPropertyRegistrar registrar = beanFactory.getBean(name, DynamicPropertyRegistrar.class);
registrar.accept(dynamicPropertyRegistry);
}
}
}

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
@Suite
@SelectClasses(
value = {
DynamicPropertyRegistryIntegrationTests.class,
DynamicPropertyRegistrarIntegrationTests.class,
DynamicPropertySourceIntegrationTests.class
},
names = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,29 @@
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatRuntimeException;

/**
* Integration tests for {@link DynamicPropertyRegistry} bean support.
* Integration tests for {@link DynamicPropertyRegistrar} bean support.
*
* @author Sam Brannen
* @since 6.2
* @see DynamicPropertySourceIntegrationTests
*/
@SpringJUnitConfig
@TestPropertySource(properties = "api.url: https://example.com/test")
class DynamicPropertyRegistryIntegrationTests {
class DynamicPropertyRegistrarIntegrationTests {

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

@Test
void customDynamicPropertyRegistryCanExistInApplicationContext(
@Autowired DynamicPropertyRegistry dynamicPropertyRegistry) {

assertThatRuntimeException()
.isThrownBy(() -> dynamicPropertyRegistry.add("test", () -> "test"))
.withMessage("Boom!");
}

@Test
void dynamicPropertySourceOverridesTestPropertySource(@Autowired ConfigurableEnvironment env) {
Expand Down Expand Up @@ -90,16 +99,25 @@ private static void assertApiUrlIsDynamic(String apiUrl) {
@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
@Bean
ApiServer apiServer() {
return new ApiServer();
}

// Accepting ApiServer as a method argument ensures that the apiServer
// 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;
DynamicPropertyRegistrar apiServerProperties(ApiServer apiServer) {
return registry -> registry.add(API_URL, apiServer::getUrl);
}

@Bean
DynamicPropertyRegistry dynamicPropertyRegistry() {
return (name, valueSupplier) -> {
throw new RuntimeException("Boom!");
};
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,24 @@

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.MutablePropertySources;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;

/**
* Integration tests for {@link DynamicPropertySource @DynamicPropertySource}.
* Integration tests for {@link DynamicPropertySource @DynamicPropertySource} and
* {@link DynamicPropertyRegistrar} bean support.
*
* @author Phillip Webb
* @author Sam Brannen
* @see DynamicPropertyRegistryIntegrationTests
* @see DynamicPropertyRegistrarIntegrationTests
*/
@SpringJUnitConfig
@TestPropertySource(properties = "test.container.ip: test")
Expand All @@ -54,8 +57,12 @@ class DynamicPropertySourceIntegrationTests {
static final DemoContainer container = new DemoContainer();

@DynamicPropertySource
static void containerProperties(DynamicPropertyRegistry registry) {
static void containerPropertiesIpAddress(DynamicPropertyRegistry registry) {
registry.add(TEST_CONTAINER_IP, container::getIpAddress);
}

@DynamicPropertySource
static void containerPropertiesPort(DynamicPropertyRegistry registry) {
registry.add("test.container.port", container::getPort);
}

Expand All @@ -65,6 +72,16 @@ void clearSystemProperty() {
System.clearProperty(TEST_CONTAINER_IP);
}

@Test
@DisplayName("A custom DynamicPropertyRegistry bean can exist in the ApplicationContext")
void customDynamicPropertyRegistryCanExistInApplicationContext(
@Autowired DynamicPropertyRegistry dynamicPropertyRegistry) {

assertThatRuntimeException()
.isThrownBy(() -> dynamicPropertyRegistry.add("test", () -> "test"))
.withMessage("Boom!");
}

@Test
@DisplayName("@DynamicPropertySource overrides @TestPropertySource and JVM system property")
void dynamicPropertySourceOverridesTestPropertySourceAndSystemProperty(@Autowired ConfigurableEnvironment env) {
Expand All @@ -74,9 +91,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("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");
}

@Test
Expand All @@ -90,6 +109,19 @@ void serviceHasInjectedValues(@Autowired Service service) {
@Configuration
@Import(Service.class)
static class Config {

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

@Bean
DynamicPropertyRegistry dynamicPropertyRegistry() {
return (name, valueSupplier) -> {
throw new RuntimeException("Boom!");
};
}

}

static class Service {
Expand Down
Loading

0 comments on commit d3f28f1

Please sign in to comment.