diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultPropertiesPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultPropertiesPropertySource.java index b5be872bf43..36affd9eab2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultPropertiesPropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultPropertiesPropertySource.java @@ -16,6 +16,7 @@ package org.springframework.boot; +import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; @@ -72,6 +73,40 @@ public class DefaultPropertiesPropertySource extends MapPropertySource { } } + /** + * Add a new {@link DefaultPropertiesPropertySource} or merge with an existing one. + * @param source the {@code Map} source + * @param sources the existing sources + * @since 2.4.4 + */ + public static void addOrMerge(Map source, MutablePropertySources sources) { + if (!CollectionUtils.isEmpty(source)) { + Map resultingSource = new HashMap<>(); + DefaultPropertiesPropertySource propertySource = new DefaultPropertiesPropertySource(resultingSource); + if (sources.contains(NAME)) { + mergeIfPossible(source, sources, resultingSource); + sources.replace(NAME, propertySource); + } + else { + resultingSource.putAll(source); + sources.addLast(propertySource); + } + } + } + + @SuppressWarnings("unchecked") + private static void mergeIfPossible(Map source, MutablePropertySources sources, + Map resultingSource) { + PropertySource existingSource = sources.get(NAME); + if (existingSource != null) { + Object underlyingSource = existingSource.getSource(); + if (underlyingSource instanceof Map) { + resultingSource.putAll((Map) underlyingSource); + } + resultingSource.putAll(source); + } + } + /** * Move the 'defaultProperties' property source so that it's the last source in the * given {@link ConfigurableEnvironment}. 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 9b2b0c9b7c1..65f33e71f6d 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 @@ -518,7 +518,9 @@ public class SpringApplication { */ protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) { MutablePropertySources sources = environment.getPropertySources(); - DefaultPropertiesPropertySource.ifNotEmpty(this.defaultProperties, sources::addLast); + if (!CollectionUtils.isEmpty(this.defaultProperties)) { + DefaultPropertiesPropertySource.addOrMerge(this.defaultProperties, sources); + } if (this.addCommandLineProperties && args.length > 0) { String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME; if (sources.contains(name)) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/DefaultPropertiesPropertySourceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/DefaultPropertiesPropertySourceTests.java index ad43f3e5c9d..e375847d81e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/DefaultPropertiesPropertySourceTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/DefaultPropertiesPropertySourceTests.java @@ -26,6 +26,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.mock.env.MockEnvironment; @@ -39,6 +40,7 @@ import static org.mockito.Mockito.verifyNoInteractions; * Tests for {@link DefaultPropertiesPropertySource}. * * @author Phillip Webb + * @author Madhura Bhave */ @ExtendWith(MockitoExtension.class) class DefaultPropertiesPropertySourceTests { @@ -104,6 +106,39 @@ class DefaultPropertiesPropertySourceTests { DefaultPropertiesPropertySource.moveToEnd(environment); } + @Test + void addOrMergeWhenExistingNotFoundShouldAdd() { + MockEnvironment environment = new MockEnvironment(); + MutablePropertySources propertySources = environment.getPropertySources(); + DefaultPropertiesPropertySource.addOrMerge(Collections.singletonMap("spring", "boot"), propertySources); + assertThat(propertySources.contains(DefaultPropertiesPropertySource.NAME)).isTrue(); + assertThat(propertySources.get(DefaultPropertiesPropertySource.NAME).getProperty("spring")).isEqualTo("boot"); + } + + @Test + void addOrMergeWhenExistingFoundShouldMerge() { + MockEnvironment environment = new MockEnvironment(); + MutablePropertySources propertySources = environment.getPropertySources(); + propertySources.addLast(new DefaultPropertiesPropertySource(Collections.singletonMap("spring", "boot"))); + DefaultPropertiesPropertySource.addOrMerge(Collections.singletonMap("hello", "world"), propertySources); + assertThat(propertySources.contains(DefaultPropertiesPropertySource.NAME)).isTrue(); + assertThat(propertySources.get(DefaultPropertiesPropertySource.NAME).getProperty("spring")).isEqualTo("boot"); + assertThat(propertySources.get(DefaultPropertiesPropertySource.NAME).getProperty("hello")).isEqualTo("world"); + } + + @Test + void addOrMergeWhenExistingNotMapPropertySourceShouldNotMerge() { + MockEnvironment environment = new MockEnvironment(); + MutablePropertySources propertySources = environment.getPropertySources(); + CompositePropertySource composite = new CompositePropertySource(DefaultPropertiesPropertySource.NAME); + composite.addPropertySource(new DefaultPropertiesPropertySource(Collections.singletonMap("spring", "boot"))); + propertySources.addFirst(composite); + DefaultPropertiesPropertySource.addOrMerge(Collections.singletonMap("hello", "world"), propertySources); + assertThat(propertySources.contains(DefaultPropertiesPropertySource.NAME)).isTrue(); + assertThat(propertySources.get(DefaultPropertiesPropertySource.NAME).getProperty("spring")).isNull(); + assertThat(propertySources.get(DefaultPropertiesPropertySource.NAME).getProperty("hello")).isEqualTo("world"); + } + @Test void moveToEndWhenPresentMovesToEnd() { MockEnvironment environment = new MockEnvironment(); 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 1b04c70fdcf..b818d3bcb53 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 @@ -53,6 +53,7 @@ import org.springframework.boot.availability.AvailabilityChangeEvent; import org.springframework.boot.availability.AvailabilityState; import org.springframework.boot.availability.LivenessState; import org.springframework.boot.availability.ReadinessState; +import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.event.ApplicationContextInitializedEvent; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationFailedEvent; @@ -105,6 +106,7 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.core.metrics.ApplicationStartup; import org.springframework.core.metrics.StartupStep; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.mock.env.MockEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -887,6 +889,18 @@ class SpringApplicationTests { assertThat(getEnvironment().getProperty("baz")).isEqualTo(""); } + @Test + void defaultPropertiesShouldBeMerged() { + MockEnvironment environment = new MockEnvironment(); + environment.getPropertySources().addFirst( + new MapPropertySource(DefaultPropertiesPropertySource.NAME, Collections.singletonMap("bar", "foo"))); + SpringApplication application = new SpringApplicationBuilder(ExampleConfig.class).environment(environment) + .properties("baz=bing").web(WebApplicationType.NONE).build(); + this.context = application.run(); + assertThat(getEnvironment().getProperty("bar")).isEqualTo("foo"); + assertThat(getEnvironment().getProperty("baz")).isEqualTo("bing"); + } + @Test void commandLineArgsApplyToSpringApplication() { TestSpringApplication application = new TestSpringApplication(ExampleConfig.class);