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 c6e9b8cd3b..63046533e6 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 @@ -47,6 +47,7 @@ import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.convert.ApplicationConversionService; +import org.springframework.boot.env.DefaultPropertiesPropertySource; import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext; import org.springframework.boot.web.reactive.context.StandardReactiveWebEnvironment; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; @@ -68,7 +69,6 @@ import org.springframework.core.env.CommandLinePropertySource; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; -import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.env.SimpleCommandLinePropertySource; @@ -351,6 +351,7 @@ public class SpringApplication { configureEnvironment(environment, applicationArguments.getSourceArgs()); ConfigurationPropertySources.attach(environment); listeners.environmentPrepared(environment); + DefaultPropertiesPropertySource.moveToEnd(environment); bindToSpringApplication(environment); if (!this.isCustomEnvironment) { environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, @@ -499,9 +500,7 @@ public class SpringApplication { */ protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) { MutablePropertySources sources = environment.getPropertySources(); - if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) { - sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties)); - } + DefaultPropertiesPropertySource.ifNotEmpty(this.defaultProperties, sources::addLast); 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/main/java/org/springframework/boot/env/DefaultPropertiesPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/DefaultPropertiesPropertySource.java new file mode 100644 index 0000000000..3dba8c2f55 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/DefaultPropertiesPropertySource.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2020 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.env; + +import java.util.Map; +import java.util.function.Consumer; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.util.CollectionUtils; + +/** + * {@link MapPropertySource} containing default properties contributed directly to a + * {@code SpringApplication}. By convention, the {@link DefaultPropertiesPropertySource} + * is always the last property source in the {@link Environment}. + * + * @author Phillip Webb + * @since 2.4.0 + */ +public class DefaultPropertiesPropertySource extends MapPropertySource { + + /** + * The name of the 'default properties' property source. + */ + public static final String NAME = "defaultProperties"; + + /** + * Create a new {@link DefaultPropertiesPropertySource} with the given {@code Map} + * source. + * @param source the source map + */ + public DefaultPropertiesPropertySource(Map source) { + super(NAME, source); + } + + /** + * Return {@code true} if the given source is named 'defaultProperties'. + * @param propertySource the property source to check + * @return {@code true} if the name matches + */ + public static boolean hasMatchingName(PropertySource propertySource) { + return (propertySource != null) && propertySource.getName().equals(NAME); + } + + /** + * Create a consume a new {@link DefaultPropertiesPropertySource} instance if the + * provided source is not empty. + * @param source the {@code Map} source + * @param action the action used to consume the + * {@link DefaultPropertiesPropertySource} + */ + public static void ifNotEmpty(Map source, Consumer action) { + if (!CollectionUtils.isEmpty(source) && action != null) { + action.accept(new DefaultPropertiesPropertySource(source)); + } + } + + /** + * Move the 'defaultProperties' property source so that it's the last source in the + * given {@link ConfigurableEnvironment}. + * @param environment the environment to update + */ + public static void moveToEnd(ConfigurableEnvironment environment) { + moveToEnd(environment.getPropertySources()); + } + + /** + * Move the 'defaultProperties' property source so that it's the last source in the + * given {@link MutablePropertySources}. + * @param propertySources the property sources to update + */ + public static void moveToEnd(MutablePropertySources propertySources) { + PropertySource propertySource = propertySources.remove(NAME); + if (propertySource != null) { + propertySources.addLast(propertySource); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/DefaultPropertiesPropertySourceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/DefaultPropertiesPropertySourceTests.java new file mode 100644 index 0000000000..b3d9ac8d29 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/DefaultPropertiesPropertySourceTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2020 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.env; + +import java.util.Collections; +import java.util.function.Consumer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.mock.env.MockPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link DefaultPropertiesPropertySource}. + * + * @author Phillip Webb + */ +class DefaultPropertiesPropertySourceTests { + + @Mock + private Consumer action; + + @Captor + private ArgumentCaptor captor; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + void nameIsDefaultProperties() { + assertThat(DefaultPropertiesPropertySource.NAME).isEqualTo("defaultProperties"); + } + + @Test + void createCreatesSource() { + DefaultPropertiesPropertySource propertySource = new DefaultPropertiesPropertySource( + Collections.singletonMap("spring", "boot")); + assertThat(propertySource.getName()).isEqualTo("defaultProperties"); + assertThat(propertySource.getProperty("spring")).isEqualTo("boot"); + } + + @Test + void hasMatchingNameWhenNameMatchesReturnsTrue() { + MockPropertySource propertySource = new MockPropertySource("defaultProperties"); + assertThat(DefaultPropertiesPropertySource.hasMatchingName(propertySource)).isTrue(); + } + + @Test + void hasMatchingNameWhenNameDoesNotMatchReturnsFalse() { + MockPropertySource propertySource = new MockPropertySource("normalProperties"); + assertThat(DefaultPropertiesPropertySource.hasMatchingName(propertySource)).isFalse(); + } + + @Test + void hasMatchingNameWhenPropertySourceIsNullReturnsFalse() { + assertThat(DefaultPropertiesPropertySource.hasMatchingName(null)).isFalse(); + } + + @Test + void ifNotEmptyWhenNullDoesNotCallAction() { + DefaultPropertiesPropertySource.ifNotEmpty(null, this.action); + verifyNoInteractions(this.action); + } + + @Test + void ifNotEmptyWhenEmptyDoesNotCallAction() { + DefaultPropertiesPropertySource.ifNotEmpty(Collections.emptyMap(), this.action); + verifyNoInteractions(this.action); + } + + @Test + void ifNotEmptyHasValueCallsAction() { + DefaultPropertiesPropertySource.ifNotEmpty(Collections.singletonMap("spring", "boot"), this.action); + verify(this.action).accept(this.captor.capture()); + assertThat(this.captor.getValue().getProperty("spring")).isEqualTo("boot"); + } + + @Test + void moveToEndWhenNotPresentDoesNothing() { + MockEnvironment environment = new MockEnvironment(); + DefaultPropertiesPropertySource.moveToEnd(environment); + } + + @Test + void moveToEndWhenPresentMovesToEnd() { + MockEnvironment environment = new MockEnvironment(); + MutablePropertySources propertySources = environment.getPropertySources(); + propertySources.addLast(new DefaultPropertiesPropertySource(Collections.singletonMap("spring", "boot"))); + propertySources.addLast(new MockPropertySource("test")); + DefaultPropertiesPropertySource.moveToEnd(environment); + String[] names = propertySources.stream().map(PropertySource::getName).toArray(String[]::new); + assertThat(names).containsExactly(MockPropertySource.MOCK_PROPERTIES_PROPERTY_SOURCE_NAME, "test", + DefaultPropertiesPropertySource.NAME); + } + +}