Short circuit already covered property sources

Add an alternative `PropertySourcesPropertyResolver` that can short
circuit resolution of properties that are already covered by the
`ConfigurationPropertySourcesPropertySource`.

Prior to this commit, calling `getProperty` or `containsProperty` on an
`Environment` that has `ConfigurationPropertySources` attached could
result in two identical calls to the underlying source. The first call
would be via the adapted source, and the second would be direct. Since
we can now plug-in a custom `PropertySourcesPropertyResolver` to the
`Environment`, we can optimize resolution so that calls happen only
once.

Closes gh-17400
pull/25947/head
Phillip Webb 4 years ago
parent 1d302f4c63
commit 6ad100eae6

@ -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 @@ class ApplicationEnvironment extends StandardEnvironment {
return null;
}
@Override
protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) {
return ConfigurationPropertySources.createPropertyResolver(propertySources);
}
}

@ -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 @@ class ApplicationReactiveWebEnvironment extends StandardReactiveWebEnvironment {
return null;
}
@Override
protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) {
return ConfigurationPropertySources.createPropertyResolver(propertySources);
}
}

@ -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 @@ class ApplicationServletEnvironment extends StandardServletEnvironment {
return null;
}
@Override
protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) {
return ConfigurationPropertySources.createPropertyResolver(propertySources);
}
}

@ -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.Collections;
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 final class ConfigurationPropertySources {
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 final class ConfigurationPropertySources {
}
}
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}.

@ -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> T getProperty(String key, Class<T> targetValueType) {
return getProperty(key, targetValueType, true);
}
@Override
protected String getPropertyAsRawString(String key) {
return getProperty(key, String.class, false);
}
private <T> T getProperty(String key, Class<T> 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<ConfigurationPropertySource> 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> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
return super.getProperty(key, targetValueType, resolveNestedPlaceholders);
}
}
}

@ -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<Iterable
super(name, source);
}
@Override
public boolean containsProperty(String name) {
return findConfigurationProperty(name) != null;
}
@Override
public Object getProperty(String name) {
ConfigurationProperty configurationProperty = findConfigurationProperty(name);
@ -57,7 +62,7 @@ class ConfigurationPropertySourcesPropertySource extends PropertySource<Iterable
}
}
private ConfigurationProperty findConfigurationProperty(ConfigurationPropertyName name) {
ConfigurationProperty findConfigurationProperty(ConfigurationPropertyName name) {
if (name == null) {
return null;
}

@ -51,6 +51,10 @@ class SpringConfigurationPropertySources implements Iterable<ConfigurationProper
this.sources = sources;
}
boolean isUsingSources(Iterable<PropertySource<?>> sources) {
return this.sources == sources;
}
@Override
public Iterator<ConfigurationPropertySource> iterator() {
return new SourcesIterator(this.sources.iterator(), this::adapt);

@ -18,8 +18,11 @@ package org.springframework.boot;
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 @@ public abstract class AbstractApplicationEnvironmentTests {
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();
}

@ -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<String, AtomicInteger> 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;
}
}
}
Loading…
Cancel
Save