Lazily register DynamicValuesPropertySource in the TestContext framework

Prior to this commit, as a result of commit 6cdb34410b, the
DynamicValuesPropertySource was eagerly registered in the Environment
even if the DynamicPropertyRegistry was never used to register dynamic
properties.

This commit ensures that the DynamicValuesPropertySource is only
registered if we know that the DynamicPropertyRegistry is actually used
-- either by a @⁠DynamicPropertySource method in a test class or via a
bean in the ApplicationContext that invokes add() on the
DynamicPropertyRegistry bean.

See gh-32271
Closes gh-32871
This commit is contained in:
Sam Brannen 2024-05-22 14:24:56 +02:00
parent c9a6b7f01e
commit b47cdaa259
5 changed files with 218 additions and 46 deletions

View File

@ -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<String, Supplier<Object>> 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<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();
}
}
}

View File

@ -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<Method> 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;
}
}

View File

@ -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<String, Supplier<Object>> 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

View File

@ -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.
*
* <p><strong>This suite is only intended to be used manually within an IDE.</strong>
*
* <h3>Logging Configuration</h3>
*
* <p>In order for our log4j2 configuration to be used in an IDE, you must
* set the following system property before running any tests &mdash; for
* example, in <em>Run Configurations</em> in Eclipse.
*
* <pre style="code">
* -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
* </pre>
*
* @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 {
}

View File

@ -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 {
}
}