From 1652c27b3c60ae13e4ee666a5a18bc2d0d8270e4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 16 Jun 2023 10:27:33 +0100 Subject: [PATCH] Apply additional sources once when using SpringApplication.from() Previously, when using SpringApplication.from() any additional sources configured using with() would be applied to every SpringApplication that was created within the scope of the call to run(). This caused problems with Spring Cloud's bootstrap context where the additional sources would be applied to both the user's application and to the boostrap context's application. This commit updates the hook that's used to apply the additional sources so that it's only applied once. This results in the additional sources only being added to the first SpringApplication that is run. Closes gh-35873 --- .../boot/SpringApplication.java | 22 ++++++- .../boot/SpringApplicationTests.java | 57 ++++++++++++++++++- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 4666648570..fe2d6ac64d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -30,6 +30,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; import org.apache.commons.logging.Log; @@ -1457,10 +1458,10 @@ public class SpringApplication { */ public SpringApplication.Running run(String... args) { RunListener runListener = new RunListener(); - SpringApplicationHook hook = (springApplication) -> { + SpringApplicationHook hook = new SingleUseSpringApplicationHook((springApplication) -> { springApplication.addPrimarySources(this.sources); return runListener; - }; + }); withHook(hook, () -> this.main.accept(args)); return runListener; } @@ -1580,4 +1581,21 @@ public class SpringApplication { } + private static final class SingleUseSpringApplicationHook implements SpringApplicationHook { + + private final AtomicBoolean used = new AtomicBoolean(); + + private final SpringApplicationHook delegate; + + private SingleUseSpringApplicationHook(SpringApplicationHook delegate) { + this.delegate = delegate; + } + + @Override + public SpringApplicationRunListener getRunListener(SpringApplication springApplication) { + return this.used.compareAndSet(false, true) ? this.delegate.getRunListener(springApplication) : null; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index 3b9d87be9b..9523dc903c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -24,6 +24,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -1363,18 +1364,30 @@ class SpringApplicationTests { @Test void fromRunsWithAdditionalSources() { assertThat(ExampleAdditionalConfig.local.get()).isNull(); - SpringApplication.from(ExampleFromMainMethod::main).with(ExampleAdditionalConfig.class).run(); + this.context = SpringApplication.from(ExampleFromMainMethod::main) + .with(ExampleAdditionalConfig.class) + .run() + .getApplicationContext(); assertThat(ExampleAdditionalConfig.local.get()).isNotNull(); ExampleAdditionalConfig.local.set(null); } @Test void fromReturnsApplicationContext() { - ConfigurableApplicationContext context = SpringApplication.from(ExampleFromMainMethod::main) + this.context = SpringApplication.from(ExampleFromMainMethod::main) .with(ExampleAdditionalConfig.class) .run() .getApplicationContext(); - assertThat(context).isNotNull(); + assertThat(this.context).isNotNull(); + } + + @Test + void fromWithMultipleApplicationsOnlyAppliesAdditionalSourcesOnce() { + this.context = SpringApplication.from(MultipleApplicationsMainMethod::main) + .with(SingleUseAdditionalConfig.class) + .run() + .getApplicationContext(); + assertThatNoException().isThrownBy(() -> this.context.getBean(SingleUseAdditionalConfig.class)); } private ArgumentMatcher isAvailabilityChangeEventWithState( @@ -1949,6 +1962,31 @@ class SpringApplicationTests { } + static class MultipleApplicationsMainMethod { + + static void main(String[] args) { + SpringApplication application = new SpringApplication(ExampleConfig.class); + application.setWebApplicationType(WebApplicationType.NONE); + application.addListeners(new ApplicationListener() { + + @Override + public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { + SpringApplicationBuilder builder = new SpringApplicationBuilder( + InnerApplicationConfiguration.class); + builder.web(WebApplicationType.NONE); + builder.run().close(); + } + + }); + application.run(args); + } + + static class InnerApplicationConfiguration { + + } + + } + @Configuration static class ExampleAdditionalConfig { @@ -1960,4 +1998,17 @@ class SpringApplicationTests { } + @Configuration + static class SingleUseAdditionalConfig { + + private static AtomicBoolean used = new AtomicBoolean(false); + + SingleUseAdditionalConfig() { + if (!used.compareAndSet(false, true)) { + throw new IllegalStateException("Single-use configuration has already been used"); + } + } + + } + }