From cc1e1cc92ab28b6c27266068abfe0e1fcbc394bf Mon Sep 17 00:00:00 2001 From: lengors <24527258+lengors@users.noreply.github.com> Date: Sun, 6 Apr 2025 17:03:45 +0100 Subject: [PATCH 1/2] Allow ConnectionDetailsFactories to use context class loader Update `ConnectionDetailsFactories` so that the context classloader can be used to load factories. See gh-45014 Signed-off-by: lengors <24527258+lengors@users.noreply.github.com> --- .../ConnectionDetailsFactories.java | 38 ++++++++++++++++++- .../lifecycle/DockerComposeProperties.java | 15 +++++++- ...ServiceConnectionsApplicationListener.java | 20 +++++----- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java index d348238f511..2bbaaec1195 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java @@ -47,8 +47,26 @@ public class ConnectionDetailsFactories { private final List> registrations = new ArrayList<>(); + /** + * Create a new {@link ConnectionDetailsFactories} instance. This constructor uses the + * class loader of {@link ConnectionDetailsFactory} class to load the factories. + */ public ConnectionDetailsFactories() { - this(SpringFactoriesLoader.forDefaultResourceLocation(ConnectionDetailsFactory.class.getClassLoader())); + this(false); + } + + /** + * Create a new {@link ConnectionDetailsFactories} instance. This constructor takes a + * boolean argument to determine whether the context class loader should be used to + * load the factories. If {@code true} and the context class loader is available it + * will be used otherwise the class loader of {@link ConnectionDetailsFactory} class + * will be used. + * @param useContextClassLoader if {@code true} and the context class loader is + * available it will be used otherwise the class loader of + * {@link ConnectionDetailsFactory} class will be used. + */ + public ConnectionDetailsFactories(boolean useContextClassLoader) { + this(SpringFactoriesLoader.forDefaultResourceLocation(getClassLoader(useContextClassLoader))); } @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -107,6 +125,24 @@ public class ConnectionDetailsFactories { return List.copyOf(result); } + /** + * Return the {@link ClassLoader} to use for loading factories. + *

+ * The default implementation returns the context class loader of the current thread + * or the class loader of this class if the context class loader is {@code null}. + * @param useContextClassLoader if {@code true} and the context class loader is + * available it will be used otherwise the class loader of + * {@link ConnectionDetailsFactory} class will be used + * @return the class loader to use for loading factories + */ + private static ClassLoader getClassLoader(boolean useContextClassLoader) { + if (!useContextClassLoader) { + return ConnectionDetailsFactory.class.getClassLoader(); + } + final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + return (classLoader != null) ? classLoader : getClassLoader(false); + } + /** * A {@link ConnectionDetailsFactory} registration. * diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java index d6e98ec0f0e..3cdf4d793da 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java @@ -46,6 +46,11 @@ public class DockerComposeProperties { */ private boolean enabled = true; + /** + * Whether to try to use the context class loader for connection details factories. + */ + private boolean useContextClassLoader = false; + /** * Arguments to pass to the Docker Compose command. */ @@ -89,10 +94,18 @@ public class DockerComposeProperties { return this.enabled; } + public boolean isUseContextClassLoader() { + return this.useContextClassLoader; + } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public void setUseContextClassLoader(boolean useContextClassLoader) { + this.useContextClassLoader = useContextClassLoader; + } + public List getArguments() { return this.arguments; } @@ -137,7 +150,7 @@ public class DockerComposeProperties { return this.readiness; } - static DockerComposeProperties get(Binder binder) { + public static DockerComposeProperties get(Binder binder) { return binder.bind(NAME, DockerComposeProperties.class).orElseGet(DockerComposeProperties::new); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java index 61acfcf9905..ba1390d96f5 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java @@ -27,7 +27,9 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.autoconfigure.container.ContainerImageMetadata; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties; import org.springframework.boot.docker.compose.lifecycle.DockerComposeServicesReadyEvent; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; @@ -46,32 +48,30 @@ import org.springframework.util.StringUtils; class DockerComposeServiceConnectionsApplicationListener implements ApplicationListener { - private final ConnectionDetailsFactories factories; - DockerComposeServiceConnectionsApplicationListener() { - this(new ConnectionDetailsFactories()); - } - DockerComposeServiceConnectionsApplicationListener(ConnectionDetailsFactories factories) { - this.factories = factories; } @Override public void onApplicationEvent(DockerComposeServicesReadyEvent event) { ApplicationContext applicationContext = event.getSource(); if (applicationContext instanceof BeanDefinitionRegistry registry) { + Binder binder = Binder.get(applicationContext.getEnvironment()); + DockerComposeProperties properties = DockerComposeProperties.get(binder); + boolean useContextClassLoader = properties.isUseContextClassLoader(); + ConnectionDetailsFactories factories = new ConnectionDetailsFactories(useContextClassLoader); Environment environment = applicationContext.getEnvironment(); - registerConnectionDetails(registry, environment, event.getRunningServices()); + registerConnectionDetails(registry, environment, event.getRunningServices(), factories); } } private void registerConnectionDetails(BeanDefinitionRegistry registry, Environment environment, - List runningServices) { + List runningServices, ConnectionDetailsFactories factories) { for (RunningService runningService : runningServices) { DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService, environment); - this.factories.getConnectionDetails(source, false).forEach((connectionDetailsType, connectionDetails) -> { + factories.getConnectionDetails(source, false).forEach((connectionDetailsType, connectionDetails) -> { register(registry, runningService, connectionDetailsType, connectionDetails); - this.factories.getConnectionDetails(connectionDetails, false) + factories.getConnectionDetails(connectionDetails, false) .forEach((adaptedType, adaptedDetails) -> register(registry, runningService, adaptedType, adaptedDetails)); }); From d15078d35f7cbf753d9d16decb87ce5d0db132f7 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 8 Apr 2025 13:03:03 -0700 Subject: [PATCH 2/2] Polish 'Allow ConnectionDetailsFactories to use context class loader' Refine the submitted pull-request to remove the configuration property with the assumption that the context classloader will work for all cases. See gh-45014 --- .../ConnectionDetailsFactories.java | 44 +++++-------------- .../lifecycle/DockerComposeProperties.java | 15 +------ ...ServiceConnectionsApplicationListener.java | 20 ++++----- ...ontainerConnectionDetailsFactoryTests.java | 6 +-- ...eConnectionAutoConfigurationRegistrar.java | 2 +- .../ServiceConnectionContextCustomizer.java | 2 +- 6 files changed, 28 insertions(+), 61 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java index 2bbaaec1195..5ec7c2deff3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -39,6 +39,7 @@ import org.springframework.util.Assert; * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Pedro Xavier Leite Cavadas * @since 3.1.0 */ public class ConnectionDetailsFactories { @@ -48,25 +49,22 @@ public class ConnectionDetailsFactories { private final List> registrations = new ArrayList<>(); /** - * Create a new {@link ConnectionDetailsFactories} instance. This constructor uses the - * class loader of {@link ConnectionDetailsFactory} class to load the factories. + * Create a new {@link ConnectionDetailsFactories} instance. + * @deprecated since 3.5.0 for removal in 4.0.0 in favor of + * {@link #ConnectionDetailsFactories(ClassLoader)} */ + @Deprecated(since = "3.5.0", forRemoval = true) public ConnectionDetailsFactories() { - this(false); + this((ClassLoader) null); } /** - * Create a new {@link ConnectionDetailsFactories} instance. This constructor takes a - * boolean argument to determine whether the context class loader should be used to - * load the factories. If {@code true} and the context class loader is available it - * will be used otherwise the class loader of {@link ConnectionDetailsFactory} class - * will be used. - * @param useContextClassLoader if {@code true} and the context class loader is - * available it will be used otherwise the class loader of - * {@link ConnectionDetailsFactory} class will be used. + * Create a new {@link ConnectionDetailsFactories} instance. + * @param classLoader the class loader used to load factories + * @since 3.5.0 */ - public ConnectionDetailsFactories(boolean useContextClassLoader) { - this(SpringFactoriesLoader.forDefaultResourceLocation(getClassLoader(useContextClassLoader))); + public ConnectionDetailsFactories(ClassLoader classLoader) { + this(SpringFactoriesLoader.forDefaultResourceLocation(classLoader)); } @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -125,24 +123,6 @@ public class ConnectionDetailsFactories { return List.copyOf(result); } - /** - * Return the {@link ClassLoader} to use for loading factories. - *

- * The default implementation returns the context class loader of the current thread - * or the class loader of this class if the context class loader is {@code null}. - * @param useContextClassLoader if {@code true} and the context class loader is - * available it will be used otherwise the class loader of - * {@link ConnectionDetailsFactory} class will be used - * @return the class loader to use for loading factories - */ - private static ClassLoader getClassLoader(boolean useContextClassLoader) { - if (!useContextClassLoader) { - return ConnectionDetailsFactory.class.getClassLoader(); - } - final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - return (classLoader != null) ? classLoader : getClassLoader(false); - } - /** * A {@link ConnectionDetailsFactory} registration. * diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java index 3cdf4d793da..e0b816fb004 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -46,11 +46,6 @@ public class DockerComposeProperties { */ private boolean enabled = true; - /** - * Whether to try to use the context class loader for connection details factories. - */ - private boolean useContextClassLoader = false; - /** * Arguments to pass to the Docker Compose command. */ @@ -94,18 +89,10 @@ public class DockerComposeProperties { return this.enabled; } - public boolean isUseContextClassLoader() { - return this.useContextClassLoader; - } - public void setEnabled(boolean enabled) { this.enabled = enabled; } - public void setUseContextClassLoader(boolean useContextClassLoader) { - this.useContextClassLoader = useContextClassLoader; - } - public List getArguments() { return this.arguments; } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java index ba1390d96f5..cb5393aa5fd 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java @@ -27,9 +27,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.autoconfigure.container.ContainerImageMetadata; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories; -import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.docker.compose.core.RunningService; -import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties; import org.springframework.boot.docker.compose.lifecycle.DockerComposeServicesReadyEvent; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; @@ -48,30 +46,32 @@ import org.springframework.util.StringUtils; class DockerComposeServiceConnectionsApplicationListener implements ApplicationListener { + private final ConnectionDetailsFactories factories; + DockerComposeServiceConnectionsApplicationListener() { + this(new ConnectionDetailsFactories(null)); + } + DockerComposeServiceConnectionsApplicationListener(ConnectionDetailsFactories factories) { + this.factories = factories; } @Override public void onApplicationEvent(DockerComposeServicesReadyEvent event) { ApplicationContext applicationContext = event.getSource(); if (applicationContext instanceof BeanDefinitionRegistry registry) { - Binder binder = Binder.get(applicationContext.getEnvironment()); - DockerComposeProperties properties = DockerComposeProperties.get(binder); - boolean useContextClassLoader = properties.isUseContextClassLoader(); - ConnectionDetailsFactories factories = new ConnectionDetailsFactories(useContextClassLoader); Environment environment = applicationContext.getEnvironment(); - registerConnectionDetails(registry, environment, event.getRunningServices(), factories); + registerConnectionDetails(registry, environment, event.getRunningServices()); } } private void registerConnectionDetails(BeanDefinitionRegistry registry, Environment environment, - List runningServices, ConnectionDetailsFactories factories) { + List runningServices) { for (RunningService runningService : runningServices) { DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService, environment); - factories.getConnectionDetails(source, false).forEach((connectionDetailsType, connectionDetails) -> { + this.factories.getConnectionDetails(source, false).forEach((connectionDetailsType, connectionDetails) -> { register(registry, runningService, connectionDetailsType, connectionDetails); - factories.getConnectionDetails(connectionDetails, false) + this.factories.getConnectionDetails(connectionDetails, false) .forEach((adaptedType, adaptedDetails) -> register(registry, runningService, adaptedType, adaptedDetails)); }); diff --git a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/service/connection/redis/CustomRedisContainerConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/service/connection/redis/CustomRedisContainerConnectionDetailsFactoryTests.java index 5d8dc04158f..9ce215bb597 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/service/connection/redis/CustomRedisContainerConnectionDetailsFactoryTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/dockerTest/java/org/springframework/boot/testcontainers/service/connection/redis/CustomRedisContainerConnectionDetailsFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -42,7 +42,7 @@ class CustomRedisContainerConnectionDetailsFactoryTests { @Test void getConnectionDetailsWhenRedisContainerWithCustomName() { - ConnectionDetailsFactories factories = new ConnectionDetailsFactories(); + ConnectionDetailsFactories factories = new ConnectionDetailsFactories(null); MergedAnnotation annotation = MergedAnnotation.of(ServiceConnection.class, Map.of("value", "")); ContainerConnectionSource source = TestContainerConnectionSource.create("test", null, @@ -53,7 +53,7 @@ class CustomRedisContainerConnectionDetailsFactoryTests { @Test void getConnectionDetailsWhenRedisStackContainerWithCustomName() { - ConnectionDetailsFactories factories = new ConnectionDetailsFactories(); + ConnectionDetailsFactories factories = new ConnectionDetailsFactories(null); MergedAnnotation annotation = MergedAnnotation.of(ServiceConnection.class, Map.of("value", "")); ContainerConnectionSource source = TestContainerConnectionSource.create("test", null, diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfigurationRegistrar.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfigurationRegistrar.java index 144d2fa6776..6eed85656a2 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfigurationRegistrar.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfigurationRegistrar.java @@ -57,7 +57,7 @@ class ServiceConnectionAutoConfigurationRegistrar implements ImportBeanDefinitio private void registerBeanDefinitions(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry) { ConnectionDetailsRegistrar registrar = new ConnectionDetailsRegistrar(beanFactory, - new ConnectionDetailsFactories()); + new ConnectionDetailsFactories(null)); for (String beanName : beanFactory.getBeanNamesForType(Container.class)) { BeanDefinition beanDefinition = getBeanDefinition(beanFactory, beanName); MergedAnnotations annotations = (beanDefinition instanceof TestcontainerBeanDefinition testcontainerBeanDefinition) diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizer.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizer.java index e5ade0d084e..42bfccfb88c 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizer.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizer.java @@ -47,7 +47,7 @@ class ServiceConnectionContextCustomizer implements ContextCustomizer { private final ConnectionDetailsFactories connectionDetailsFactories; ServiceConnectionContextCustomizer(List> sources) { - this(sources, new ConnectionDetailsFactories()); + this(sources, new ConnectionDetailsFactories(null)); } ServiceConnectionContextCustomizer(List> sources,