Introduce DynamicPropertyRegistrar to replace DynamicPropertyRegistry 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
This commit is contained in:
Sam Brannen
2024-09-10 16:34:50 +02:00
parent 78028cde05
commit e7b52cf8b2
15 changed files with 429 additions and 301 deletions

View File

@@ -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`.
====
[[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. The following
sections provide examples of how to use the `DynamicPropertyRegistry`.
[[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 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.
`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.
`@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.
be `static` and must accept a single `DynamicPropertyRegistry` argument. See the
class-level javadoc for `DynamicPropertyRegistry` for further details.
[TIP]
====
@@ -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]
======
@@ -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);
}
}
----
@@ -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.