Skip to content

Commit 89874d3

Browse files
committed
Ensure containers are started before binding datasource properties
Update `TestcontainersLifecycleBeanPostProcessor` so that containers are now initialized either on the first `postProcessAfterInitialization` call with a frozen configuration or just before a test container property is supplied. Prior to this commit, it was assumed that the first post-process call after the configuration was frozen was suitably early to initialize the containers. This turns out to not be no always the case. Specifically, in the `finishBeanFactoryInitialization` method of `AbstractApplicationContext` we see that `LoadTimeWeaverAware` beans are obtained before the configuration is frozen. One such bean is `DefaultPersistenceUnitManager` which is likely to need datasource properties that will require a started container. To fix the problem, the `TestcontainersPropertySource` now publishes a `BeforeTestcontainersPropertySuppliedEvent` to the ApplicationContext just before any value is supplied. By listening for this event, we can ensure that containers are initialized and started before any dynamic property is read. Fixes gh-38913
1 parent f59fa2e commit 89874d3

10 files changed

+311
-37
lines changed

Diff for: spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java

+6-4
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.
@@ -20,6 +20,7 @@
2020
import java.lang.reflect.Modifier;
2121
import java.util.Set;
2222

23+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
2324
import org.springframework.boot.testcontainers.properties.TestcontainersPropertySource;
2425
import org.springframework.core.MethodIntrospector;
2526
import org.springframework.core.annotation.MergedAnnotations;
@@ -43,16 +44,17 @@ class DynamicPropertySourceMethodsImporter {
4344
this.environment = environment;
4445
}
4546

46-
void registerDynamicPropertySources(Class<?> definitionClass) {
47+
void registerDynamicPropertySources(BeanDefinitionRegistry beanDefinitionRegistry, Class<?> definitionClass) {
4748
Set<Method> methods = MethodIntrospector.selectMethods(definitionClass, this::isAnnotated);
4849
if (methods.isEmpty()) {
4950
return;
5051
}
51-
DynamicPropertyRegistry registry = TestcontainersPropertySource.attach(this.environment);
52+
DynamicPropertyRegistry dynamicPropertyRegistry = TestcontainersPropertySource.attach(this.environment,
53+
beanDefinitionRegistry);
5254
methods.forEach((method) -> {
5355
assertValid(method);
5456
ReflectionUtils.makeAccessible(method);
55-
ReflectionUtils.invokeMethod(method, null, registry);
57+
ReflectionUtils.invokeMethod(method, null, dynamicPropertyRegistry);
5658
});
5759
}
5860

Diff for: spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainersRegistrar.java

+2-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.
@@ -62,7 +62,7 @@ private void registerBeanDefinitions(BeanDefinitionRegistry registry, Class<?>[]
6262
for (Class<?> definitionClass : definitionClasses) {
6363
this.containerFieldsImporter.registerBeanDefinitions(registry, definitionClass);
6464
if (this.dynamicPropertySourceMethodsImporter != null) {
65-
this.dynamicPropertySourceMethodsImporter.registerDynamicPropertySources(definitionClass);
65+
this.dynamicPropertySourceMethodsImporter.registerDynamicPropertySources(registry, definitionClass);
6666
}
6767
}
6868
}

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

+5-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.
@@ -48,7 +48,10 @@ public void initialize(ConfigurableApplicationContext applicationContext) {
4848
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
4949
applicationContext.addBeanFactoryPostProcessor(new TestcontainersLifecycleBeanFactoryPostProcessor());
5050
TestcontainersStartup startup = TestcontainersStartup.get(applicationContext.getEnvironment());
51-
beanFactory.addBeanPostProcessor(new TestcontainersLifecycleBeanPostProcessor(beanFactory, startup));
51+
TestcontainersLifecycleBeanPostProcessor beanPostProcessor = new TestcontainersLifecycleBeanPostProcessor(
52+
beanFactory, startup);
53+
beanFactory.addBeanPostProcessor(beanPostProcessor);
54+
applicationContext.addApplicationListener(beanPostProcessor);
5255
}
5356

5457
}

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

+22-12
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.
@@ -38,6 +38,8 @@
3838
import org.springframework.beans.factory.config.BeanPostProcessor;
3939
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
4040
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
41+
import org.springframework.boot.testcontainers.properties.BeforeTestcontainersPropertySuppliedEvent;
42+
import org.springframework.context.ApplicationListener;
4143
import org.springframework.core.Ordered;
4244
import org.springframework.core.annotation.Order;
4345
import org.springframework.core.log.LogMessage;
@@ -56,7 +58,8 @@
5658
* @see TestcontainersLifecycleApplicationContextInitializer
5759
*/
5860
@Order(Ordered.LOWEST_PRECEDENCE)
59-
class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPostProcessor {
61+
class TestcontainersLifecycleBeanPostProcessor
62+
implements DestructionAwareBeanPostProcessor, ApplicationListener<BeforeTestcontainersPropertySuppliedEvent> {
6063

6164
private static final Log logger = LogFactory.getLog(TestcontainersLifecycleBeanPostProcessor.class);
6265

@@ -74,9 +77,14 @@ class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPo
7477
this.startup = startup;
7578
}
7679

80+
@Override
81+
public void onApplicationEvent(BeforeTestcontainersPropertySuppliedEvent event) {
82+
initializeContainers();
83+
}
84+
7785
@Override
7886
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
79-
if (this.beanFactory.isConfigurationFrozen() && this.containersInitialized.compareAndSet(false, true)) {
87+
if (this.beanFactory.isConfigurationFrozen()) {
8088
initializeContainers();
8189
}
8290
if (bean instanceof Startable startableBean) {
@@ -121,15 +129,17 @@ private void start(List<Object> beans) {
121129
}
122130

123131
private void initializeContainers() {
124-
logger.trace("Initializing containers");
125-
List<String> beanNames = List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false));
126-
List<Object> beans = getBeans(beanNames);
127-
if (beans != null) {
128-
logger.trace(LogMessage.format("Initialized containers %s", beanNames));
129-
}
130-
else {
131-
logger.trace(LogMessage.format("Failed to initialize containers %s", beanNames));
132-
this.containersInitialized.set(false);
132+
if (this.containersInitialized.compareAndSet(false, true)) {
133+
logger.trace("Initializing containers");
134+
List<String> beanNames = List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false));
135+
List<Object> beans = getBeans(beanNames);
136+
if (beans != null) {
137+
logger.trace(LogMessage.format("Initialized containers %s", beanNames));
138+
}
139+
else {
140+
logger.trace(LogMessage.format("Failed to initialize containers %s", beanNames));
141+
this.containersInitialized.set(false);
142+
}
133143
}
134144
}
135145

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.properties;
18+
19+
import java.util.function.Supplier;
20+
21+
import org.springframework.context.ApplicationEvent;
22+
23+
/**
24+
* Event published just before the {@link Supplier value supplier} of a
25+
* {@link TestcontainersPropertySource} property is called.
26+
*
27+
* @author Phillip Webb
28+
* @since 3.2.2
29+
*/
30+
public class BeforeTestcontainersPropertySuppliedEvent extends ApplicationEvent {
31+
32+
private final String propertyName;
33+
34+
BeforeTestcontainersPropertySuppliedEvent(TestcontainersPropertySource source, String propertyName) {
35+
super(source);
36+
this.propertyName = propertyName;
37+
}
38+
39+
/**
40+
* Return the name of the property about to be supplied.
41+
* @return the propertyName the property name
42+
*/
43+
public String getPropertyName() {
44+
return this.propertyName;
45+
}
46+
47+
}

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

+81-6
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.
@@ -19,10 +19,20 @@
1919
import java.util.Collections;
2020
import java.util.LinkedHashMap;
2121
import java.util.Map;
22+
import java.util.Set;
23+
import java.util.concurrent.CopyOnWriteArraySet;
2224
import java.util.function.Supplier;
2325

2426
import org.testcontainers.containers.Container;
2527

28+
import org.springframework.beans.BeansException;
29+
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
30+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
31+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
32+
import org.springframework.beans.factory.support.RootBeanDefinition;
33+
import org.springframework.context.ApplicationEventPublisher;
34+
import org.springframework.context.ApplicationEventPublisherAware;
35+
import org.springframework.context.ConfigurableApplicationContext;
2636
import org.springframework.core.env.ConfigurableEnvironment;
2737
import org.springframework.core.env.EnumerablePropertySource;
2838
import org.springframework.core.env.Environment;
@@ -44,6 +54,8 @@ public class TestcontainersPropertySource extends EnumerablePropertySource<Map<S
4454

4555
private final DynamicPropertyRegistry registry;
4656

57+
private final Set<ApplicationEventPublisher> eventPublishers = new CopyOnWriteArraySet<>();
58+
4759
TestcontainersPropertySource() {
4860
this(Collections.synchronizedMap(new LinkedHashMap<>()));
4961
}
@@ -57,10 +69,20 @@ private TestcontainersPropertySource(Map<String, Supplier<Object>> valueSupplier
5769
};
5870
}
5971

72+
private void addEventPublisher(ApplicationEventPublisher eventPublisher) {
73+
this.eventPublishers.add(eventPublisher);
74+
}
75+
6076
@Override
6177
public Object getProperty(String name) {
6278
Supplier<Object> valueSupplier = this.source.get(name);
63-
return (valueSupplier != null) ? valueSupplier.get() : null;
79+
return (valueSupplier != null) ? getProperty(name, valueSupplier) : null;
80+
}
81+
82+
private Object getProperty(String name, Supplier<Object> valueSupplier) {
83+
BeforeTestcontainersPropertySuppliedEvent event = new BeforeTestcontainersPropertySuppliedEvent(this, name);
84+
this.eventPublishers.forEach((eventPublisher) -> eventPublisher.publishEvent(event));
85+
return valueSupplier.get();
6486
}
6587

6688
@Override
@@ -74,20 +96,73 @@ public String[] getPropertyNames() {
7496
}
7597

7698
public static DynamicPropertyRegistry attach(Environment environment) {
99+
return attach(environment, null);
100+
}
101+
102+
static DynamicPropertyRegistry attach(ConfigurableApplicationContext applicationContext) {
103+
return attach(applicationContext.getEnvironment(), applicationContext, null);
104+
}
105+
106+
public static DynamicPropertyRegistry attach(Environment environment, BeanDefinitionRegistry registry) {
107+
return attach(environment, null, registry);
108+
}
109+
110+
private static DynamicPropertyRegistry attach(Environment environment, ApplicationEventPublisher eventPublisher,
111+
BeanDefinitionRegistry registry) {
77112
Assert.state(environment instanceof ConfigurableEnvironment,
78113
"TestcontainersPropertySource can only be attached to a ConfigurableEnvironment");
79-
return attach((ConfigurableEnvironment) environment);
114+
TestcontainersPropertySource propertySource = getOrAdd((ConfigurableEnvironment) environment);
115+
if (eventPublisher != null) {
116+
propertySource.addEventPublisher(eventPublisher);
117+
}
118+
else if (registry != null) {
119+
registry.registerBeanDefinition(EventPublisherRegistrar.NAME, new RootBeanDefinition(
120+
EventPublisherRegistrar.class, () -> new EventPublisherRegistrar(environment)));
121+
}
122+
return propertySource.registry;
80123
}
81124

82-
private static DynamicPropertyRegistry attach(ConfigurableEnvironment environment) {
125+
static TestcontainersPropertySource getOrAdd(ConfigurableEnvironment environment) {
83126
PropertySource<?> propertySource = environment.getPropertySources().get(NAME);
84127
if (propertySource == null) {
85128
environment.getPropertySources().addFirst(new TestcontainersPropertySource());
86-
return attach(environment);
129+
return getOrAdd(environment);
87130
}
88131
Assert.state(propertySource instanceof TestcontainersPropertySource,
89132
"Incorrect DynamicValuesPropertySource type registered");
90-
return ((TestcontainersPropertySource) propertySource).registry;
133+
return ((TestcontainersPropertySource) propertySource);
134+
}
135+
136+
/**
137+
* {@link BeanFactoryPostProcessor} to register the {@link ApplicationEventPublisher}
138+
* to the {@link TestcontainersPropertySource}. This class is a
139+
* {@link BeanFactoryPostProcessor} so that it is initialized as early as possible.
140+
*/
141+
private static class EventPublisherRegistrar implements BeanFactoryPostProcessor, ApplicationEventPublisherAware {
142+
143+
static final String NAME = EventPublisherRegistrar.class.getName();
144+
145+
private final Environment environment;
146+
147+
private ApplicationEventPublisher eventPublisher;
148+
149+
EventPublisherRegistrar(Environment environment) {
150+
this.environment = environment;
151+
}
152+
153+
@Override
154+
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
155+
this.eventPublisher = eventPublisher;
156+
}
157+
158+
@Override
159+
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
160+
if (this.eventPublisher != null) {
161+
TestcontainersPropertySource.getOrAdd((ConfigurableEnvironment) this.environment)
162+
.addEventPublisher(this.eventPublisher);
163+
}
164+
}
165+
91166
}
92167

93168
}

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

+9-4
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.
@@ -16,9 +16,12 @@
1616

1717
package org.springframework.boot.testcontainers.properties;
1818

19+
import org.springframework.boot.autoconfigure.AutoConfiguration;
1920
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
21+
import org.springframework.context.ConfigurableApplicationContext;
2022
import org.springframework.context.annotation.Bean;
21-
import org.springframework.core.env.ConfigurableEnvironment;
23+
import org.springframework.core.Ordered;
24+
import org.springframework.core.annotation.Order;
2225
import org.springframework.test.context.DynamicPropertyRegistry;
2326

2427
/**
@@ -28,15 +31,17 @@
2831
* @author Phillip Webb
2932
* @since 3.1.0
3033
*/
34+
@AutoConfiguration
35+
@Order(Ordered.HIGHEST_PRECEDENCE)
3136
@ConditionalOnClass(DynamicPropertyRegistry.class)
3237
public class TestcontainersPropertySourceAutoConfiguration {
3338

3439
TestcontainersPropertySourceAutoConfiguration() {
3540
}
3641

3742
@Bean
38-
DynamicPropertyRegistry dynamicPropertyRegistry(ConfigurableEnvironment environment) {
39-
return TestcontainersPropertySource.attach(environment);
43+
static DynamicPropertyRegistry dynamicPropertyRegistry(ConfigurableApplicationContext applicationContext) {
44+
return TestcontainersPropertySource.attach(applicationContext);
4045
}
4146

4247
}

0 commit comments

Comments
 (0)