From 3c095b4ec21f014efe7787b1485c268f76e2f259 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Sep 2024 13:54:43 +0100 Subject: [PATCH] Prefer DynamicPropertyRegistar to DynamicPropertyRegistry Closes gh-41996 --- .../pages/features/dev-services.adoc | 6 +- .../MyContainersConfiguration.java | 17 ++-- .../MyContainersConfiguration.kt | 19 +++-- .../ImportTestcontainersTests.java | 2 +- ...sPropertySourceAutoConfigurationTests.java | 79 ++++++++++++++++- ...tionWithSpringBootTestIntegrationTest.java | 26 +++++- .../context/ContainerFieldsImporter.java | 10 ++- .../DynamicPropertySourceMethodsImporter.java | 85 +++++++++++++++---- .../context/ImportTestcontainers.java | 5 +- .../ImportTestcontainersRegistrar.java | 12 ++- .../BeforeTestcontainerUsedEvent.java | 5 ++ ...tcontainersLifecycleBeanPostProcessor.java | 2 + .../TestcontainersPropertySource.java | 56 +++++++++++- ...ainersPropertySourceAutoConfiguration.java | 22 ++++- .../ContainerConnectionDetailsFactory.java | 11 ++- ...itional-spring-configuration-metadata.json | 28 +++++- .../TestcontainersPropertySourceTests.java | 22 +++-- ...ontainerConnectionDetailsFactoryTests.java | 9 +- 18 files changed, 340 insertions(+), 76 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index 51cfebad4bd..c769d9003de 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -364,9 +364,9 @@ TIP: You can use the Maven goal `spring-boot:test-run` or the Gradle task `bootT [[features.dev-services.testcontainers.at-development-time.dynamic-properties]] ==== Contributing Dynamic Properties at Development Time -If you want to contribute dynamic properties at development time from your `Container` `@Bean` methods, you can do so by injecting a `DynamicPropertyRegistry`. -This works in a similar way to the xref:testing/testcontainers.adoc#testing.testcontainers.dynamic-properties[`@DynamicPropertySource` annotation] that you can use in your tests. -It allows you to add properties that will become available once your container has started. +If you want to contribute dynamic properties at development time from your `Container` `@Bean` methods, define an additional `DynamicPropertyRegistrar` bean. +The registrar should be defined using a `@Bean` method that injects the container from which the properties will be sourced as a parameter. +This arrangement ensures that container has been started before the properties are used. A typical configuration would look like this: diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/devservices/testcontainers/atdevelopmenttime/dynamicproperties/MyContainersConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/devservices/testcontainers/atdevelopmenttime/dynamicproperties/MyContainersConfiguration.java index d099931f286..9c816309fd2 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/devservices/testcontainers/atdevelopmenttime/dynamicproperties/MyContainersConfiguration.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/devservices/testcontainers/atdevelopmenttime/dynamicproperties/MyContainersConfiguration.java @@ -20,17 +20,22 @@ import org.testcontainers.containers.MongoDBContainer; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertyRegistrar; @TestConfiguration(proxyBeanMethods = false) public class MyContainersConfiguration { @Bean - public MongoDBContainer mongoDbContainer(DynamicPropertyRegistry properties) { - MongoDBContainer container = new MongoDBContainer("mongo:5.0"); - properties.add("spring.data.mongodb.host", container::getHost); - properties.add("spring.data.mongodb.port", container::getFirstMappedPort); - return container; + public MongoDBContainer mongoDbContainer() { + return new MongoDBContainer("mongo:5.0"); + } + + @Bean + public DynamicPropertyRegistrar mongoDbProperties(MongoDBContainer container) { + return (properties) -> { + properties.add("spring.data.mongodb.host", container::getHost); + properties.add("spring.data.mongodb.port", container::getFirstMappedPort); + }; } } diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testcontainers/atdevelopmenttime/dynamicproperties/MyContainersConfiguration.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testcontainers/atdevelopmenttime/dynamicproperties/MyContainersConfiguration.kt index 04959303fca..db3f5b0f9e9 100644 --- a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testcontainers/atdevelopmenttime/dynamicproperties/MyContainersConfiguration.kt +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testcontainers/atdevelopmenttime/dynamicproperties/MyContainersConfiguration.kt @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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,18 +18,23 @@ package org.springframework.boot.docs.features.testcontainers.atdevelopmenttime. import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean -import org.springframework.test.context.DynamicPropertyRegistry +import org.springframework.test.context.DynamicPropertyRegistrar; import org.testcontainers.containers.MongoDBContainer @TestConfiguration(proxyBeanMethods = false) class MyContainersConfiguration { @Bean - fun mongoDbContainer(properties: DynamicPropertyRegistry): MongoDBContainer { - var container = MongoDBContainer("mongo:5.0") - properties.add("spring.data.mongodb.host", container::getHost) - properties.add("spring.data.mongodb.port", container::getFirstMappedPort) - return container + fun mongoDbContainer(): MongoDBContainer { + return MongoDBContainer("mongo:5.0") + } + + @Bean + fun mongoDbProperties(container: MongoDBContainer): DynamicPropertyRegistrar { + return DynamicPropertyRegistrar { properties -> + properties.add("spring.data.mongodb.host") { container.host } + properties.add("spring.data.mongodb.port") { container.firstMappedPort } + } } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersTests.java b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersTests.java index c3d0bd43703..0ca80cbb01c 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersTests.java @@ -103,7 +103,7 @@ class ImportTestcontainersTests { void importWhenHasContainerDefinitionsWithDynamicPropertySource() { this.applicationContext = new AnnotationConfigApplicationContext( ContainerDefinitionsWithDynamicPropertySource.class); - assertThat(this.applicationContext.getEnvironment().containsProperty("container.port")).isTrue(); + assertThat(this.applicationContext.getEnvironment().getProperty("container.port")).isNotNull(); } @Test diff --git a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java index 8cf5003f64a..10ddb2f93f0 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java @@ -21,19 +21,22 @@ import java.util.List; import com.redis.testcontainers.RedisContainer; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent; import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleApplicationContextInitializer; import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable; import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.context.ApplicationEvent; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.test.context.DynamicPropertyRegistrar; import org.springframework.test.context.DynamicPropertyRegistry; import static org.assertj.core.api.Assertions.assertThat; @@ -42,8 +45,10 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link TestcontainersPropertySourceAutoConfiguration}. * * @author Phillip Webb + * @author Andy Wilkinson */ @DisabledIfDockerUnavailable +@ExtendWith(OutputCaptureExtension.class) class TestcontainersPropertySourceAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -51,18 +56,67 @@ class TestcontainersPropertySourceAutoConfigurationTests { .withConfiguration(AutoConfigurations.of(TestcontainersPropertySourceAutoConfiguration.class)); @Test - void containerBeanMethodContributesProperties() { - List events = new ArrayList<>(); + @SuppressWarnings("removal") + @Deprecated(since = "3.4.0", forRemoval = true) + void registeringADynamicPropertyFailsByDefault() { this.contextRunner.withUserConfiguration(ContainerAndPropertiesConfiguration.class) + .run((context) -> assertThat(context).getFailure() + .rootCause() + .isInstanceOf( + org.springframework.boot.testcontainers.properties.TestcontainersPropertySource.DynamicPropertyRegistryInjectionException.class) + .hasMessageStartingWith( + "Support for injecting a DynamicPropertyRegistry into @Bean methods is deprecated")); + } + + @Test + @SuppressWarnings("removal") + @Deprecated(since = "3.4.0", forRemoval = true) + void registeringADynamicPropertyCanLogAWarningAndContributeProperty(CapturedOutput output) { + List events = new ArrayList<>(); + this.contextRunner.withPropertyValues("spring.testcontainers.dynamic-property-registry-injection=warn") + .withUserConfiguration(ContainerAndPropertiesConfiguration.class) + .withInitializer((context) -> context.addApplicationListener(events::add)) + .run((context) -> { + TestBean testBean = context.getBean(TestBean.class); + RedisContainer redisContainer = context.getBean(RedisContainer.class); + assertThat(testBean.getUsingPort()).isEqualTo(redisContainer.getFirstMappedPort()); + assertThat(events.stream() + .filter(org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent.class::isInstance)) + .hasSize(1); + assertThat(output) + .contains("Support for injecting a DynamicPropertyRegistry into @Bean methods is deprecated"); + }); + } + + @Test + @SuppressWarnings("removal") + @Deprecated(since = "3.4.0", forRemoval = true) + void registeringADynamicPropertyCanBePermittedAndContributeProperty(CapturedOutput output) { + List events = new ArrayList<>(); + this.contextRunner.withPropertyValues("spring.testcontainers.dynamic-property-registry-injection=allow") + .withUserConfiguration(ContainerAndPropertiesConfiguration.class) .withInitializer((context) -> context.addApplicationListener(events::add)) .run((context) -> { TestBean testBean = context.getBean(TestBean.class); RedisContainer redisContainer = context.getBean(RedisContainer.class); assertThat(testBean.getUsingPort()).isEqualTo(redisContainer.getFirstMappedPort()); - assertThat(events.stream().filter(BeforeTestcontainerUsedEvent.class::isInstance)).hasSize(1); + assertThat(events.stream() + .filter(org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent.class::isInstance)) + .hasSize(1); + assertThat(output) + .doesNotContain("Support for injecting a DynamicPropertyRegistry into @Bean methods is deprecated"); }); } + @Test + void dynamicPropertyRegistrarBeanContributesProperties(CapturedOutput output) { + this.contextRunner.withUserConfiguration(ContainerAndPropertyRegistrarConfiguration.class).run((context) -> { + TestBean testBean = context.getBean(TestBean.class); + RedisContainer redisContainer = context.getBean(RedisContainer.class); + assertThat(testBean.getUsingPort()).isEqualTo(redisContainer.getFirstMappedPort()); + }); + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(ContainerProperties.class) @Import(TestBean.class) @@ -77,6 +131,23 @@ class TestcontainersPropertySourceAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(ContainerProperties.class) + @Import(TestBean.class) + static class ContainerAndPropertyRegistrarConfiguration { + + @Bean + RedisContainer redisContainer() { + return TestImage.container(RedisContainer.class); + } + + @Bean + DynamicPropertyRegistrar redisProperties(RedisContainer container) { + return (registry) -> registry.add("container.port", container::getFirstMappedPort); + } + + } + @ConfigurationProperties("container") record ContainerProperties(int port) { } diff --git a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationWithSpringBootTestIntegrationTest.java b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationWithSpringBootTestIntegrationTest.java index 85791e4d439..ea791d179cc 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationWithSpringBootTestIntegrationTest.java +++ b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationWithSpringBootTestIntegrationTest.java @@ -18,26 +18,41 @@ package org.springframework.boot.testcontainers.properties; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.testcontainers.properties.TestcontainersPropertySourceAutoConfigurationWithSpringBootTestIntegrationTest.TestConfig; import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; +import org.springframework.test.context.DynamicPropertyRegistrar; import org.springframework.test.context.DynamicPropertyRegistry; +import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link TestcontainersPropertySourceAutoConfiguration} when combined with * {@link SpringBootTest @SpringBootTest}. * * @author Phillip Webb + * @author Andy Wilkinson */ -@SpringBootTest(classes = TestConfig.class) +@SpringBootTest(classes = TestConfig.class, + properties = "spring.testcontainers.dynamic-property-registry-injection=allow") class TestcontainersPropertySourceAutoConfigurationWithSpringBootTestIntegrationTest { + @Autowired + private Environment environment; + @Test - void injectsRegistry() { + void injectsRegistryIntoBeanMethod() { + assertThat(this.environment.getProperty("from.bean.method")).isEqualTo("one"); + } + @Test + void callsRegistrars() { + assertThat(this.environment.getProperty("from.registrar")).isEqualTo("two"); } @TestConfiguration @@ -47,10 +62,15 @@ class TestcontainersPropertySourceAutoConfigurationWithSpringBootTestIntegration @Bean String example(DynamicPropertyRegistry registry) { - registry.add("test", () -> "test"); + registry.add("from.bean.method", () -> "one"); return "Hello"; } + @Bean + DynamicPropertyRegistrar propertyRegistrar() { + return (registry) -> registry.add("from.registrar", () -> "two"); + } + } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ContainerFieldsImporter.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ContainerFieldsImporter.java index a6909d890b8..7aa37330075 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ContainerFieldsImporter.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ContainerFieldsImporter.java @@ -19,9 +19,12 @@ package org.springframework.boot.testcontainers.context; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.testcontainers.containers.Container; +import org.testcontainers.lifecycle.Startable; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.autoconfigure.container.ContainerImageMetadata; @@ -35,12 +38,17 @@ import org.springframework.util.ReflectionUtils; */ class ContainerFieldsImporter { - void registerBeanDefinitions(BeanDefinitionRegistry registry, Class definitionClass) { + Set registerBeanDefinitions(BeanDefinitionRegistry registry, Class definitionClass) { + Set importedContainers = new HashSet<>(); for (Field field : getContainerFields(definitionClass)) { assertValid(field); Container container = getContainer(field); + if (container instanceof Startable startable) { + importedContainers.add(startable); + } registerBeanDefinition(registry, field, container); } + return importedContainers; } private List getContainerFields(Class containersClass) { diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java index d680f7504c8..be7cc9e1124 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java @@ -19,12 +19,16 @@ package org.springframework.boot.testcontainers.context; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Set; +import java.util.function.Supplier; +import org.testcontainers.lifecycle.Startable; + +import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.boot.testcontainers.properties.TestcontainersPropertySource; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.MethodIntrospector; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.env.Environment; +import org.springframework.test.context.DynamicPropertyRegistrar; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.util.Assert; @@ -32,30 +36,29 @@ import org.springframework.util.ReflectionUtils; /** * Used by {@link ImportTestcontainersRegistrar} to import - * {@link DynamicPropertySource @DynamicPropertySource} methods. + * {@link DynamicPropertySource @DynamicPropertySource} through a + * {@link DynamicPropertyRegistrar}. * * @author Phillip Webb + * @author Andy Wilkinson */ class DynamicPropertySourceMethodsImporter { - private final Environment environment; - - DynamicPropertySourceMethodsImporter(Environment environment) { - this.environment = environment; - } - - void registerDynamicPropertySources(BeanDefinitionRegistry beanDefinitionRegistry, Class definitionClass) { + void registerDynamicPropertySources(BeanDefinitionRegistry beanDefinitionRegistry, Class definitionClass, + Set importedContainers) { Set methods = MethodIntrospector.selectMethods(definitionClass, this::isAnnotated); if (methods.isEmpty()) { return; } - DynamicPropertyRegistry dynamicPropertyRegistry = TestcontainersPropertySource.attach(this.environment, - beanDefinitionRegistry); - methods.forEach((method) -> { - assertValid(method); - ReflectionUtils.makeAccessible(method); - ReflectionUtils.invokeMethod(method, null, dynamicPropertyRegistry); - }); + methods.forEach((method) -> assertValid(method)); + RootBeanDefinition registrarDefinition = new RootBeanDefinition(); + registrarDefinition.setBeanClass(DynamicPropertySourcePropertyRegistrar.class); + ConstructorArgumentValues arguments = new ConstructorArgumentValues(); + arguments.addGenericArgumentValue(methods); + arguments.addGenericArgumentValue(importedContainers); + registrarDefinition.setConstructorArgumentValues(arguments); + beanDefinitionRegistry.registerBeanDefinition(definitionClass.getName() + ".dynamicPropertyRegistrar", + registrarDefinition); } private boolean isAnnotated(Method method) { @@ -71,4 +74,52 @@ class DynamicPropertySourceMethodsImporter { + "' must accept a single DynamicPropertyRegistry argument"); } + static class DynamicPropertySourcePropertyRegistrar implements DynamicPropertyRegistrar { + + private final Set methods; + + private final Set containers; + + DynamicPropertySourcePropertyRegistrar(Set methods, Set containers) { + this.methods = methods; + this.containers = containers; + } + + @Override + public void accept(DynamicPropertyRegistry registry) { + DynamicPropertyRegistry containersBackedRegistry = new ContainersBackedDynamicPropertyRegistry(registry, + this.containers); + this.methods.forEach((method) -> { + ReflectionUtils.makeAccessible(method); + ReflectionUtils.invokeMethod(method, null, containersBackedRegistry); + }); + } + + } + + static class ContainersBackedDynamicPropertyRegistry implements DynamicPropertyRegistry { + + private final DynamicPropertyRegistry delegate; + + private final Set containers; + + ContainersBackedDynamicPropertyRegistry(DynamicPropertyRegistry delegate, Set containers) { + this.delegate = delegate; + this.containers = containers; + } + + @Override + public void add(String name, Supplier valueSupplier) { + this.delegate.add(name, () -> { + startContainers(); + return valueSupplier.get(); + }); + } + + private void startContainers() { + this.containers.forEach(Startable::start); + } + + } + } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainers.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainers.java index 5f99743017b..f20b98651f3 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainers.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainers.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -24,6 +24,8 @@ import java.lang.annotation.Target; import org.testcontainers.containers.Container; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.properties.TestcontainersPropertySourceAutoConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Import; @@ -43,6 +45,7 @@ import org.springframework.context.annotation.Import; @Retention(RetentionPolicy.RUNTIME) @Documented @Import(ImportTestcontainersRegistrar.class) +@ImportAutoConfiguration(TestcontainersPropertySourceAutoConfiguration.class) public @interface ImportTestcontainers { /** diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainersRegistrar.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainersRegistrar.java index 1c2cf49725c..e84a9e74537 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainersRegistrar.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainersRegistrar.java @@ -16,6 +16,10 @@ package org.springframework.boot.testcontainers.context; +import java.util.Set; + +import org.testcontainers.lifecycle.Startable; + import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.annotation.MergedAnnotation; @@ -43,7 +47,7 @@ class ImportTestcontainersRegistrar implements ImportBeanDefinitionRegistrar { ImportTestcontainersRegistrar(Environment environment) { this.containerFieldsImporter = new ContainerFieldsImporter(); this.dynamicPropertySourceMethodsImporter = (!ClassUtils.isPresent(DYNAMIC_PROPERTY_SOURCE_CLASS, null)) ? null - : new DynamicPropertySourceMethodsImporter(environment); + : new DynamicPropertySourceMethodsImporter(); } @Override @@ -60,9 +64,11 @@ class ImportTestcontainersRegistrar implements ImportBeanDefinitionRegistrar { private void registerBeanDefinitions(BeanDefinitionRegistry registry, Class[] definitionClasses) { for (Class definitionClass : definitionClasses) { - this.containerFieldsImporter.registerBeanDefinitions(registry, definitionClass); + Set importedContainers = this.containerFieldsImporter.registerBeanDefinitions(registry, + definitionClass); if (this.dynamicPropertySourceMethodsImporter != null) { - this.dynamicPropertySourceMethodsImporter.registerDynamicPropertySources(registry, definitionClass); + this.dynamicPropertySourceMethodsImporter.registerDynamicPropertySources(registry, definitionClass, + importedContainers); } } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/BeforeTestcontainerUsedEvent.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/BeforeTestcontainerUsedEvent.java index c31963302fc..eeab2128608 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/BeforeTestcontainerUsedEvent.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/BeforeTestcontainerUsedEvent.java @@ -19,13 +19,18 @@ package org.springframework.boot.testcontainers.lifecycle; import org.testcontainers.containers.Container; import org.springframework.context.ApplicationEvent; +import org.springframework.test.context.DynamicPropertyRegistrar; /** * Event published just before a Testcontainers {@link Container} is used. * * @author Andy Wilkinson * @since 3.2.6 + * @deprecated since 3.4.0 for removal in 3.6.0 in favor of property registration using a + * {@link DynamicPropertyRegistrar} bean that injects the {@link Container} from which the + * properties will be sourced. */ +@Deprecated(since = "3.4.0", forRemoval = true) public class BeforeTestcontainerUsedEvent extends ApplicationEvent { public BeforeTestcontainerUsedEvent(Object source) { diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java index 26a8cc9187f..ee3e52434ff 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java @@ -58,6 +58,7 @@ import org.springframework.core.log.LogMessage; * @author Scott Frederick * @see TestcontainersLifecycleApplicationContextInitializer */ +@SuppressWarnings({ "removal", "deprecation" }) @Order(Ordered.LOWEST_PRECEDENCE) class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPostProcessor, ApplicationListener { @@ -79,6 +80,7 @@ class TestcontainersLifecycleBeanPostProcessor } @Override + @Deprecated(since = "3.4.0", forRemoval = true) public void onApplicationEvent(BeforeTestcontainerUsedEvent event) { initializeContainers(); } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java index ee2ae41359b..f9a5730242d 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java @@ -23,6 +23,8 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.function.Supplier; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.testcontainers.containers.Container; import org.springframework.beans.BeansException; @@ -30,6 +32,8 @@ import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; @@ -39,6 +43,7 @@ import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; +import org.springframework.test.context.DynamicPropertyRegistrar; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.util.Assert; import org.springframework.util.function.SupplierUtils; @@ -49,23 +54,31 @@ import org.springframework.util.function.SupplierUtils; * * @author Phillip Webb * @since 3.1.0 + * @deprecated since 3.4.0 for removal in 3.6.0 in favor of declaring one or more + * {@link DynamicPropertyRegistrar} beans. */ +@SuppressWarnings("removal") +@Deprecated(since = "3.4.0", forRemoval = true) public class TestcontainersPropertySource extends MapPropertySource { + private static final Log logger = LogFactory.getLog(TestcontainersPropertySource.class); + static final String NAME = "testcontainersPropertySource"; private final DynamicPropertyRegistry registry; private final Set eventPublishers = new CopyOnWriteArraySet<>(); - TestcontainersPropertySource() { - this(Collections.synchronizedMap(new LinkedHashMap<>())); + TestcontainersPropertySource(DynamicPropertyRegistryInjection registryInjection) { + this(Collections.synchronizedMap(new LinkedHashMap<>()), registryInjection); } - private TestcontainersPropertySource(Map> valueSuppliers) { + private TestcontainersPropertySource(Map> valueSuppliers, + DynamicPropertyRegistryInjection registryInjection) { super(NAME, Collections.unmodifiableMap(valueSuppliers)); this.registry = (name, valueSupplier) -> { Assert.hasText(name, "'name' must not be null or blank"); + DynamicPropertyRegistryInjectionException.throwIfNecessary(name, registryInjection); Assert.notNull(valueSupplier, "'valueSupplier' must not be null"); valueSuppliers.put(name, valueSupplier); }; @@ -117,7 +130,12 @@ public class TestcontainersPropertySource extends MapPropertySource { static TestcontainersPropertySource getOrAdd(ConfigurableEnvironment environment) { PropertySource propertySource = environment.getPropertySources().get(NAME); if (propertySource == null) { - environment.getPropertySources().addFirst(new TestcontainersPropertySource()); + BindResult bindingResult = Binder.get(environment) + .bind("spring.testcontainers.dynamic-property-registry-injection", + DynamicPropertyRegistryInjection.class); + environment.getPropertySources() + .addFirst( + new TestcontainersPropertySource(bindingResult.orElse(DynamicPropertyRegistryInjection.FAIL))); return getOrAdd(environment); } Assert.state(propertySource instanceof TestcontainersPropertySource, @@ -157,4 +175,34 @@ public class TestcontainersPropertySource extends MapPropertySource { } + private enum DynamicPropertyRegistryInjection { + + ALLOW, + + FAIL, + + WARN + + } + + static final class DynamicPropertyRegistryInjectionException extends RuntimeException { + + private DynamicPropertyRegistryInjectionException(String propertyName) { + super("Support for injecting a DynamicPropertyRegistry into @Bean methods is deprecated. Register '" + + propertyName + "' using a DynamicPropertyRegistrar bean instead. Alternatively, set " + + "spring.testcontainers.dynamic-property-registry-injection to 'warn' to replace this " + + "failure with a warning or to 'allow' to permit injection of the registry."); + } + + private static void throwIfNecessary(String propertyName, DynamicPropertyRegistryInjection registryInjection) { + switch (registryInjection) { + case FAIL -> throw new DynamicPropertyRegistryInjectionException(propertyName); + case WARN -> logger + .warn("Support for injecting a DynamicPropertyRegistry into @Bean methods is deprecated. Register '" + + propertyName + "' using a DynamicPropertyRegistrar bean instead."); + } + } + + } + } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfiguration.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfiguration.java index e6219f2d2d4..0d6271906a1 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfiguration.java @@ -16,19 +16,27 @@ package org.springframework.boot.testcontainers.properties; +import org.testcontainers.containers.GenericContainer; + +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Role; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.support.DynamicPropertyRegistrarBeanInitializer; /** * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration - * Auto-configuration} to add {@link TestcontainersPropertySource} support. + * Auto-configuration} to add support for properties sourced from a Testcontainers + * {@link GenericContainer container}. * * @author Phillip Webb + * @author Andy Wilkinson * @since 3.1.0 */ @AutoConfiguration @@ -36,12 +44,18 @@ import org.springframework.test.context.DynamicPropertyRegistry; @ConditionalOnClass(DynamicPropertyRegistry.class) public class TestcontainersPropertySourceAutoConfiguration { - TestcontainersPropertySourceAutoConfiguration() { - } - @Bean + @SuppressWarnings("removal") + @Deprecated(since = "3.4.0", forRemoval = true) static DynamicPropertyRegistry dynamicPropertyRegistry(ConfigurableApplicationContext applicationContext) { return TestcontainersPropertySource.attach(applicationContext); } + @Bean + @ConditionalOnMissingBean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static DynamicPropertyRegistrarBeanInitializer dynamicPropertyRegistrarBeanInitializer() { + return new DynamicPropertyRegistrarBeanInitializer(); + } + } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactory.java index 968c34b56fa..af6e6f916e7 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactory.java @@ -23,6 +23,7 @@ import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.testcontainers.containers.Container; +import org.testcontainers.lifecycle.Startable; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -32,10 +33,8 @@ import org.springframework.boot.autoconfigure.service.connection.ConnectionDetai import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory; import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.OriginProvider; -import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.ResolvableType; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler; @@ -164,8 +163,6 @@ public abstract class ContainerConnectionDetailsFactory, private final ContainerConnectionSource source; - private volatile ApplicationEventPublisher eventPublisher; - private volatile C container; /** @@ -190,7 +187,9 @@ public abstract class ContainerConnectionDetailsFactory, protected final C getContainer() { Assert.state(this.container != null, "Container cannot be obtained before the connection details bean has been initialized"); - this.eventPublisher.publishEvent(new BeforeTestcontainerUsedEvent(this)); + if (this.container instanceof Startable startable) { + startable.start(); + } return this.container; } @@ -200,8 +199,8 @@ public abstract class ContainerConnectionDetailsFactory, } @Override + @Deprecated(since = "3.4.0", forRemoval = true) public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.eventPublisher = applicationContext; } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json index ca82e22875b..cc24a21765e 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -3,8 +3,32 @@ { "name": "spring.testcontainers.beans.startup", "type": "org.springframework.boot.testcontainers.lifecycle.TestcontainersStartup", - "description": "Testcontainers startup modes.", - "defaultValue": "sequential" + "description": "Testcontainers startup modes.", + "defaultValue": "sequential" + }, + { + "name": "spring.testcontainers.dynamic-property-registry-injection", + "description": "How to treat injection of DynamicPropertyRegistry into a @Bean method.", + "defaultValue": "fail" + } + ], + "hints": [ + { + "name": "spring.testcontainers.dynamic-property-registry-injection", + "values": [ + { + "value": "fail", + "description": "Fail with an exception." + }, + { + "value": "warn", + "description": "Log a warning." + }, + { + "value": "allow", + "description": "Allow the use despite its deprecation." + } + ] } ] } diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java index 86d43e647f2..177322aeede 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java @@ -25,10 +25,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent; -import org.springframework.boot.testcontainers.properties.TestcontainersPropertySource.EventPublisherRegistrar; import org.springframework.context.ApplicationEvent; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.context.DynamicPropertyRegistry; @@ -40,10 +41,14 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * Tests for {@link TestcontainersPropertySource}. * * @author Phillip Webb + * @deprecated since 3.4.0 for removal in 3.6.0 */ +@SuppressWarnings("removal") +@Deprecated(since = "3.4.0", forRemoval = true) class TestcontainersPropertySourceTests { - private MockEnvironment environment = new MockEnvironment(); + private MockEnvironment environment = new MockEnvironment() + .withProperty("spring.testcontainers.dynamic-property-registry-injection", "allow"); private GenericApplicationContext context = new GenericApplicationContext(); @@ -121,7 +126,8 @@ class TestcontainersPropertySourceTests { TestcontainersPropertySource.attach(this.environment, this.context); PropertySource propertySource = this.environment.getPropertySources().get(TestcontainersPropertySource.NAME); assertThat(propertySource).isNotNull(); - assertThat(this.context.containsBean(EventPublisherRegistrar.NAME)); + assertThat(this.context.containsBean( + org.springframework.boot.testcontainers.properties.TestcontainersPropertySource.EventPublisherRegistrar.NAME)); } @Test @@ -137,15 +143,19 @@ class TestcontainersPropertySourceTests { @Test void getPropertyPublishesEvent() { try (GenericApplicationContext applicationContext = new GenericApplicationContext()) { + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + environment.getPropertySources() + .addLast(new MapPropertySource("test", + Map.of("spring.testcontainers.dynamic-property-registry-injection", "allow"))); List events = new ArrayList<>(); applicationContext.addApplicationListener(events::add); - DynamicPropertyRegistry registry = TestcontainersPropertySource.attach(applicationContext.getEnvironment(), + DynamicPropertyRegistry registry = TestcontainersPropertySource.attach(environment, (BeanDefinitionRegistry) applicationContext.getBeanFactory()); applicationContext.refresh(); registry.add("test", () -> "spring"); - assertThat(applicationContext.getEnvironment().containsProperty("test")).isTrue(); + assertThat(environment.containsProperty("test")).isTrue(); assertThat(events.isEmpty()); - assertThat(applicationContext.getEnvironment().getProperty("test")).isEqualTo("spring"); + assertThat(environment.getProperty("test")).isEqualTo("spring"); assertThat(events.stream().filter(BeforeTestcontainerUsedEvent.class::isInstance)).hasSize(1); } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactoryTests.java index f5cc2b4e282..b24b25f1a81 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactoryTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactoryTests.java @@ -30,16 +30,12 @@ import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory; import org.springframework.boot.origin.Origin; -import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactoryTests.TestContainerConnectionDetailsFactory.TestContainerConnectionDetails; -import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.MergedAnnotation; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; /** @@ -135,14 +131,11 @@ class ContainerConnectionDetailsFactoryTests { } @Test - void getContainerWhenInitializedPublishesEventAndReturnsSuppliedContainer() throws Exception { + void getContainerWhenInitializedReturnsSuppliedContainer() throws Exception { TestContainerConnectionDetailsFactory factory = new TestContainerConnectionDetailsFactory(); TestContainerConnectionDetails connectionDetails = getConnectionDetails(factory, this.source); - ApplicationContext context = mock(ApplicationContext.class); - connectionDetails.setApplicationContext(context); connectionDetails.afterPropertiesSet(); assertThat(connectionDetails.callGetContainer()).isSameAs(this.container); - then(context).should().publishEvent(any(BeforeTestcontainerUsedEvent.class)); } @Test