Skip to content

Commit

Permalink
Introduce DynamicPropertyRegistrar to replace DynamicPropertyRegistry…
Browse files Browse the repository at this point in the history
… bean

Spring Boot's testing support registers a DynamicPropertyRegistry as a
bean in the ApplicationContext, which conflicts with the
DynamicPropertyRegistry registered as a bean by the Spring TestContext
Framework (TCF) since Spring Framework 6.2 M2.

To avoid that conflict and to improve the user experience for Spring's
testing support, this commit introduces a DynamicPropertyRegistrar API
to replace the DynamicPropertyRegistry bean support.

Specifically, the TCF no longer registers a DynamicPropertyRegistry as
a bean in the ApplicationContext.

Instead, users can now register custom implementations of
DynamicPropertyRegistrar as beans in the ApplicationContext, and the
DynamicPropertiesContextCustomizer now registers a
DynamicPropertyRegistrarBeanInitializer which eagerly initializes
DynamicPropertyRegistrar beans and invokes their accept() methods with
an appropriate DynamicPropertyRegistry.

In addition, a singleton DynamicValuesPropertySource is created and
registered with the Environment for use in
DynamicPropertiesContextCustomizer and
DynamicPropertyRegistrarBeanInitializer, which allows
@⁠DynamicPropertySource methods and DynamicPropertyRegistrar beans to
transparently populate the same DynamicValuesPropertySource.

Closes gh-33501
  • Loading branch information
sbrannen committed Sep 11, 2024
1 parent 78028cd commit e7b52cf
Show file tree
Hide file tree
Showing 15 changed files with 429 additions and 301 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,51 @@
= Context Configuration with Dynamic Property Sources

The Spring TestContext Framework provides support for _dynamic_ properties via the
`@DynamicPropertySource` annotation and the `DynamicPropertyRegistry`.
`DynamicPropertyRegistry`, the `@DynamicPropertySource` annotation, and the
`DynamicPropertyRegistrar` API.

[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 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`.
The dynamic property source infrastructure was originally designed to allow properties
from {testcontainers-site}[Testcontainers] based tests to be exposed easily to Spring
integration tests. However, these features 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` 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.

[[testcontext-ctx-management-dynamic-property-sources-precedence]]
== Precedence

Dynamic properties have higher precedence than those loaded from `@TestPropertySource`,
the operating system's environment, Java system properties, or property sources added by
the application declaratively by using `@PropertySource` or programmatically. Thus,
dynamic properties can be used to selectively override properties loaded via
`@TestPropertySource`, system property sources, and application property sources.


[[testcontext-ctx-management-dynamic-property-sources-dynamic-property-registry]]
== `DynamicPropertyRegistry`

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.
is resolved. Typically, method references are used to supply values. The following
sections provide examples of how to use the `DynamicPropertyRegistry`.

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.
[[testcontext-ctx-management-dynamic-property-sources-dynamic-property-source]]
== `@DynamicPropertySource`

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 in order to add properties with dynamic
values to the set of `PropertySources` in the `Environment` for the `ApplicationContext`
loaded for the integration test.

Methods in integration test classes that are annotated with `@DynamicPropertySource` must
be `static` and must accept a single `DynamicPropertyRegistry` argument. See the
class-level javadoc for `DynamicPropertyRegistry` for further details.

[TIP]
====
Expand Down Expand Up @@ -107,11 +119,33 @@ Kotlin::
----
======

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.

[[testcontext-ctx-management-dynamic-property-sources-dynamic-property-registrar]]
== `DynamicPropertyRegistrar`

As an alternative to implementing `@DynamicPropertySource` methods in integration test
classes, you can register implementations of the `DynamicPropertyRegistrar` API as beans
within the test's `ApplicationContext`. Doing so allows you to support additional use
cases that are not possible with a `@DynamicPropertySource` method. For example, since a
`DynamicPropertyRegistrar` is itself a bean in the `ApplicationContext`, it can interact
with other beans in the context and register dynamic properties that are sourced from
those beans.

Any bean in a test's `ApplicationContext` that implements the `DynamicPropertyRegistrar`
interface will be automatically detected and eagerly initialized before the singleton
pre-instantiation phase, and the `accept()` methods of such beans will be invoked with a
`DynamicPropertyRegistry` that performs the actual dynamic property registration on
behalf of the registrar.

WARNING: Any interaction with other beans results in eager initialization of those other
beans and their dependencies.

The following example demonstrates how to implement a `DynamicPropertyRegistrar` as a
lambda expression that registers a dynamic property for the `ApiServer` bean. 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}")`,
and the value of the `api.url` property will be dynamically retrieved from the
`ApiServer` bean.

[tabs]
======
Expand All @@ -123,11 +157,13 @@ Java::
class TestConfig {
@Bean
@DynamicPropertySource
ApiServer apiServer(DynamicPropertyRegistry registry) {
ApiServer apiServer = new ApiServer();
registry.add("api.url", apiServer::getUrl);
return apiServer;
ApiServer apiServer() {
return new ApiServer();
}
@Bean
DynamicPropertyRegistrar apiServerProperties(ApiServer apiServer) {
return registry -> registry.add("api.url", apiServer::getUrl);
}
}
----
Expand All @@ -140,27 +176,14 @@ Kotlin::
class TestConfig {
@Bean
@DynamicPropertySource
fun apiServer(registry: DynamicPropertyRegistry): ApiServer {
val apiServer = ApiServer()
registry.add("api.url", apiServer::getUrl)
return apiServer
fun apiServer(): ApiServer {
return ApiServer()
}
@Bean
fun apiServerProperties(apiServer: ApiServer): DynamicPropertyRegistrar {
return registry -> registry.add("api.url", apiServer::getUrl)
}
}
----
======

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`,
the operating system's environment, Java system properties, or property sources added by
the application declaratively by using `@PropertySource` or programmatically. Thus,
dynamic properties can be used to selectively override properties loaded via
`@TestPropertySource`, system property sources, and application property sources.

Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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}.
*
* <p>Any bean in a test's {@code ApplicationContext} that implements the
* {@code DynamicPropertyRegistrar} interface will be automatically detected and
* eagerly initialized before the singleton pre-instantiation phase, and the
* {@link #accept} methods of such beans will be invoked with a
* {@code DynamicPropertyRegistry} that performs the actual dynamic property
* registration on behalf of the registrar.
*
* <p>This is an alternative to implementing
* {@link DynamicPropertySource @DynamicPropertySource} methods in integration
* test classes and supports additional use cases that are not possible with a
* {@code @DynamicPropertySource} method. For example, since a
* {@code DynamicPropertyRegistrar} is itself a bean in the {@code ApplicationContext},
* it can interact with other beans in the context and register dynamic properties
* that are sourced from those beans. Note, however, that any interaction with
* other beans results in eager initialization of those other beans and their
* dependencies.
*
* <h3>Precedence</h3>
*
* <p>Dynamic properties have higher precedence than those loaded from
* {@link TestPropertySource @TestPropertySource}, the operating system's
* environment, Java system properties, or property sources added by the
* application declaratively by using
* {@link org.springframework.context.annotation.PropertySource @PropertySource}
* or programmatically. Thus, dynamic properties can be used to selectively
* override properties loaded via {@code @TestPropertySource}, system property
* sources, and application property sources.
*
* <h3>Example</h3>
*
* <p>The following example demonstrates how to implement a
* {@code DynamicPropertyRegistrar} as a lambda expression that registers a
* dynamic property for the {@code ApiServer} bean. Other beans in the
* {@code ApplicationContext} can access the {@code api.url} property which is
* dynamically retrieved from the {@code ApiServer} bean &mdash; for example,
* via {@code @Value("${api.url}")}.
*
* <pre class="code">
* &#064;Configuration
* class TestConfig {
*
* &#064;Bean
* ApiServer apiServer() {
* return new ApiServer();
* }
*
* &#064;Bean
* DynamicPropertyRegistrar apiServerProperties(ApiServer apiServer) {
* return registry -> registry.add("api.url", apiServer::getUrl);
* }
*
* }</pre>
*
* @author Sam Brannen
* @since 6.2
* @see DynamicPropertySource
* @see DynamicPropertyRegistry
* @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory#preInstantiateSingletons()
*/
@FunctionalInterface
public interface DynamicPropertyRegistrar {

void accept(DynamicPropertyRegistry registry);

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,18 @@
* test classes.
*
* <p>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.
* supplied to {@link DynamicPropertyRegistrar} beans in the test's
* {@code ApplicationContext}, making it possible to register dynamic properties
* based on beans in the context. For example, a {@code @Bean} method can return
* a {@code DynamicPropertyRegistrar} that registers a property whose value is
* dynamically sourced from another bean in the context. See the documentation
* for {@code DynamicPropertyRegistrar} for an example.
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.2.5
* @see DynamicPropertySource
* @see DynamicPropertyRegistrar
*/
public interface DynamicPropertyRegistry {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,42 +24,35 @@

/**
* {@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}.
* methods in integration test classes in order to add properties with dynamic
* values to the {@code Environment}'s set of {@code PropertySources}.
*
* <p>Alternatively, dynamic properties can be added to the {@code Environment}
* by special beans in the test's {@code ApplicationContext}. See
* {@link DynamicPropertyRegistrar} for details.
*
* <p>This annotation and its supporting infrastructure were originally designed
* to allow properties from
* <a href="https://www.testcontainers.org/">Testcontainers</a> 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 {@code ApplicationContext} or with beans whose lifecycle is managed by
* the test's {@code ApplicationContext}.
* test's {@code ApplicationContext}.
*
* <p>{@code @DynamicPropertySource}-annotated methods use a
* {@code DynamicPropertyRegistry} to add <em>name-value</em> 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.
* <p>{@code @DynamicPropertySource} methods use a {@link DynamicPropertyRegistry}
* to add <em>name-value</em> 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.
*
* <p>Methods in integration test classes that are annotated with
* {@code @DynamicPropertySource} must be {@code static} and must accept a single
* {@link DynamicPropertyRegistry} argument.
*
* <p>{@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.
* {@code DynamicPropertyRegistry} argument.
*
* <p>Dynamic properties from methods annotated with {@code @DynamicPropertySource}
* will be <em>inherited</em> from enclosing test classes, analogous to inheritance
* from superclasses and interfaces.
* See {@link NestedTestConfiguration @NestedTestConfiguration} for details.
* from superclasses and interfaces. See
* {@link NestedTestConfiguration @NestedTestConfiguration} for details.
*
* <p><strong>NOTE</strong>: if you use {@code @DynamicPropertySource} in a base
* class and discover that tests in subclasses fail because the dynamic properties
Expand All @@ -69,6 +62,7 @@
* correct dynamic properties.
*
* <h3>Precedence</h3>
*
* <p>Dynamic properties have higher precedence than those loaded from
* {@link TestPropertySource @TestPropertySource}, the operating system's
* environment, Java system properties, or property sources added by the
Expand Down Expand Up @@ -103,28 +97,11 @@
* }
* }</pre>
*
* <p>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.
*
* <pre class="code">
* &#064;Configuration
* class TestConfig {
*
* &#064;Bean
* &#064;DynamicPropertySource
* ApiServer apiServer(DynamicPropertyRegistry registry) {
* ApiServer apiServer = new ApiServer();
* registry.add("api.url", apiServer::getUrl);
* return apiServer;
* }
* }</pre>
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.2.5
* @see DynamicPropertyRegistry
* @see DynamicPropertyRegistrar
* @see ContextConfiguration
* @see TestPropertySource
* @see org.springframework.core.env.PropertySource
Expand Down
Loading

0 comments on commit e7b52cf

Please sign in to comment.