Browse Source

Prefer DynamicPropertyRegistar to DynamicPropertyRegistry

Closes gh-41996
pull/42592/head
Andy Wilkinson 1 year ago
parent
commit
3c095b4ec2
  1. 6
      spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc
  2. 17
      spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/devservices/testcontainers/atdevelopmenttime/dynamicproperties/MyContainersConfiguration.java
  3. 19
      spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testcontainers/atdevelopmenttime/dynamicproperties/MyContainersConfiguration.kt
  4. 2
      spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersTests.java
  5. 79
      spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java
  6. 26
      spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationWithSpringBootTestIntegrationTest.java
  7. 10
      spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ContainerFieldsImporter.java
  8. 85
      spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java
  9. 5
      spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainers.java
  10. 12
      spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainersRegistrar.java
  11. 5
      spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/BeforeTestcontainerUsedEvent.java
  12. 2
      spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java
  13. 56
      spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java
  14. 22
      spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfiguration.java
  15. 11
      spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactory.java
  16. 28
      spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json
  17. 22
      spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java
  18. 9
      spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactoryTests.java

6
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 @@ -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:

17
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; @@ -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);
};
}
}

19
spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testcontainers/atdevelopmenttime/dynamicproperties/MyContainersConfiguration.kt

@ -1,5 +1,5 @@ @@ -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. @@ -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 }
}
}
}

2
spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/ImportTestcontainersTests.java

@ -103,7 +103,7 @@ class ImportTestcontainersTests { @@ -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

79
spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java

@ -21,19 +21,22 @@ import java.util.List; @@ -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; @@ -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 { @@ -51,18 +56,67 @@ class TestcontainersPropertySourceAutoConfigurationTests {
.withConfiguration(AutoConfigurations.of(TestcontainersPropertySourceAutoConfiguration.class));
@Test
void containerBeanMethodContributesProperties() {
List<ApplicationEvent> 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<ApplicationEvent> 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<ApplicationEvent> 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 { @@ -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) {
}

26
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; @@ -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 @@ -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");
}
}
}

10
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; @@ -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; @@ -35,12 +38,17 @@ import org.springframework.util.ReflectionUtils;
*/
class ContainerFieldsImporter {
void registerBeanDefinitions(BeanDefinitionRegistry registry, Class<?> definitionClass) {
Set<Startable> registerBeanDefinitions(BeanDefinitionRegistry registry, Class<?> definitionClass) {
Set<Startable> 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<Field> getContainerFields(Class<?> containersClass) {

85
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; @@ -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; @@ -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<Startable> importedContainers) {
Set<Method> 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 { @@ -71,4 +74,52 @@ class DynamicPropertySourceMethodsImporter {
+ "' must accept a single DynamicPropertyRegistry argument");
}
static class DynamicPropertySourcePropertyRegistrar implements DynamicPropertyRegistrar {
private final Set<Method> methods;
private final Set<Startable> containers;
DynamicPropertySourcePropertyRegistrar(Set<Method> methods, Set<Startable> 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<Startable> containers;
ContainersBackedDynamicPropertyRegistry(DynamicPropertyRegistry delegate, Set<Startable> containers) {
this.delegate = delegate;
this.containers = containers;
}
@Override
public void add(String name, Supplier<Object> valueSupplier) {
this.delegate.add(name, () -> {
startContainers();
return valueSupplier.get();
});
}
private void startContainers() {
this.containers.forEach(Startable::start);
}
}
}

5
spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainers.java

@ -1,5 +1,5 @@ @@ -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; @@ -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; @@ -43,6 +45,7 @@ import org.springframework.context.annotation.Import;
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ImportTestcontainersRegistrar.class)
@ImportAutoConfiguration(TestcontainersPropertySourceAutoConfiguration.class)
public @interface ImportTestcontainers {
/**

12
spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainersRegistrar.java

@ -16,6 +16,10 @@ @@ -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 { @@ -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 { @@ -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<Startable> importedContainers = this.containerFieldsImporter.registerBeanDefinitions(registry,
definitionClass);
if (this.dynamicPropertySourceMethodsImporter != null) {
this.dynamicPropertySourceMethodsImporter.registerDynamicPropertySources(registry, definitionClass);
this.dynamicPropertySourceMethodsImporter.registerDynamicPropertySources(registry, definitionClass,
importedContainers);
}
}
}

5
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; @@ -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) {

2
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; @@ -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<BeforeTestcontainerUsedEvent> {
@ -79,6 +80,7 @@ class TestcontainersLifecycleBeanPostProcessor @@ -79,6 +80,7 @@ class TestcontainersLifecycleBeanPostProcessor
}
@Override
@Deprecated(since = "3.4.0", forRemoval = true)
public void onApplicationEvent(BeforeTestcontainerUsedEvent event) {
initializeContainers();
}

56
spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java

@ -23,6 +23,8 @@ import java.util.Set; @@ -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; @@ -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; @@ -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; @@ -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<ApplicationEventPublisher> eventPublishers = new CopyOnWriteArraySet<>();
TestcontainersPropertySource() {
this(Collections.synchronizedMap(new LinkedHashMap<>()));
TestcontainersPropertySource(DynamicPropertyRegistryInjection registryInjection) {
this(Collections.synchronizedMap(new LinkedHashMap<>()), registryInjection);
}
private TestcontainersPropertySource(Map<String, Supplier<Object>> valueSuppliers) {
private TestcontainersPropertySource(Map<String, Supplier<Object>> 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 { @@ -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<DynamicPropertyRegistryInjection> 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 { @@ -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.");
}
}
}
}

22
spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfiguration.java

@ -16,19 +16,27 @@ @@ -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; @@ -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();
}
}

11
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; @@ -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 @@ -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<C extends Container<?>, @@ -164,8 +163,6 @@ public abstract class ContainerConnectionDetailsFactory<C extends Container<?>,
private final ContainerConnectionSource<C> source;
private volatile ApplicationEventPublisher eventPublisher;
private volatile C container;
/**
@ -190,7 +187,9 @@ public abstract class ContainerConnectionDetailsFactory<C extends Container<?>, @@ -190,7 +187,9 @@ public abstract class ContainerConnectionDetailsFactory<C extends Container<?>,
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<C extends Container<?>, @@ -200,8 +199,8 @@ public abstract class ContainerConnectionDetailsFactory<C extends Container<?>,
}
@Override
@Deprecated(since = "3.4.0", forRemoval = true)
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.eventPublisher = applicationContext;
}
}

28
spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json

@ -3,8 +3,32 @@ @@ -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."
}
]
}
]
}

22
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; @@ -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; @@ -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 { @@ -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 { @@ -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<ApplicationEvent> 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);
}
}

9
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; @@ -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 { @@ -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

Loading…
Cancel
Save