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
This commit is contained in:
		
							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();
 | 
			
		||||
		return new SourcesIterator(this.sources.iterator(), this::adapt);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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());
 | 
			
		||||
		}
 | 
			
		||||
		return Stream.of(source);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private boolean isIncluded(PropertySource<?> source) {
 | 
			
		||||
		return !(source instanceof StubPropertySource)
 | 
			
		||||
				&& !(source instanceof ConfigurationPropertySourcesPropertySource);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static class PropertySourcesKey {
 | 
			
		||||
 | 
			
		||||
		private final List<PropertySourceKey> keys = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
		PropertySourcesKey(Iterable<PropertySource<?>> sources) {
 | 
			
		||||
			sources.forEach(this::addKey);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void addKey(PropertySource<?> source) {
 | 
			
		||||
			this.keys.add(new PropertySourceKey(source));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public int hashCode() {
 | 
			
		||||
			return this.keys.hashCode();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public boolean equals(Object obj) {
 | 
			
		||||
			if (this == obj) {
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
			if (obj == null || getClass() != obj.getClass()) {
 | 
			
		||||
				return false;
 | 
			
		||||
			}
 | 
			
		||||
			return this.keys.equals(((PropertySourcesKey) obj).keys);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static class PropertySourceKey {
 | 
			
		||||
 | 
			
		||||
		private final String name;
 | 
			
		||||
 | 
			
		||||
		private final Class<?> type;
 | 
			
		||||
 | 
			
		||||
		PropertySourceKey(PropertySource<?> source) {
 | 
			
		||||
			this.name = source.getName();
 | 
			
		||||
			this.type = source.getClass();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@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);
 | 
			
		||||
	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;
 | 
			
		||||
		}
 | 
			
		||||
		result = SpringConfigurationPropertySource.from(source);
 | 
			
		||||
		this.cache.put(source, result);
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static class SourcesIterator
 | 
			
		||||
			implements Iterator<ConfigurationPropertySource> {
 | 
			
		||||
 | 
			
		||||
		private Deque<Iterator<PropertySource<?>>> iterators;
 | 
			
		||||
 | 
			
		||||
		private ConfigurationPropertySource next;
 | 
			
		||||
 | 
			
		||||
		private final Function<PropertySource<?>, ConfigurationPropertySource> adapter;
 | 
			
		||||
 | 
			
		||||
		SourcesIterator(Iterator<PropertySource<?>> iterator,
 | 
			
		||||
				Function<PropertySource<?>, ConfigurationPropertySource> adapter) {
 | 
			
		||||
			this.iterators = new ArrayDeque<>(4);
 | 
			
		||||
			this.iterators.push(iterator);
 | 
			
		||||
			this.adapter = adapter;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public boolean equals(Object obj) {
 | 
			
		||||
			if (this == obj) {
 | 
			
		||||
				return true;
 | 
			
		||||
		public boolean hasNext() {
 | 
			
		||||
			return fetchNext() != null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public ConfigurationPropertySource next() {
 | 
			
		||||
			ConfigurationPropertySource next = fetchNext();
 | 
			
		||||
			this.next = null;
 | 
			
		||||
			if (next == null) {
 | 
			
		||||
				throw new NoSuchElementException();
 | 
			
		||||
			}
 | 
			
		||||
			if (obj == null || getClass() != obj.getClass()) {
 | 
			
		||||
				return false;
 | 
			
		||||
			return next;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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);
 | 
			
		||||
			}
 | 
			
		||||
			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;
 | 
			
		||||
			return this.next;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void push(ConfigurableEnvironment environment) {
 | 
			
		||||
			this.iterators.push(environment.getPropertySources().iterator());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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…
	
		Reference in New Issue