Skip to content

Commit 468e246

Browse files
committed
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
1 parent cb22d57 commit 468e246

11 files changed

+174
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.testcontainers.lifecycle;
18+
19+
import org.testcontainers.containers.Container;
20+
21+
import org.springframework.context.ApplicationEvent;
22+
23+
/**
24+
* Event published just before a Testcontainers {@link Container} is used.
25+
*
26+
* @author Andy Wilkinson
27+
* @since 3.2.6
28+
*/
29+
public class BeforeTestcontainerUsedEvent extends ApplicationEvent {
30+
31+
public BeforeTestcontainerUsedEvent(Object source) {
32+
super(source);
33+
}
34+
35+
}

Diff for: spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
import org.springframework.beans.factory.config.BeanPostProcessor;
4040
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
4141
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
42-
import org.springframework.boot.testcontainers.properties.BeforeTestcontainersPropertySuppliedEvent;
4342
import org.springframework.context.ApplicationListener;
4443
import org.springframework.core.Ordered;
4544
import org.springframework.core.annotation.Order;
@@ -61,7 +60,7 @@
6160
*/
6261
@Order(Ordered.LOWEST_PRECEDENCE)
6362
class TestcontainersLifecycleBeanPostProcessor
64-
implements DestructionAwareBeanPostProcessor, ApplicationListener<BeforeTestcontainersPropertySuppliedEvent> {
63+
implements DestructionAwareBeanPostProcessor, ApplicationListener<BeforeTestcontainerUsedEvent> {
6564

6665
private static final Log logger = LogFactory.getLog(TestcontainersLifecycleBeanPostProcessor.class);
6766

@@ -80,7 +79,7 @@ class TestcontainersLifecycleBeanPostProcessor
8079
}
8180

8281
@Override
83-
public void onApplicationEvent(BeforeTestcontainersPropertySuppliedEvent event) {
82+
public void onApplicationEvent(BeforeTestcontainerUsedEvent event) {
8483
initializeContainers();
8584
}
8685

Diff for: spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/BeforeTestcontainersPropertySuppliedEvent.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,18 @@
1818

1919
import java.util.function.Supplier;
2020

21-
import org.springframework.context.ApplicationEvent;
21+
import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent;
2222

2323
/**
2424
* Event published just before the {@link Supplier value supplier} of a
2525
* {@link TestcontainersPropertySource} property is called.
2626
*
2727
* @author Phillip Webb
2828
* @since 3.2.2
29+
* @deprecated since 3.2.6 in favor of {@link BeforeTestcontainerUsedEvent}
2930
*/
30-
public class BeforeTestcontainersPropertySuppliedEvent extends ApplicationEvent {
31+
@Deprecated(since = "3.2.6", forRemoval = true)
32+
public class BeforeTestcontainersPropertySuppliedEvent extends BeforeTestcontainerUsedEvent {
3133

3234
private final String propertyName;
3335

Diff for: spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public Object getProperty(String name) {
7979
return (valueSupplier != null) ? getProperty(name, valueSupplier) : null;
8080
}
8181

82+
@SuppressWarnings({ "removal", "deprecation" })
8283
private Object getProperty(String name, Supplier<Object> valueSupplier) {
8384
BeforeTestcontainersPropertySuppliedEvent event = new BeforeTestcontainersPropertySuppliedEvent(this, name);
8485
this.eventPublishers.forEach((eventPublisher) -> eventPublisher.publishEvent(event));

Diff for: spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactory.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,16 @@
2525

2626
import org.springframework.aot.hint.RuntimeHints;
2727
import org.springframework.aot.hint.RuntimeHintsRegistrar;
28+
import org.springframework.beans.BeansException;
2829
import org.springframework.beans.factory.InitializingBean;
2930
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
3031
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
3132
import org.springframework.boot.origin.Origin;
3233
import org.springframework.boot.origin.OriginProvider;
34+
import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent;
35+
import org.springframework.context.ApplicationContext;
36+
import org.springframework.context.ApplicationContextAware;
37+
import org.springframework.context.ApplicationEventPublisher;
3338
import org.springframework.core.ResolvableType;
3439
import org.springframework.core.io.support.SpringFactoriesLoader;
3540
import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
@@ -123,10 +128,12 @@ private Class<?>[] resolveGenerics() {
123128
* @param <C> the container type
124129
*/
125130
protected static class ContainerConnectionDetails<C extends Container<?>>
126-
implements ConnectionDetails, OriginProvider, InitializingBean {
131+
implements ConnectionDetails, OriginProvider, InitializingBean, ApplicationContextAware {
127132

128133
private final ContainerConnectionSource<C> source;
129134

135+
private volatile ApplicationEventPublisher eventPublisher;
136+
130137
private volatile C container;
131138

132139
/**
@@ -151,6 +158,7 @@ public void afterPropertiesSet() throws Exception {
151158
protected final C getContainer() {
152159
Assert.state(this.container != null,
153160
"Container cannot be obtained before the connection details bean has been initialized");
161+
this.eventPublisher.publishEvent(new BeforeTestcontainerUsedEvent(this));
154162
return this.container;
155163
}
156164

@@ -159,6 +167,11 @@ public Origin getOrigin() {
159167
return this.source.getOrigin();
160168
}
161169

170+
@Override
171+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
172+
this.eventPublisher = applicationContext;
173+
}
174+
162175
}
163176

164177
static class ContainerConnectionDetailsFactoriesRuntimeHints implements RuntimeHintsRegistrar {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.testcontainers;
18+
19+
import org.testcontainers.containers.PostgreSQLContainer;
20+
import org.testcontainers.junit.jupiter.Container;
21+
22+
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
23+
24+
/**
25+
* Container definitions for {@link LoadTimeWeaverAwareConsumerImportTestcontainersTests}.
26+
*
27+
* @author Andy Wilkinson
28+
*/
29+
interface LoadTimeWeaverAwareConsumerContainers {
30+
31+
@Container
32+
@ServiceConnection
33+
PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>("postgres:16.1");
34+
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.testcontainers;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.beans.factory.annotation.Autowired;
22+
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
23+
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
24+
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
25+
import org.springframework.boot.test.context.SpringBootTest;
26+
import org.springframework.boot.testcontainers.context.ImportTestcontainers;
27+
import org.springframework.context.annotation.Bean;
28+
import org.springframework.context.annotation.Configuration;
29+
import org.springframework.context.weaving.LoadTimeWeaverAware;
30+
import org.springframework.instrument.classloading.LoadTimeWeaver;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
34+
@SpringBootTest
35+
@ImportTestcontainers(LoadTimeWeaverAwareConsumerContainers.class)
36+
public class LoadTimeWeaverAwareConsumerImportTestcontainersTests implements LoadTimeWeaverAwareConsumerContainers {
37+
38+
@Autowired
39+
private LoadTimeWeaverAwareConsumer consumer;
40+
41+
@Test
42+
void loadTimeWeaverAwareBeanCanUseJdbcUrlFromContainerBasedConnectionDetails() {
43+
assertThat(this.consumer.jdbcUrl).isNotNull();
44+
}
45+
46+
@Configuration
47+
@ImportAutoConfiguration(DataSourceAutoConfiguration.class)
48+
static class TestConfiguration {
49+
50+
@Bean
51+
LoadTimeWeaverAwareConsumer loadTimeWeaverAwareConsumer(JdbcConnectionDetails connectionDetails) {
52+
return new LoadTimeWeaverAwareConsumer(connectionDetails);
53+
}
54+
55+
}
56+
57+
static class LoadTimeWeaverAwareConsumer implements LoadTimeWeaverAware {
58+
59+
private final String jdbcUrl;
60+
61+
LoadTimeWeaverAwareConsumer(JdbcConnectionDetails connectionDetails) {
62+
this.jdbcUrl = connectionDetails.getJdbcUrl();
63+
}
64+
65+
@Override
66+
public void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver) {
67+
}
68+
69+
}
70+
71+
}

Diff for: spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class TestcontainersPropertySourceAutoConfigurationTests {
4949
.withConfiguration(AutoConfigurations.of(TestcontainersPropertySourceAutoConfiguration.class));
5050

5151
@Test
52+
@SuppressWarnings("removal")
5253
void containerBeanMethodContributesProperties() {
5354
List<ApplicationEvent> events = new ArrayList<>();
5455
this.contextRunner.withUserConfiguration(ContainerAndPropertiesConfiguration.class)

Diff for: spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java

+1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ void attachToEnvironmentAndContextWhenAlreadyAttachedReturnsExisting() {
134134
}
135135

136136
@Test
137+
@SuppressWarnings("removal")
137138
void getPropertyPublishesEvent() {
138139
try (GenericApplicationContext applicationContext = new GenericApplicationContext()) {
139140
List<ApplicationEvent> events = new ArrayList<>();

Diff for: spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionDetailsFactoryTests.java

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,11 +28,15 @@
2828
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
2929
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
3030
import org.springframework.boot.origin.Origin;
31+
import org.springframework.boot.testcontainers.lifecycle.BeforeTestcontainerUsedEvent;
3132
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactoryTests.TestContainerConnectionDetailsFactory.TestContainerConnectionDetails;
33+
import org.springframework.context.ApplicationContext;
3234
import org.springframework.core.annotation.MergedAnnotation;
3335

3436
import static org.assertj.core.api.Assertions.assertThat;
3537
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
38+
import static org.mockito.ArgumentMatchers.any;
39+
import static org.mockito.BDDMockito.then;
3640
import static org.mockito.Mockito.mock;
3741

3842
/**
@@ -112,11 +116,14 @@ void getContainerWhenNotInitializedThrowsException() {
112116
}
113117

114118
@Test
115-
void getContainerWhenInitializedReturnsSuppliedContainer() throws Exception {
119+
void getContainerWhenInitializedPublishesEventAndReturnsSuppliedContainer() throws Exception {
116120
TestContainerConnectionDetailsFactory factory = new TestContainerConnectionDetailsFactory();
117121
TestContainerConnectionDetails connectionDetails = getConnectionDetails(factory, this.source);
122+
ApplicationContext context = mock(ApplicationContext.class);
123+
connectionDetails.setApplicationContext(context);
118124
connectionDetails.afterPropertiesSet();
119125
assertThat(connectionDetails.callGetContainer()).isSameAs(this.container);
126+
then(context).should().publishEvent(any(BeforeTestcontainerUsedEvent.class));
120127
}
121128

122129
@SuppressWarnings({ "rawtypes", "unchecked" })

Diff for: src/checkstyle/checkstyle-suppressions.xml

+1
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,5 @@
8282
<suppress files="ImportTestcontainersTests\.java" checks="InterfaceIsType" />
8383
<suppress files="MyContainers\.java" checks="InterfaceIsType" />
8484
<suppress files="CertificateMatchingTest\.java" checks="SpringTestFileName" />
85+
<suppress files="LoadTimeWeaverAwareConsumerContainers\.java" checks="InterfaceIsType" />
8586
</suppressions>

0 commit comments

Comments
 (0)