diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationEnvironment.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationEnvironment.java index 3cec5cab59de..c948a8d4925a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationEnvironment.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationEnvironment.java @@ -16,6 +16,9 @@ package org.springframework.boot; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.core.env.ConfigurablePropertyResolver; +import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.StandardEnvironment; /** @@ -35,4 +38,9 @@ protected String doGetDefaultProfilesProperty() { return null; } + @Override + protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) { + return ConfigurationPropertySources.createPropertyResolver(propertySources); + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationReactiveWebEnvironment.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationReactiveWebEnvironment.java index 64d141fc8a21..6f0324e65f0a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationReactiveWebEnvironment.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationReactiveWebEnvironment.java @@ -16,7 +16,10 @@ package org.springframework.boot; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.web.reactive.context.StandardReactiveWebEnvironment; +import org.springframework.core.env.ConfigurablePropertyResolver; +import org.springframework.core.env.MutablePropertySources; /** * {@link StandardReactiveWebEnvironment} for typical use in a typical @@ -36,4 +39,9 @@ protected String doGetDefaultProfilesProperty() { return null; } + @Override + protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) { + return ConfigurationPropertySources.createPropertyResolver(propertySources); + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationServletEnvironment.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationServletEnvironment.java index 7efa2267cc2b..22c3eedd319e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationServletEnvironment.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationServletEnvironment.java @@ -16,6 +16,9 @@ package org.springframework.boot; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.core.env.ConfigurablePropertyResolver; +import org.springframework.core.env.MutablePropertySources; import org.springframework.web.context.support.StandardServletEnvironment; /** @@ -36,4 +39,9 @@ protected String doGetDefaultProfilesProperty() { return null; } + @Override + protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) { + return ConfigurationPropertySources.createPropertyResolver(propertySources); + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySources.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySources.java index 57ba2e84598e..97d0a2b65b25 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySources.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySources.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -20,8 +20,10 @@ import java.util.stream.Stream; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.ConfigurablePropertyResolver; import org.springframework.core.env.Environment; import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertyResolver; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource.StubPropertySource; import org.springframework.core.env.PropertySources; @@ -44,6 +46,19 @@ public final class ConfigurationPropertySources { private ConfigurationPropertySources() { } + /** + * Create a new {@link PropertyResolver} that resolves property values against an + * underlying set of {@link PropertySources}. Provides an + * {@link ConfigurationPropertySource} aware and optimized alternative to + * {@link PropertySourcesPropertyResolver}. + * @param propertySources the set of {@link PropertySource} objects to use + * @return a {@link ConfigurablePropertyResolver} implementation + * @since 2.5.0 + */ + public static ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) { + return new ConfigurationPropertySourcesPropertyResolver(propertySources); + } + /** * Determines if the specific {@link PropertySource} is the * {@link ConfigurationPropertySource} that was {@link #attach(Environment) attached} @@ -71,7 +86,7 @@ public static boolean isAttachedConfigurationPropertySource(PropertySource pr public static void attach(Environment environment) { Assert.isInstanceOf(ConfigurableEnvironment.class, environment); MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources(); - PropertySource attached = sources.get(ATTACHED_PROPERTY_SOURCE_NAME); + PropertySource attached = getAttached(sources); if (attached != null && attached.getSource() != sources) { sources.remove(ATTACHED_PROPERTY_SOURCE_NAME); attached = null; @@ -82,6 +97,10 @@ public static void attach(Environment environment) { } } + static PropertySource getAttached(MutablePropertySources sources) { + return (sources != null) ? sources.get(ATTACHED_PROPERTY_SOURCE_NAME) : null; + } + /** * Return a set of {@link ConfigurationPropertySource} instances that have previously * been {@link #attach(Environment) attached} to the {@link Environment}. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertyResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertyResolver.java new file mode 100644 index 000000000000..1815da7535dd --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertyResolver.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2021 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.context.properties.source; + +import org.springframework.core.env.AbstractPropertyResolver; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySources; +import org.springframework.core.env.PropertySourcesPropertyResolver; + +/** + * Alternative {@link PropertySourcesPropertyResolver} implementation that recognizes + * {@link ConfigurationPropertySourcesPropertySource} and saves duplicate calls to the + * underlying sources if the name is a value {@link ConfigurationPropertyName}. + * + * @author Phillip Webb + */ +class ConfigurationPropertySourcesPropertyResolver extends AbstractPropertyResolver { + + private final MutablePropertySources propertySources; + + private final DefaultResolver defaultResolver; + + ConfigurationPropertySourcesPropertyResolver(MutablePropertySources propertySources) { + this.propertySources = propertySources; + this.defaultResolver = new DefaultResolver(propertySources); + } + + @Override + public boolean containsProperty(String key) { + ConfigurationPropertySourcesPropertySource attached = getAttached(); + if (attached != null) { + ConfigurationPropertyName name = ConfigurationPropertyName.of(key, true); + if (name != null) { + try { + return attached.findConfigurationProperty(name) != null; + } + catch (Exception ex) { + } + } + } + return this.defaultResolver.containsProperty(key); + } + + @Override + public String getProperty(String key) { + return getProperty(key, String.class, true); + } + + @Override + public T getProperty(String key, Class targetValueType) { + return getProperty(key, targetValueType, true); + } + + @Override + protected String getPropertyAsRawString(String key) { + return getProperty(key, String.class, false); + } + + private T getProperty(String key, Class targetValueType, boolean resolveNestedPlaceholders) { + Object value = findPropertyValue(key, targetValueType); + if (value == null) { + return null; + } + if (resolveNestedPlaceholders && value instanceof String) { + value = resolveNestedPlaceholders((String) value); + } + return convertValueIfNecessary(value, targetValueType); + } + + private Object findPropertyValue(String key, Class targetValueType) { + ConfigurationPropertySourcesPropertySource attached = getAttached(); + if (attached != null) { + ConfigurationPropertyName name = ConfigurationPropertyName.of(key, true); + if (name != null) { + try { + ConfigurationProperty configurationProperty = attached.findConfigurationProperty(name); + return (configurationProperty != null) ? configurationProperty.getValue() : null; + } + catch (Exception ex) { + } + } + } + return this.defaultResolver.getProperty(key, targetValueType, false); + } + + private ConfigurationPropertySourcesPropertySource getAttached() { + ConfigurationPropertySourcesPropertySource attached = (ConfigurationPropertySourcesPropertySource) ConfigurationPropertySources + .getAttached(this.propertySources); + Iterable attachedSource = (attached != null) ? attached.getSource() : null; + if ((attachedSource instanceof SpringConfigurationPropertySources) + && ((SpringConfigurationPropertySources) attachedSource).isUsingSources(this.propertySources)) { + return attached; + } + return null; + } + + /** + * Default {@link PropertySourcesPropertyResolver} used if + * {@link ConfigurationPropertySources} is not attached. + */ + static class DefaultResolver extends PropertySourcesPropertyResolver { + + DefaultResolver(PropertySources propertySources) { + super(propertySources); + } + + @Override + public T getProperty(String key, Class targetValueType, boolean resolveNestedPlaceholders) { + return super.getProperty(key, targetValueType, resolveNestedPlaceholders); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySource.java index 128594aaa597..dc09d6ca1ff3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 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. @@ -37,6 +37,11 @@ class ConfigurationPropertySourcesPropertySource extends PropertySource> sources) { + return this.sources == sources; + } + @Override public Iterator iterator() { return new SourcesIterator(this.sources.iterator(), this::adapt); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/AbstractApplicationEnvironmentTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/AbstractApplicationEnvironmentTests.java index 5f5f789179a2..14bc3dedf4d8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/AbstractApplicationEnvironmentTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/AbstractApplicationEnvironmentTests.java @@ -18,8 +18,11 @@ import org.junit.jupiter.api.Test; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.ConfigurablePropertyResolver; import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.StandardEnvironment; import org.springframework.mock.env.MockPropertySource; @@ -50,6 +53,14 @@ void getDefaultProfilesDoesNotResolveProperty() { assertThat(environment.getDefaultProfiles()).containsExactly("default"); } + @Test + void propertyResolverIsOptimizedForConfigurationProperties() { + StandardEnvironment environment = createEnvironment(); + ConfigurablePropertyResolver expected = ConfigurationPropertySources + .createPropertyResolver(new MutablePropertySources()); + assertThat(environment).extracting("propertyResolver").hasSameClassAs(expected); + } + protected abstract StandardEnvironment createEnvironment(); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertyResolverTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertyResolverTests.java new file mode 100644 index 000000000000..2447ae9c2926 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesPropertyResolverTests.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012-2021 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.context.properties.source; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.env.ConfigurablePropertyResolver; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.mock.env.MockPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertySourcesPropertyResolver}. + * + * @author Phillip Webb + */ +class ConfigurationPropertySourcesPropertyResolverTests { + + @Test + void standardPropertyResolverResolvesMultipleTimes() { + StandardEnvironment environment = new StandardEnvironment(); + CountingMockPropertySource propertySource = createMockPropertySource(environment, true); + environment.getProperty("missing"); + assertThat(propertySource.getCount("missing")).isEqualTo(2); + } + + @Test + void configurationPropertySourcesPropertyResolverResolvesSingleTime() { + ResolverEnvironment environment = new ResolverEnvironment(); + CountingMockPropertySource propertySource = createMockPropertySource(environment, true); + environment.getProperty("missing"); + assertThat(propertySource.getCount("missing")).isEqualTo(1); + } + + @Test + void containsPropertyWhenValidConfigurationPropertyName() { + ResolverEnvironment environment = new ResolverEnvironment(); + CountingMockPropertySource propertySource = createMockPropertySource(environment, true); + assertThat(environment.containsProperty("spring")).isTrue(); + assertThat(environment.containsProperty("sprong")).isFalse(); + assertThat(propertySource.getCount("spring")).isEqualTo(1); + assertThat(propertySource.getCount("sprong")).isEqualTo(1); + } + + @Test + void containsPropertyWhenNotValidConfigurationPropertyName() { + ResolverEnvironment environment = new ResolverEnvironment(); + CountingMockPropertySource propertySource = createMockPropertySource(environment, true); + assertThat(environment.containsProperty("spr!ng")).isTrue(); + assertThat(environment.containsProperty("spr*ng")).isFalse(); + assertThat(propertySource.getCount("spr!ng")).isEqualTo(1); + assertThat(propertySource.getCount("spr*ng")).isEqualTo(1); + } + + @Test + void getPropertyWhenValidConfigurationPropertyName() { + ResolverEnvironment environment = new ResolverEnvironment(); + CountingMockPropertySource propertySource = createMockPropertySource(environment, true); + assertThat(environment.getProperty("spring")).isEqualTo("boot"); + assertThat(environment.getProperty("sprong")).isNull(); + assertThat(propertySource.getCount("spring")).isEqualTo(1); + assertThat(propertySource.getCount("sprong")).isEqualTo(1); + } + + @Test + void getPropertyWhenNotValidConfigurationPropertyName() { + ResolverEnvironment environment = new ResolverEnvironment(); + CountingMockPropertySource propertySource = createMockPropertySource(environment, true); + assertThat(environment.getProperty("spr!ng")).isEqualTo("boot"); + assertThat(environment.getProperty("spr*ng")).isNull(); + assertThat(propertySource.getCount("spr!ng")).isEqualTo(1); + assertThat(propertySource.getCount("spr*ng")).isEqualTo(1); + } + + @Test + void getPropertyWhenNotAttached() { + ResolverEnvironment environment = new ResolverEnvironment(); + CountingMockPropertySource propertySource = createMockPropertySource(environment, false); + assertThat(environment.getProperty("spring")).isEqualTo("boot"); + assertThat(environment.getProperty("sprong")).isNull(); + assertThat(propertySource.getCount("spring")).isEqualTo(1); + assertThat(propertySource.getCount("sprong")).isEqualTo(1); + } + + private CountingMockPropertySource createMockPropertySource(StandardEnvironment environment, boolean attach) { + CountingMockPropertySource propertySource = new CountingMockPropertySource(); + propertySource.withProperty("spring", "boot"); + propertySource.withProperty("spr!ng", "boot"); + environment.getPropertySources().addFirst(propertySource); + if (attach) { + ConfigurationPropertySources.attach(environment); + } + return propertySource; + } + + static class ResolverEnvironment extends StandardEnvironment { + + @Override + protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) { + return new ConfigurationPropertySourcesPropertyResolver(propertySources); + } + + } + + static class CountingMockPropertySource extends MockPropertySource { + + private final Map counts = new HashMap<>(); + + @Override + public Object getProperty(String name) { + incrementCount(name); + return super.getProperty(name); + } + + @Override + public boolean containsProperty(String name) { + incrementCount(name); + return super.containsProperty(name); + } + + private void incrementCount(String name) { + this.counts.computeIfAbsent(name, (k) -> new AtomicInteger()).incrementAndGet(); + } + + int getCount(String name) { + AtomicInteger count = this.counts.get(name); + return (count != null) ? count.get() : 0; + } + + } + +}