From 468e246dbbb558c7b1b8bc415a4cdf8aec9cf60c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 21 May 2024 19:53:40 +0100 Subject: [PATCH] Make sure container's started before connection details use it Prior to this commit, a Testcontainer that was managed as a bean would not have been started in time if it was accessed before the bean factory's configuration had been frozen. A common way for this to occur is when using JPA. The entity manager factory bean is LoadTimeWeaverAware which causes it to be created before configuration is frozen. Creating this bean requires the DataSource which in turn requires the JdbcConnectionDetails and its JDBC URL. Getting the JDBC URL From the connection details requires the container hosting the SQL database to have been started. This commit updates ContainerConnectionDetails, the super-class for all Testcontainer-based ConnectionDetails implementations, to publish an event when the Container is retrieved from the details. When this event is published, TestcontainersLifecycleBeanPostProcessor initializes all containers that are defined as beans. Closes gh-40585 --- .../BeforeTestcontainerUsedEvent.java | 35 +++++++++ ...tcontainersLifecycleBeanPostProcessor.java | 5 +- ...reTestcontainersPropertySuppliedEvent.java | 6 +- .../TestcontainersPropertySource.java | 1 + .../ContainerConnectionDetailsFactory.java | 15 +++- ...LoadTimeWeaverAwareConsumerContainers.java | 35 +++++++++ ...wareConsumerImportTestcontainersTests.java | 71 +++++++++++++++++++ ...sPropertySourceAutoConfigurationTests.java | 1 + .../TestcontainersPropertySourceTests.java | 1 + ...ontainerConnectionDetailsFactoryTests.java | 11 ++- src/checkstyle/checkstyle-suppressions.xml | 1 + 11 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/BeforeTestcontainerUsedEvent.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/LoadTimeWeaverAwareConsumerContainers.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/LoadTimeWeaverAwareConsumerImportTestcontainersTests.java 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 new file mode 100644 index 00000000000..c31963302fc --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/BeforeTestcontainerUsedEvent.java @@ -0,0 +1,35 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testcontainers.lifecycle; + +import org.testcontainers.containers.Container; + +import org.springframework.context.ApplicationEvent; + +/** + * Event published just before a Testcontainers {@link Container} is used. + * + * @author Andy Wilkinson + * @since 3.2.6 + */ +public class BeforeTestcontainerUsedEvent extends ApplicationEvent { + + public BeforeTestcontainerUsedEvent(Object source) { + super(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 7d23eb65367..7239af49515 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 @@ -39,7 +39,6 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; -import org.springframework.boot.testcontainers.properties.BeforeTestcontainersPropertySuppliedEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -61,7 +60,7 @@ import org.springframework.core.log.LogMessage; */ @Order(Ordered.LOWEST_PRECEDENCE) class TestcontainersLifecycleBeanPostProcessor - implements DestructionAwareBeanPostProcessor, ApplicationListener { + implements DestructionAwareBeanPostProcessor, ApplicationListener { private static final Log logger = LogFactory.getLog(TestcontainersLifecycleBeanPostProcessor.class); @@ -80,7 +79,7 @@ class TestcontainersLifecycleBeanPostProcessor } @Override - public void onApplicationEvent(BeforeTestcontainersPropertySuppliedEvent event) { + public void onApplicationEvent(BeforeTestcontainerUsedEvent event) { initializeContainers(); } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/BeforeTestcontainersPropertySuppliedEvent.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/BeforeTestcontainersPropertySuppliedEvent.java index 4efe5990297..073eada8559 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/BeforeTestcontainersPropertySuppliedEvent.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/BeforeTestcontainersPropertySuppliedEvent.java @@ -18,7 +18,7 @@ package org.springframework.boot.testcontainers.properties; import java.util.function.Supplier; -import org.springframework.context.ApplicationEvent; +import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent; /** * Event published just before the {@link Supplier value supplier} of a @@ -26,8 +26,10 @@ import org.springframework.context.ApplicationEvent; * * @author Phillip Webb * @since 3.2.2 + * @deprecated since 3.2.6 in favor of {@link BeforeTestcontainerUsedEvent} */ -public class BeforeTestcontainersPropertySuppliedEvent extends ApplicationEvent { +@Deprecated(since = "3.2.6", forRemoval = true) +public class BeforeTestcontainersPropertySuppliedEvent extends BeforeTestcontainerUsedEvent { private final String propertyName; 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 d2df1e65b10..f1ecfe878c8 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 @@ -79,6 +79,7 @@ public class TestcontainersPropertySource extends EnumerablePropertySource valueSupplier) { BeforeTestcontainersPropertySuppliedEvent event = new BeforeTestcontainersPropertySuppliedEvent(this, name); this.eventPublishers.forEach((eventPublisher) -> eventPublisher.publishEvent(event)); 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 d2edbd9db57..b4bb38e17e7 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 @@ -25,11 +25,16 @@ import org.testcontainers.containers.Container; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; 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.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; @@ -123,10 +128,12 @@ public abstract class ContainerConnectionDetailsFactory, * @param the container type */ protected static class ContainerConnectionDetails> - implements ConnectionDetails, OriginProvider, InitializingBean { + implements ConnectionDetails, OriginProvider, InitializingBean, ApplicationContextAware { private final ContainerConnectionSource source; + private volatile ApplicationEventPublisher eventPublisher; + private volatile C container; /** @@ -151,6 +158,7 @@ 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)); return this.container; } @@ -159,6 +167,11 @@ public abstract class ContainerConnectionDetailsFactory, return this.source.getOrigin(); } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.eventPublisher = applicationContext; + } + } static class ContainerConnectionDetailsFactoriesRuntimeHints implements RuntimeHintsRegistrar { diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/LoadTimeWeaverAwareConsumerContainers.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/LoadTimeWeaverAwareConsumerContainers.java new file mode 100644 index 00000000000..da5f6c8a787 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/LoadTimeWeaverAwareConsumerContainers.java @@ -0,0 +1,35 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testcontainers; + +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; + +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; + +/** + * Container definitions for {@link LoadTimeWeaverAwareConsumerImportTestcontainersTests}. + * + * @author Andy Wilkinson + */ +interface LoadTimeWeaverAwareConsumerContainers { + + @Container + @ServiceConnection + PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer<>("postgres:16.1"); + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/LoadTimeWeaverAwareConsumerImportTestcontainersTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/LoadTimeWeaverAwareConsumerImportTestcontainersTests.java new file mode 100644 index 00000000000..75b5b7473eb --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/LoadTimeWeaverAwareConsumerImportTestcontainersTests.java @@ -0,0 +1,71 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testcontainers; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.context.ImportTestcontainers; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.weaving.LoadTimeWeaverAware; +import org.springframework.instrument.classloading.LoadTimeWeaver; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ImportTestcontainers(LoadTimeWeaverAwareConsumerContainers.class) +public class LoadTimeWeaverAwareConsumerImportTestcontainersTests implements LoadTimeWeaverAwareConsumerContainers { + + @Autowired + private LoadTimeWeaverAwareConsumer consumer; + + @Test + void loadTimeWeaverAwareBeanCanUseJdbcUrlFromContainerBasedConnectionDetails() { + assertThat(this.consumer.jdbcUrl).isNotNull(); + } + + @Configuration + @ImportAutoConfiguration(DataSourceAutoConfiguration.class) + static class TestConfiguration { + + @Bean + LoadTimeWeaverAwareConsumer loadTimeWeaverAwareConsumer(JdbcConnectionDetails connectionDetails) { + return new LoadTimeWeaverAwareConsumer(connectionDetails); + } + + } + + static class LoadTimeWeaverAwareConsumer implements LoadTimeWeaverAware { + + private final String jdbcUrl; + + LoadTimeWeaverAwareConsumer(JdbcConnectionDetails connectionDetails) { + this.jdbcUrl = connectionDetails.getJdbcUrl(); + } + + @Override + public void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver) { + } + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java index 90103b24d78..59f1817e02a 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java @@ -49,6 +49,7 @@ class TestcontainersPropertySourceAutoConfigurationTests { .withConfiguration(AutoConfigurations.of(TestcontainersPropertySourceAutoConfiguration.class)); @Test + @SuppressWarnings("removal") void containerBeanMethodContributesProperties() { List events = new ArrayList<>(); this.contextRunner.withUserConfiguration(ContainerAndPropertiesConfiguration.class) 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 e11488b349c..8b913d4945b 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 @@ -134,6 +134,7 @@ class TestcontainersPropertySourceTests { } @Test + @SuppressWarnings("removal") void getPropertyPublishesEvent() { try (GenericApplicationContext applicationContext = new GenericApplicationContext()) { List events = new ArrayList<>(); 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 bab9bf3488f..d1039f3e75b 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 @@ -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. @@ -28,11 +28,15 @@ 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.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; /** @@ -112,11 +116,14 @@ class ContainerConnectionDetailsFactoryTests { } @Test - void getContainerWhenInitializedReturnsSuppliedContainer() throws Exception { + void getContainerWhenInitializedPublishesEventAndReturnsSuppliedContainer() 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)); } @SuppressWarnings({ "rawtypes", "unchecked" }) diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index fdec73a1639..238b5d5c6c5 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -82,4 +82,5 @@ +