Fix same name configuration properties adapting

Update `SpringConfigurationPropertySources` adapter logic so that and
exact instance match is used when checking if the cache can be used.

Prior to this commit if a property source was replaced with one that
had exactly the same name, the adapted version would not change.

Fixes gh-12451
pull/12519/merge
Phillip Webb 7 years ago
parent c201c1dac1
commit 522e3df394

@ -124,7 +124,8 @@ public final class ConfigurationPropertySources {
* This method will flatten any nested property sources and will filter all
* {@link StubPropertySource stub property sources}. Updates to the underlying source,
* identified by changes in the sources returned by its iterator, will be
* automatically tracked.
* automatically tracked. The underlying source should be thread safe, for example a
* {@link MutablePropertySources}
* @param sources the Spring property sources to adapt
* @return an {@link Iterable} containing newly adapted
* {@link SpringConfigurationPropertySource} instances

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -16,19 +16,20 @@
package org.springframework.boot.context.properties.source;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.function.Function;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.PropertySource.StubPropertySource;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
/**
* Adapter to convert Spring's {@link MutablePropertySources} to
@ -41,9 +42,8 @@ class SpringConfigurationPropertySources
private final Iterable<PropertySource<?>> sources;
private volatile PropertySourcesKey lastKey;
private volatile List<ConfigurationPropertySource> adaptedSources;
private final Map<PropertySource<?>, ConfigurationPropertySource> cache = new ConcurrentReferenceHashMap<>(
16, ReferenceType.SOFT);
SpringConfigurationPropertySources(Iterable<PropertySource<?>> sources) {
Assert.notNull(sources, "Sources must not be null");
@ -52,107 +52,81 @@ class SpringConfigurationPropertySources
@Override
public Iterator<ConfigurationPropertySource> iterator() {
checkForChanges();
return this.adaptedSources.iterator();
}
private void checkForChanges() {
PropertySourcesKey lastKey = this.lastKey;
PropertySourcesKey currentKey = new PropertySourcesKey(this.sources);
if (!currentKey.equals(lastKey)) {
onChange(this.sources);
this.lastKey = currentKey;
}
}
private void onChange(Iterable<PropertySource<?>> sources) {
this.adaptedSources = streamPropertySources(sources)
.map(SpringConfigurationPropertySource::from)
.collect(Collectors.toList());
return new SourcesIterator(this.sources.iterator(), this::adapt);
}
private Stream<PropertySource<?>> streamPropertySources(
Iterable<PropertySource<?>> sources) {
return StreamSupport.stream(sources.spliterator(), false).flatMap(this::flatten)
.filter(this::isIncluded);
}
private Stream<PropertySource<?>> flatten(PropertySource<?> source) {
if (source.getSource() instanceof ConfigurableEnvironment) {
return streamPropertySources(
((ConfigurableEnvironment) source.getSource()).getPropertySources());
private ConfigurationPropertySource adapt(PropertySource<?> source) {
ConfigurationPropertySource result = this.cache.get(source);
// Most PropertySource test quality only using the source name, we need to
// check the actual source hasn't also changed.
if (result != null && result.getUnderlyingSource() == source) {
return result;
}
return Stream.of(source);
result = SpringConfigurationPropertySource.from(source);
this.cache.put(source, result);
return result;
}
private boolean isIncluded(PropertySource<?> source) {
return !(source instanceof StubPropertySource)
&& !(source instanceof ConfigurationPropertySourcesPropertySource);
}
private static class SourcesIterator
implements Iterator<ConfigurationPropertySource> {
private static class PropertySourcesKey {
private Deque<Iterator<PropertySource<?>>> iterators;
private final List<PropertySourceKey> keys = new ArrayList<>();
private ConfigurationPropertySource next;
PropertySourcesKey(Iterable<PropertySource<?>> sources) {
sources.forEach(this::addKey);
}
private final Function<PropertySource<?>, ConfigurationPropertySource> adapter;
private void addKey(PropertySource<?> source) {
this.keys.add(new PropertySourceKey(source));
SourcesIterator(Iterator<PropertySource<?>> iterator,
Function<PropertySource<?>, ConfigurationPropertySource> adapter) {
this.iterators = new ArrayDeque<>(4);
this.iterators.push(iterator);
this.adapter = adapter;
}
@Override
public int hashCode() {
return this.keys.hashCode();
public boolean hasNext() {
return fetchNext() != null;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
public ConfigurationPropertySource next() {
ConfigurationPropertySource next = fetchNext();
this.next = null;
if (next == null) {
throw new NoSuchElementException();
}
return this.keys.equals(((PropertySourcesKey) obj).keys);
return next;
}
}
private static class PropertySourceKey {
private final String name;
private final Class<?> type;
PropertySourceKey(PropertySource<?> source) {
this.name = source.getName();
this.type = source.getClass();
private ConfigurationPropertySource fetchNext() {
if (this.next == null) {
if (this.iterators.isEmpty()) {
return null;
}
if (!this.iterators.peek().hasNext()) {
this.iterators.pop();
return fetchNext();
}
PropertySource<?> candidate = this.iterators.peek().next();
if (candidate.getSource() instanceof ConfigurableEnvironment) {
push((ConfigurableEnvironment) candidate.getSource());
return fetchNext();
}
if (isIgnored(candidate)) {
return fetchNext();
}
this.next = this.adapter.apply(candidate);
}
return this.next;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ObjectUtils.nullSafeHashCode(this.name);
result = prime * result + ObjectUtils.nullSafeHashCode(this.type);
return result;
private void push(ConfigurableEnvironment environment) {
this.iterators.push(environment.getPropertySources().iterator());
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
PropertySourceKey other = (PropertySourceKey) obj;
boolean result = true;
result = result && ObjectUtils.nullSafeEquals(this.name, other.name);
result = result && ObjectUtils.nullSafeEquals(this.type, other.type);
return result;
private boolean isIgnored(PropertySource<?> candidate) {
return (candidate instanceof StubPropertySource
|| candidate instanceof ConfigurationPropertySourcesPropertySource);
}
}

@ -152,12 +152,36 @@ public class SpringConfigurationPropertySourcesTests {
@Test
public void shouldTrackChanges() {
MutablePropertySources sources = new MutablePropertySources();
sources.addLast(
new MapPropertySource("test1", Collections.singletonMap("a", "b")));
assertThat(new SpringConfigurationPropertySources(sources).iterator()).hasSize(1);
sources.addLast(
new MapPropertySource("test2", Collections.singletonMap("b", "c")));
assertThat(new SpringConfigurationPropertySources(sources).iterator()).hasSize(2);
SpringConfigurationPropertySources configurationSources = new SpringConfigurationPropertySources(
sources);
assertThat(configurationSources.iterator()).hasSize(0);
MapPropertySource source1 = new MapPropertySource("test1",
Collections.singletonMap("a", "b"));
sources.addLast(source1);
assertThat(configurationSources.iterator()).hasSize(1);
MapPropertySource source2 = new MapPropertySource("test2",
Collections.singletonMap("b", "c"));
sources.addLast(source2);
assertThat(configurationSources.iterator()).hasSize(2);
}
@Test
public void shouldTrackWhenSourceHasIdenticalName() {
MutablePropertySources sources = new MutablePropertySources();
SpringConfigurationPropertySources configurationSources = new SpringConfigurationPropertySources(
sources);
ConfigurationPropertyName name = ConfigurationPropertyName.of("a");
MapPropertySource source1 = new MapPropertySource("test",
Collections.singletonMap("a", "s1"));
sources.addLast(source1);
assertThat(configurationSources.iterator().next().getConfigurationProperty(name)
.getValue()).isEqualTo("s1");
MapPropertySource source2 = new MapPropertySource("test",
Collections.singletonMap("a", "s2"));
sources.remove("test");
sources.addLast(source2);
assertThat(configurationSources.iterator().next().getConfigurationProperty(name)
.getValue()).isEqualTo("s2");
}
}

Loading…
Cancel
Save