diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultDynamicPropertyRegistry.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultDynamicPropertyRegistry.java new file mode 100644 index 00000000000..e079c379541 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultDynamicPropertyRegistry.java @@ -0,0 +1,79 @@ +/* + * 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 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; + +/** + * Default {@link DynamicPropertyRegistry} implementation. + * + * @author Sam Brannen + * @since 6.2 + */ +final class DefaultDynamicPropertyRegistry implements DynamicPropertyRegistry { + + final Map> valueSuppliers = Collections.synchronizedMap(new LinkedHashMap<>()); + + 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; + } + + + @Override + public void add(String name, Supplier 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(); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java index dce421cb467..ed6df381b1f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java @@ -26,7 +26,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.PropertySource; +import org.springframework.core.env.MutablePropertySources; import org.springframework.lang.Nullable; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.DynamicPropertyRegistry; @@ -75,33 +75,35 @@ class DynamicPropertiesContextCustomizer implements ContextCustomizer { @Override public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { - DynamicValuesPropertySource propertySource = getOrAdd(context.getEnvironment()); - - if (!context.containsBean(DYNAMIC_PROPERTY_REGISTRY_BEAN_NAME)) { - ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); - beanFactory.registerSingleton(DYNAMIC_PROPERTY_REGISTRY_BEAN_NAME, propertySource.dynamicPropertyRegistry); + ConfigurableEnvironment environment = context.getEnvironment(); + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + if (!(beanFactory instanceof BeanDefinitionRegistry beanDefinitionRegistry)) { + throw new IllegalStateException("BeanFactory must be a BeanDefinitionRegistry"); } - if (!context.containsBean(DYNAMIC_PROPERTY_SOURCE_BEAN_INITIALIZER_BEAN_NAME)) { - if (!(context.getBeanFactory() instanceof BeanDefinitionRegistry registry)) { - throw new IllegalStateException("BeanFactory must be a BeanDefinitionRegistry"); - } - BeanDefinition beanDefinition = new RootBeanDefinition(DynamicPropertySourceBeanInitializer.class); - beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - registry.registerBeanDefinition(DYNAMIC_PROPERTY_SOURCE_BEAN_INITIALIZER_BEAN_NAME, beanDefinition); - } + DefaultDynamicPropertyRegistry dynamicPropertyRegistry = + new DefaultDynamicPropertyRegistry(environment, this.methods.isEmpty()); + beanFactory.registerSingleton(DYNAMIC_PROPERTY_REGISTRY_BEAN_NAME, dynamicPropertyRegistry); - this.methods.forEach(method -> { - ReflectionUtils.makeAccessible(method); - ReflectionUtils.invokeMethod(method, null, propertySource.dynamicPropertyRegistry); - }); + BeanDefinition beanDefinition = new RootBeanDefinition(DynamicPropertySourceBeanInitializer.class); + beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + beanDefinitionRegistry.registerBeanDefinition( + DYNAMIC_PROPERTY_SOURCE_BEAN_INITIALIZER_BEAN_NAME, beanDefinition); + + if (!this.methods.isEmpty()) { + MutablePropertySources propertySources = environment.getPropertySources(); + propertySources.addFirst(new DynamicValuesPropertySource(dynamicPropertyRegistry.valueSuppliers)); + this.methods.forEach(method -> { + ReflectionUtils.makeAccessible(method); + ReflectionUtils.invokeMethod(method, null, dynamicPropertyRegistry); + }); + } } Set getMethods() { return this.methods; } - @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof DynamicPropertiesContextCustomizer that && @@ -113,17 +115,4 @@ class DynamicPropertiesContextCustomizer implements ContextCustomizer { return this.methods.hashCode(); } - - private static DynamicValuesPropertySource getOrAdd(ConfigurableEnvironment environment) { - PropertySource propertySource = environment.getPropertySources() - .get(DynamicValuesPropertySource.PROPERTY_SOURCE_NAME); - if (propertySource == null) { - environment.getPropertySources().addFirst(new DynamicValuesPropertySource()); - return getOrAdd(environment); - } - Assert.state(propertySource instanceof DynamicValuesPropertySource, - "Incorrect DynamicValuesPropertySource type registered"); - return (DynamicValuesPropertySource) propertySource; - } - } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DynamicValuesPropertySource.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicValuesPropertySource.java index 8205a6c5568..8d60699e04b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicValuesPropertySource.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicValuesPropertySource.java @@ -17,14 +17,11 @@ package org.springframework.test.context.support; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Supplier; import org.springframework.core.env.MapPropertySource; import org.springframework.lang.Nullable; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.util.Assert; import org.springframework.util.function.SupplierUtils; /** @@ -39,20 +36,9 @@ class DynamicValuesPropertySource extends MapPropertySource { static final String PROPERTY_SOURCE_NAME = "Dynamic Test Properties"; - final DynamicPropertyRegistry dynamicPropertyRegistry; - - - DynamicValuesPropertySource() { - this(Collections.synchronizedMap(new LinkedHashMap<>())); - } DynamicValuesPropertySource(Map> valueSuppliers) { super(PROPERTY_SOURCE_NAME, Collections.unmodifiableMap(valueSuppliers)); - this.dynamicPropertyRegistry = (name, valueSupplier) -> { - Assert.hasText(name, "'name' must not be null or blank"); - Assert.notNull(valueSupplier, "'valueSupplier' must not be null"); - valueSuppliers.put(name, valueSupplier); - }; } @Override diff --git a/spring-test/src/test/java/org/springframework/test/context/DynamicPropertiesTestSuite.java b/spring-test/src/test/java/org/springframework/test/context/DynamicPropertiesTestSuite.java new file mode 100644 index 00000000000..e2842bd53e5 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/DynamicPropertiesTestSuite.java @@ -0,0 +1,55 @@ +/* + * 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.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; + +/** + * JUnit Platform based test suite for tests that involve the Spring TestContext + * Framework and dynamic properties. + * + *

This suite is only intended to be used manually within an IDE. + * + *

Logging Configuration

+ * + *

In order for our log4j2 configuration to be used in an IDE, you must + * set the following system property before running any tests — for + * example, in Run Configurations in Eclipse. + * + *

+ * -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
+ * 
+ * + * @author Sam Brannen + * @since 6.2 + */ +@Suite +@SelectClasses( + value = { + DynamicPropertyRegistryIntegrationTests.class, + DynamicPropertySourceIntegrationTests.class + }, + names = { + "org.springframework.test.context.junit.jupiter.nested.DynamicPropertySourceNestedTests", + "org.springframework.test.context.support.DefaultTestPropertySourcesIntegrationTests", + "org.springframework.test.context.support.DynamicPropertiesContextCustomizerFactoryTests", + "org.springframework.test.context.support.DynamicValuesPropertySourceTests" + } +) +class DynamicPropertiesTestSuite { +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/DefaultTestPropertySourcesIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/support/DefaultTestPropertySourcesIntegrationTests.java new file mode 100644 index 00000000000..ccda9d35577 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/support/DefaultTestPropertySourcesIntegrationTests.java @@ -0,0 +1,63 @@ +/* + * 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.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests which ensure that test-related property sources are not + * registered by default. + * + * @author Sam Brannen + * @since 6.2 + */ +@SpringJUnitConfig +@DirtiesContext +class DefaultTestPropertySourcesIntegrationTests { + + @Autowired + ConfigurableEnvironment env; + + + @Test + void ensureTestRelatedPropertySourcesAreNotRegisteredByDefault() { + assertPropertySourceIsNotRegistered(TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); + assertPropertySourceIsNotRegistered(DynamicValuesPropertySource.PROPERTY_SOURCE_NAME); + } + + private void assertPropertySourceIsNotRegistered(String name) { + MutablePropertySources propertySources = this.env.getPropertySources(); + assertThat(propertySources.contains(name)) + .as("PropertySource \"%s\" should not be registered by default", name) + .isFalse(); + } + + + @Configuration + static class Config { + } + +}