diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc index 4705d57cbe6..a3936d68824 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc @@ -1,43 +1,58 @@ [[testcontext-ctx-management-dynamic-property-sources]] = Context Configuration with Dynamic Property Sources -As of Spring Framework 5.2.5, the TestContext framework provides support for _dynamic_ -properties via the `@DynamicPropertySource` annotation. This annotation can be used in -integration tests that need to add properties with dynamic values to the set of -`PropertySources` in the `Environment` for the `ApplicationContext` loaded for the -integration test. +The Spring TestContext Framework provides support for _dynamic_ properties via the +`@DynamicPropertySource` annotation and the `DynamicPropertyRegistry`. [NOTE] ==== -The `@DynamicPropertySource` annotation and its supporting infrastructure were -originally designed to allow properties from -{testcontainers-site}[Testcontainers] based tests to be exposed easily to -Spring integration tests. However, this feature may also be used with any form of -external resource whose lifecycle is maintained outside the test's `ApplicationContext`. +The `@DynamicPropertySource` annotation and its supporting infrastructure were originally +designed to allow properties from {testcontainers-site}[Testcontainers] based tests to be +exposed easily to Spring integration tests. However, this feature may be used with any +form of external resource whose lifecycle is managed outside the test's +`ApplicationContext` or with beans whose lifecycle is managed by the test's +`ApplicationContext`. ==== -In contrast to the xref:testing/testcontext-framework/ctx-management/property-sources.adoc[`@TestPropertySource`] -annotation that is applied at the class level, `@DynamicPropertySource` must be applied -to a `static` method that accepts a single `DynamicPropertyRegistry` argument which is -used to add _name-value_ pairs to the `Environment`. Values are dynamic and provided via -a `Supplier` which is only invoked when the property is resolved. Typically, method -references are used to supply values, as can be seen in the following example which uses -the Testcontainers project to manage a Redis container outside of the Spring -`ApplicationContext`. The IP address and port of the managed Redis container are made -available to components within the test's `ApplicationContext` via the `redis.host` and -`redis.port` properties. These properties can be accessed via Spring's `Environment` -abstraction or injected directly into Spring-managed components – for example, via -`@Value("${redis.host}")` and `@Value("${redis.port}")`, respectively. +In contrast to the +xref:testing/testcontext-framework/ctx-management/property-sources.adoc[`@TestPropertySource`] +annotation that is applied at the class level, `@DynamicPropertySource` can be applied to +`static` methods in integration test classes or to `@Bean` methods in test +`@Configuration` classes in order to add properties with dynamic values to the set of +`PropertySources` in the `Environment` for the `ApplicationContext` loaded for the +integration test. + +A `DynamicPropertyRegistry` is used to add _name-value_ pairs to the `Environment`. +Values are dynamic and provided via a `Supplier` which is only invoked when the property +is resolved. Typically, method references are used to supply values. + +Methods in integration test classes that are annotated with `@DynamicPropertySource` must +be `static` and must accept a single `DynamicPropertyRegistry` argument. + +`@Bean` methods annotated with `@DynamicPropertySource` may either accept an argument of +type `DynamicPropertyRegistry` or access a `DynamicPropertyRegistry` instance autowired +into their enclosing `@Configuration` class. Note, however, that `@Bean` methods which +interact with a `DynamicPropertyRegistry` are not required to be annotated with +`@DynamicPropertySource` unless they need to enforce eager initialization of the bean +within the context. See the class-level javadoc for `DynamicPropertyRegistry` for details. [TIP] ==== If you use `@DynamicPropertySource` in a base class and discover that tests in subclasses fail because the dynamic properties change between subclasses, you may need to annotate -your base class with xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[`@DirtiesContext`] to -ensure that each subclass gets its own `ApplicationContext` with the correct dynamic +your base class with +xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[`@DirtiesContext`] +to ensure that each subclass gets its own `ApplicationContext` with the correct dynamic properties. ==== +The following example uses the Testcontainers project to manage a Redis container outside +of the Spring `ApplicationContext`. The IP address and port of the managed Redis +container are made available to components within the test's `ApplicationContext` via the +`redis.host` and `redis.port` properties. These properties can be accessed via Spring's +`Environment` abstraction or injected directly into Spring-managed components – for +example, via `@Value("${redis.host}")` and `@Value("${redis.port}")`, respectively. + [tabs] ====== Java:: @@ -92,7 +107,55 @@ Kotlin:: ---- ====== -[[precedence]] +The following example demonstrates how to use `DynamicPropertyRegistry` and +`@DynamicPropertySource` with a `@Bean` method. The `api.url` property can be accessed +via Spring's `Environment` abstraction or injected directly into other Spring-managed +components – for example, via `@Value("${api.url}")`. The value of the `api.url` property +will be dynamically retrieved from the `ApiServer` bean. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + @Configuration + class TestConfig { + + @Bean + @DynamicPropertySource + ApiServer apiServer(DynamicPropertyRegistry registry) { + ApiServer apiServer = new ApiServer(); + registry.add("api.url", apiServer::getUrl); + return apiServer; + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + @Configuration + class TestConfig { + + @Bean + @DynamicPropertySource + fun apiServer(registry: DynamicPropertyRegistry): ApiServer { + val apiServer = ApiServer() + registry.add("api.url", apiServer::getUrl) + return apiServer + } + } +---- +====== + +NOTE: The use of `@DynamicPropertySource` on the `@Bean` method is optional and results +in the `ApiServer` bean being eagerly initialized so that other beans in the context can +be given access to the dynamic properties sourced from the `ApiServer` bean when those +other beans are initialized. + +[[testcontext-ctx-management-dynamic-property-sources-precedence]] == Precedence Dynamic properties have higher precedence than those loaded from `@TestPropertySource`, diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc index 34057860b85..bb83c4d4da6 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/property-sources.adoc @@ -184,7 +184,6 @@ meta-present `@TestPropertySource` annotations. In other words, `locations` and meta-annotation. ==== - [[default-properties-file-detection]] == Default Properties File Detection @@ -195,7 +194,7 @@ if the annotated test class is `com.example.MyTest`, the corresponding default p file is `classpath:com/example/MyTest.properties`. If the default cannot be detected, an `IllegalStateException` is thrown. -[[precedence]] +[[testcontext-ctx-management-property-sources-precedence]] == Precedence Test properties have higher precedence than those defined in the operating system's diff --git a/spring-test/src/main/java/org/springframework/test/context/DynamicPropertyRegistry.java b/spring-test/src/main/java/org/springframework/test/context/DynamicPropertyRegistry.java index 2f521c4925d..cc73b838a5b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DynamicPropertyRegistry.java +++ b/spring-test/src/main/java/org/springframework/test/context/DynamicPropertyRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * 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. @@ -19,9 +19,25 @@ package org.springframework.test.context; import java.util.function.Supplier; /** - * Registry used with {@link DynamicPropertySource @DynamicPropertySource} - * methods so that they can add properties to the {@code Environment} that have - * dynamically resolved values. + * Registry that is used to add properties with dynamically resolved values to + * the {@code Environment}. + * + *

A {@code DynamicPropertyRegistry} is supplied as an argument to static + * {@link DynamicPropertySource @DynamicPropertySource} methods in integration + * test classes. + * + *

As of Spring Framework 6.2, a {@code DynamicPropertyRegistry} is also + * registered as a singleton bean in the test's {@code ApplicationContext}. This + * allows a {@code DynamicPropertyRegistry} to be autowired into a + * {@code @Configuration} class or supplied to a {@code @Bean} method as an + * argument, making it possible to register a dynamic property from within a test's + * {@code ApplicationContext}. For example, a {@code @Bean} method can register + * a property whose value is dynamically sourced from the bean that the method + * returns. Note that such a {@code @Bean} method can optionally be annotated + * with {@code @DynamicPropertySource} to enforce eager initialization of the + * bean within the context, thereby ensuring that any dynamic properties sourced + * from that bean are available to other singleton beans within the context. + * See {@link DynamicPropertySource @DynamicPropertySource} for an example. * * @author Phillip Webb * @author Sam Brannen diff --git a/spring-test/src/main/java/org/springframework/test/context/DynamicPropertySource.java b/spring-test/src/main/java/org/springframework/test/context/DynamicPropertySource.java index c21bb6b05e9..a491abe3949 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DynamicPropertySource.java +++ b/spring-test/src/main/java/org/springframework/test/context/DynamicPropertySource.java @@ -23,29 +23,43 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * {@code @DynamicPropertySource} is an annotation that can be applied to methods - * in integration test classes that need to add properties with dynamic values to - * the {@code Environment}'s set of {@code PropertySources}. + * {@code @DynamicPropertySource} is an annotation that can be applied to static + * methods in integration test classes or to {@code @Bean} methods in test + * {@code @Configuration} classes in order to add properties with dynamic values + * to the {@code Environment}'s set of {@code PropertySources}. * *

This annotation and its supporting infrastructure were originally designed * to allow properties from * Testcontainers based tests to be - * exposed easily to Spring integration tests. However, this feature may also be - * used with any form of external resource whose lifecycle is maintained outside + * exposed easily to Spring integration tests. However, this feature may be used + * with any form of external resource whose lifecycle is managed outside the + * test's {@code ApplicationContext} or with beans whose lifecycle is managed by * the test's {@code ApplicationContext}. * - *

Methods annotated with {@code @DynamicPropertySource} must be {@code static} - * and must have a single {@link DynamicPropertyRegistry} argument which is used - * to add name-value pairs to the {@code Environment}'s set of - * {@code PropertySources}. Values are dynamic and provided via a - * {@link java.util.function.Supplier} which is only invoked when the property - * is resolved. Typically, method references are used to supply values, as in the - * example below. + *

{@code @DynamicPropertySource}-annotated methods use a + * {@code DynamicPropertyRegistry} to add name-value pairs to the + * {@code Environment}'s set of {@code PropertySources}. Values are dynamic and + * provided via a {@link java.util.function.Supplier} which is only invoked when + * the property is resolved. Typically, method references are used to supply values, + * as in the example below. * - *

As of Spring Framework 5.3.2, dynamic properties from methods annotated with - * {@code @DynamicPropertySource} will be inherited from enclosing test - * classes, analogous to inheritance from superclasses and interfaces. See - * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + *

Methods in integration test classes that are annotated with + * {@code @DynamicPropertySource} must be {@code static} and must accept a single + * {@link DynamicPropertyRegistry} argument. + * + *

{@code @Bean} methods annotated with {@code @DynamicPropertySource} may + * either accept an argument of type {@code DynamicPropertyRegistry} or access a + * {@code DynamicPropertyRegistry} instance autowired into their enclosing + * {@code @Configuration} class. Note, however, that {@code @Bean} methods which + * interact with a {@code DynamicPropertyRegistry} are not required to be annotated + * with {@code @DynamicPropertySource} unless they need to enforce eager + * initialization of the bean within the context. + * See {@link DynamicPropertyRegistry} for details. + * + *

Dynamic properties from methods annotated with {@code @DynamicPropertySource} + * will be inherited from enclosing test classes, analogous to inheritance + * from superclasses and interfaces. + * See {@link NestedTestConfiguration @NestedTestConfiguration} for details. * *

NOTE: if you use {@code @DynamicPropertySource} in a base * class and discover that tests in subclasses fail because the dynamic properties @@ -64,7 +78,13 @@ import java.lang.annotation.Target; * override properties loaded via {@code @TestPropertySource}, system property * sources, and application property sources. * - *

Example

+ *

Examples

+ * + *

The following example demonstrates how to use {@code @DynamicPropertySource} + * in an integration test class. Beans in the {@code ApplicationContext} can + * access the {@code redis.host} and {@code redis.port} properties which are + * dynamically retrieved from the Redis container. + * *

  * @SpringJUnitConfig(...)
  * @Testcontainers
@@ -81,7 +101,24 @@ import java.lang.annotation.Target;
  *         registry.add("redis.host", redis::getHost);
  *         registry.add("redis.port", redis::getFirstMappedPort);
  *     }
+ * }
* + *

The following example demonstrates how to use {@code @DynamicPropertySource} + * with a {@code @Bean} method. Beans in the {@code ApplicationContext} can + * access the {@code api.url} property which is dynamically retrieved from the + * {@code ApiServer} bean. + * + *

+ * @Configuration
+ * class TestConfig {
+ *
+ *     @Bean
+ *     @DynamicPropertySource
+ *     ApiServer apiServer(DynamicPropertyRegistry registry) {
+ *         ApiServer apiServer = new ApiServer();
+ *         registry.add("api.url", apiServer::getUrl);
+ *         return apiServer;
+ *     }
  * }
* * @author Phillip Webb 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 266782314aa..91604615458 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -18,14 +18,15 @@ package org.springframework.test.context.support; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; import java.util.Set; -import java.util.function.Supplier; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertySource; import org.springframework.lang.Nullable; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.DynamicPropertyRegistry; @@ -35,8 +36,10 @@ import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; /** - * {@link ContextCustomizer} to support - * {@link DynamicPropertySource @DynamicPropertySource} methods. + * {@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. * * @author Phillip Webb * @author Sam Brannen @@ -45,7 +48,12 @@ import org.springframework.util.ReflectionUtils; */ class DynamicPropertiesContextCustomizer implements ContextCustomizer { - private static final String PROPERTY_SOURCE_NAME = "Dynamic Test Properties"; + 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 final Set methods; @@ -61,27 +69,32 @@ class DynamicPropertiesContextCustomizer implements ContextCustomizer { () -> "@DynamicPropertySource method '" + method.getName() + "' must be static"); Class[] types = method.getParameterTypes(); Assert.state(types.length == 1 && types[0] == DynamicPropertyRegistry.class, - () -> "@DynamicPropertySource method '" + method.getName() + "' must accept a single DynamicPropertyRegistry argument"); + () -> "@DynamicPropertySource method '" + method.getName() + + "' must accept a single DynamicPropertyRegistry argument"); } @Override public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { - MutablePropertySources sources = context.getEnvironment().getPropertySources(); - sources.addFirst(new DynamicValuesPropertySource(PROPERTY_SOURCE_NAME, buildDynamicPropertiesMap())); - } + 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); + } + + 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); + } - private Map> buildDynamicPropertiesMap() { - Map> map = new LinkedHashMap<>(); - DynamicPropertyRegistry dynamicPropertyRegistry = (name, valueSupplier) -> { - Assert.hasText(name, "'name' must not be null or blank"); - Assert.notNull(valueSupplier, "'valueSupplier' must not be null"); - map.put(name, valueSupplier); - }; this.methods.forEach(method -> { ReflectionUtils.makeAccessible(method); - ReflectionUtils.invokeMethod(method, null, dynamicPropertyRegistry); + ReflectionUtils.invokeMethod(method, null, propertySource.dynamicPropertyRegistry); }); - return Collections.unmodifiableMap(map); } Set getMethods() { @@ -100,4 +113,17 @@ 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/DynamicPropertiesContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java index 7f9e3da7798..f25a7335eef 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -17,6 +17,7 @@ package org.springframework.test.context.support; import java.lang.reflect.Method; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -26,12 +27,15 @@ import org.springframework.core.annotation.MergedAnnotations; import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.TestContextAnnotationUtils; /** - * {@link ContextCustomizerFactory} to support - * {@link DynamicPropertySource @DynamicPropertySource} methods. + * {@link ContextCustomizerFactory} which supports + * {@link DynamicPropertySource @DynamicPropertySource} methods and the + * registration of a {@link DynamicPropertyRegistry} as a singleton bean in the + * container for use in {@code @Configuration} classes and {@code @Bean} methods. * * @author Phillip Webb * @author Sam Brannen @@ -49,7 +53,7 @@ class DynamicPropertiesContextCustomizerFactory implements ContextCustomizerFact Set methods = new LinkedHashSet<>(); findMethods(testClass, methods); if (methods.isEmpty()) { - return null; + methods = Collections.emptySet(); } return new DynamicPropertiesContextCustomizer(methods); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertySourceBeanInitializer.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertySourceBeanInitializer.java new file mode 100644 index 00000000000..ace5ea8ad8e --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertySourceBeanInitializer.java @@ -0,0 +1,73 @@ +/* + * 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.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.context.weaving.LoadTimeWeaverAware; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.lang.Nullable; +import org.springframework.test.context.DynamicPropertySource; + +/** + * Internal component which eagerly initializes beans created by {@code @Bean} + * factory methods annotated with {@link DynamicPropertySource @DynamicPropertySource}. + * + *

This class implements {@link LoadTimeWeaverAware} since doing so is + * currently the only way to have a component eagerly initialized before the + * {@code ConfigurableListableBeanFactory.preInstantiateSingletons()} phase. + * + * @author Sam Brannen + * @since 6.2 + */ +class DynamicPropertySourceBeanInitializer implements BeanFactoryAware, InitializingBean, LoadTimeWeaverAware { + + private static final Log logger = LogFactory.getLog(DynamicPropertySourceBeanInitializer.class); + + @Nullable + private BeanFactory beanFactory; + + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public void afterPropertiesSet() { + if (!(this.beanFactory instanceof ListableBeanFactory lbf)) { + throw new IllegalStateException("BeanFactory must be set and must be a ListableBeanFactory"); + } + for (String name : lbf.getBeanNamesForAnnotation(DynamicPropertySource.class)) { + if (logger.isDebugEnabled()) { + logger.debug("Eagerly initializing @DynamicPropertySource bean '%s'".formatted(name)); + } + this.beanFactory.getBean(name); + } + } + + @Override + public void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver) { + // no-op + } + +} 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 b87662215f4..8205a6c5568 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 @@ -16,11 +16,15 @@ 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; /** @@ -33,9 +37,22 @@ import org.springframework.util.function.SupplierUtils; */ class DynamicValuesPropertySource extends MapPropertySource { - @SuppressWarnings({"rawtypes", "unchecked"}) - DynamicValuesPropertySource(String name, Map> valueSuppliers) { - super(name, (Map) valueSuppliers); + 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/DynamicPropertyRegistryIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/DynamicPropertyRegistryIntegrationTests.java new file mode 100644 index 00000000000..c659f6f495b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/DynamicPropertyRegistryIntegrationTests.java @@ -0,0 +1,165 @@ +/* + * 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.jupiter.api.Test; + +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.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link DynamicPropertyRegistry} bean support. + * + * @author Sam Brannen + * @since 6.2 + * @see DynamicPropertySourceIntegrationTests + */ +@SpringJUnitConfig +@TestPropertySource(properties = "api.url: https://example.com/test") +class DynamicPropertyRegistryIntegrationTests { + + private static final String API_URL = "api.url"; + + + @Test + void dynamicPropertySourceOverridesTestPropertySource(@Autowired ConfigurableEnvironment env) { + assertApiUrlIsDynamic(env.getProperty(API_URL)); + + 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"); + } + + @Test + void testReceivesDynamicProperty(@Value("${api.url}") String apiUrl) { + assertApiUrlIsDynamic(apiUrl); + } + + @Test + void environmentInjectedServiceCanRetrieveDynamicProperty(@Autowired EnvironmentInjectedService service) { + assertApiUrlIsDynamic(service); + } + + @Test + void constructorInjectedServiceReceivesDynamicProperty(@Autowired ConstructorInjectedService service) { + assertApiUrlIsDynamic(service); + } + + @Test + void setterInjectedServiceReceivesDynamicProperty(@Autowired SetterInjectedService service) { + assertApiUrlIsDynamic(service); + } + + + private static void assertApiUrlIsDynamic(ApiUrlClient service) { + assertApiUrlIsDynamic(service.getApiUrl()); + } + + private static void assertApiUrlIsDynamic(String apiUrl) { + assertThat(apiUrl).isEqualTo("https://example.com/dynamic"); + } + + + @Configuration + @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 + // 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; + } + + } + + interface ApiUrlClient { + + String getApiUrl(); + } + + static class EnvironmentInjectedService implements ApiUrlClient { + + private final Environment env; + + + EnvironmentInjectedService(Environment env) { + this.env = env; + } + + @Override + public String getApiUrl() { + return this.env.getProperty(API_URL); + } + } + + static class ConstructorInjectedService implements ApiUrlClient { + + private final String apiUrl; + + + ConstructorInjectedService(@Value("${api.url}") String apiUrl) { + this.apiUrl = apiUrl; + } + + @Override + public String getApiUrl() { + return this.apiUrl; + } + } + + static class SetterInjectedService implements ApiUrlClient { + + private String apiUrl; + + + @Autowired + void setApiUrl(@Value("${api.url}") String apiUrl) { + this.apiUrl = apiUrl; + } + + @Override + public String getApiUrl() { + return this.apiUrl; + } + } + + static class ApiServer { + + String getUrl() { + return "https://example.com/dynamic"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/DynamicPropertySourceIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/DynamicPropertySourceIntegrationTests.java index 550de54679f..73a86f9dcb5 100644 --- a/spring-test/src/test/java/org/springframework/test/context/DynamicPropertySourceIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/DynamicPropertySourceIntegrationTests.java @@ -37,6 +37,7 @@ import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; * * @author Phillip Webb * @author Sam Brannen + * @see DynamicPropertyRegistryIntegrationTests */ @SpringJUnitConfig @TestPropertySource(properties = "test.container.ip: test") diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java index a18710b975b..99e746d8add 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/AbstractAotTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -40,6 +40,7 @@ abstract class AbstractAotTests { "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests__TestContext001_BeanDefinitions.java", "org/springframework/test/context/aot/samples/basic/BasicSpringJupiterImportedConfigTests__TestContext001_BeanFactoryRegistrations.java", "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext001_BeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext001_BeanDefinitions.java", // BasicSpringJupiterSharedConfigTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext002_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext002_BeanDefinitions.java", @@ -50,6 +51,7 @@ abstract class AbstractAotTests { "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java", "org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext002_BeanDefinitions.java", "org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext002_ManagementBeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext002_BeanDefinitions.java", // BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests. // BasicSpringJupiterTests.NestedTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext003_BeanDefinitions.java", @@ -61,24 +63,28 @@ abstract class AbstractAotTests { "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext003_BeanDefinitions.java", "org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext003_BeanDefinitions.java", "org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext003_ManagementBeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext003_BeanDefinitions.java", // BasicSpringTestNGTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext004_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext004_BeanDefinitions.java", "org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext004_ApplicationContextInitializer.java", "org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext004_BeanFactoryRegistrations.java", "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext004_BeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext004_BeanDefinitions.java", // BasicSpringVintageTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext005_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext005_BeanDefinitions.java", "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext005_ApplicationContextInitializer.java", "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext005_BeanFactoryRegistrations.java", "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext005_BeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext005_BeanDefinitions.java", // DisabledInAotRuntimeMethodLevelTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext006_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext006_BeanDefinitions.java", "org/springframework/test/context/aot/samples/basic/DisabledInAotRuntimeMethodLevelTests__TestContext006_ApplicationContextInitializer.java", "org/springframework/test/context/aot/samples/basic/DisabledInAotRuntimeMethodLevelTests__TestContext006_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/basic/DisabledInAotRuntimeMethodLevelTests__TestContext006_BeanFactoryRegistrations.java" + "org/springframework/test/context/aot/samples/basic/DisabledInAotRuntimeMethodLevelTests__TestContext006_BeanFactoryRegistrations.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext006_BeanDefinitions.java" }; Stream> scan() { diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java index 0fdbe04b1ef..d98478befd4 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java @@ -395,6 +395,7 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext001_BeanDefinitions.java", "org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext001_BeanDefinitions.java", "org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext001_ManagementBeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext001_BeanDefinitions.java", // BasicSpringJupiterTests -- not generated b/c already generated for BasicSpringJupiterSharedConfigTests. // BasicSpringJupiterTests.NestedTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext002_BeanDefinitions.java", @@ -406,24 +407,28 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext002_BeanDefinitions.java", "org/springframework/test/context/aot/samples/management/ManagementConfiguration__TestContext002_BeanDefinitions.java", "org/springframework/test/context/aot/samples/management/ManagementMessageService__TestContext002_ManagementBeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext002_BeanDefinitions.java", // BasicSpringTestNGTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext003_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext003_BeanDefinitions.java", "org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext003_ApplicationContextInitializer.java", "org/springframework/test/context/aot/samples/basic/BasicSpringTestNGTests__TestContext003_BeanFactoryRegistrations.java", "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext003_BeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext003_BeanDefinitions.java", // BasicSpringVintageTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext004_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext004_BeanDefinitions.java", "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext004_ApplicationContextInitializer.java", "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext004_BeanFactoryRegistrations.java", "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext004_BeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext004_BeanDefinitions.java", // SqlScriptsSpringJupiterTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext005_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext005_BeanDefinitions.java", "org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext005_ApplicationContextInitializer.java", "org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext005_BeanFactoryRegistrations.java", "org/springframework/test/context/jdbc/EmptyDatabaseConfig__TestContext005_BeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext005_BeanDefinitions.java", // WebSpringJupiterTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext006_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext006_BeanDefinitions.java", @@ -432,12 +437,14 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { "org/springframework/test/context/aot/samples/web/WebTestConfiguration__TestContext006_BeanDefinitions.java", "org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext006_Autowiring.java", "org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext006_BeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext006_BeanDefinitions.java", // XmlSpringJupiterTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext007_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext007_BeanDefinitions.java", "org/springframework/test/context/aot/samples/common/DefaultMessageService__TestContext007_BeanDefinitions.java", "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext007_ApplicationContextInitializer.java", - "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext007_BeanFactoryRegistrations.java" + "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext007_BeanFactoryRegistrations.java", + "org/springframework/test/context/support/DynamicPropertySourceBeanInitializer__TestContext007_BeanDefinitions.java", }; } diff --git a/spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java index 5dd24e7ebef..6345184bbbe 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/BootstrapTestUtilsMergedConfigTests.java @@ -36,7 +36,6 @@ import org.springframework.test.context.web.WebDelegatingSmartContextLoader; import org.springframework.test.context.web.WebMergedContextConfiguration; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link BootstrapTestUtils} involving {@link MergedContextConfiguration}. @@ -59,10 +58,14 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt */ @Test void buildMergedConfigWithContextConfigurationWithoutLocationsClassesOrInitializers() { - assertThatIllegalStateException().isThrownBy(() -> - buildMergedContextConfiguration(MissingContextAttributesTestCase.class)) - .withMessageStartingWith("DelegatingSmartContextLoader was unable to detect defaults, " - + "and no ApplicationContextInitializers or ContextCustomizers were declared for context configuration attributes"); + Class testClass = MissingContextAttributesTestCase.class; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, DelegatingSmartContextLoader.class); + assertThat(mergedConfig.getContextCustomizers()) + .map(Object::getClass) + .map(Class::getSimpleName) + .containsOnly("DynamicPropertiesContextCustomizer"); } @Test diff --git a/spring-test/src/test/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactoryTests.java b/spring-test/src/test/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactoryTests.java index 4f58a1b65b1..42211185917 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactoryTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * 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. @@ -40,10 +40,11 @@ class DynamicPropertiesContextCustomizerFactoryTests { private final List configAttributes = Collections.emptyList(); @Test - void createContextCustomizerWhenNoAnnotatedMethodsReturnsNull() { + void createContextCustomizerWhenNoAnnotatedMethodsReturnsCustomizerWithEmptyMethods() { DynamicPropertiesContextCustomizer customizer = this.factory.createContextCustomizer( NoDynamicPropertySource.class, this.configAttributes); - assertThat(customizer).isNull(); + assertThat(customizer).isNotNull(); + assertThat(customizer.getMethods()).isEmpty(); } @Test diff --git a/spring-test/src/test/java/org/springframework/test/context/support/DynamicValuesPropertySourceTests.java b/spring-test/src/test/java/org/springframework/test/context/support/DynamicValuesPropertySourceTests.java index 065486b2a27..7f75fe927eb 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/DynamicValuesPropertySourceTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/DynamicValuesPropertySourceTests.java @@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; */ class DynamicValuesPropertySourceTests { - private final DynamicValuesPropertySource source = new DynamicValuesPropertySource("test", + private final DynamicValuesPropertySource source = new DynamicValuesPropertySource( Map.of("a", () -> "A", "b", () -> "B"));