Don't call close methods for reusable testcontainers

Refine `TestcontainersLifecycleApplicationContextInitializer` so that
the `close()` method is not called for reusable containers.

Closes gh-35210
This commit is contained in:
Phillip Webb
2023-04-30 06:07:05 -07:00
parent e7357ba805
commit c13041201d
4 changed files with 165 additions and 17 deletions

View File

@@ -18,6 +18,7 @@ package org.springframework.boot.testcontainers.lifecycle;
import org.testcontainers.lifecycle.Startable;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
@@ -33,7 +34,9 @@ public class TestcontainersLifecycleApplicationContextInitializer
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.getBeanFactory().addBeanPostProcessor(new TestcontainersLifecycleBeanPostProcessor());
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
applicationContext.addBeanFactoryPostProcessor(new TestcontainersLifecycleBeanFactoryPostProcessor());
beanFactory.addBeanPostProcessor(new TestcontainersLifecycleBeanPostProcessor(beanFactory));
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2023 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.lifecycle.Startable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
/**
* {@link BeanFactoryPostProcessor} to prevent {@link AutoCloseable} destruction calls so
* that {@link TestcontainersLifecycleBeanFactoryPostProcessor} can be smarter about which
* containers to close.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @see TestcontainersLifecycleApplicationContextInitializer
*/
@Order(Ordered.LOWEST_PRECEDENCE)
class TestcontainersLifecycleBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
for (String beanName : beanFactory.getBeanNamesForType(Startable.class, false, false)) {
try {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
String destroyMethodName = beanDefinition.getDestroyMethodName();
if (destroyMethodName == null || AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName)) {
beanDefinition.setDestroyMethodName("");
}
}
catch (NoSuchBeanDefinitionException ex) {
}
}
}
}

View File

@@ -16,10 +16,17 @@
package org.springframework.boot.testcontainers.lifecycle;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.lifecycle.Startable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
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.core.Ordered;
import org.springframework.core.annotation.Order;
/**
* {@link BeanPostProcessor} to manage the lifecycle of {@link Startable startable
@@ -29,7 +36,14 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
* @author Stephane Nicoll
* @see TestcontainersLifecycleApplicationContextInitializer
*/
class TestcontainersLifecycleBeanPostProcessor implements BeanPostProcessor {
@Order(Ordered.LOWEST_PRECEDENCE)
class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPostProcessor {
private final ConfigurableListableBeanFactory beanFactory;
TestcontainersLifecycleBeanPostProcessor(ConfigurableListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
@@ -39,4 +53,31 @@ class TestcontainersLifecycleBeanPostProcessor implements BeanPostProcessor {
return bean;
}
@Override
public boolean requiresDestruction(Object bean) {
return bean instanceof Startable;
}
@Override
public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
if (bean instanceof Startable startable && !isDestroyedByFramework(beanName) && !isReusedContainer(bean)) {
startable.close();
}
}
private boolean isDestroyedByFramework(String beanName) {
try {
BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition(beanName);
String destroyMethodName = beanDefinition.getDestroyMethodName();
return !"".equals(destroyMethodName);
}
catch (NoSuchBeanDefinitionException ex) {
return false;
}
}
private boolean isReusedContainer(Object bean) {
return (bean instanceof GenericContainer<?> container) && container.isShouldBeReused();
}
}