Skip to content

Commit 4af6e7f

Browse files
committed
Improve ConfigurationPropertySource performance
Attempt to improve the performance of the `ConfigurationPropertySource` adapters `containsDescendantOf` method. The method now operates on arrays rather than iterators and reduces the inner for-loop when possible. See gh-21416
1 parent 376098d commit 4af6e7f

File tree

7 files changed

+119
-66
lines changed

7 files changed

+119
-66
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/DefaultPropertyMapper.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,6 @@ private ConfigurationPropertyName tryMap(String propertySourceName) {
7979
return ConfigurationPropertyName.EMPTY;
8080
}
8181

82-
@Override
83-
public boolean isAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate) {
84-
return name.isAncestorOf(candidate);
85-
}
86-
8782
private static class LastMapping<T, M> {
8883

8984
private final T from;

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/PropertyMapper.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.context.properties.source;
1818

1919
import java.util.List;
20+
import java.util.function.BiPredicate;
2021

2122
import org.springframework.core.env.EnumerablePropertySource;
2223
import org.springframework.core.env.PropertySource;
@@ -39,6 +40,11 @@
3940
*/
4041
interface PropertyMapper {
4142

43+
/**
44+
* The default ancestor of check.
45+
*/
46+
BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> DEFAULT_ANCESTOR_OF_CHECK = ConfigurationPropertyName::isAncestorOf;
47+
4248
/**
4349
* Provide mappings from a {@link ConfigurationPropertySource}
4450
* {@link ConfigurationPropertyName}.
@@ -56,12 +62,12 @@ interface PropertyMapper {
5662
ConfigurationPropertyName map(String propertySourceName);
5763

5864
/**
59-
* Returns {@code true} if {@code name} is an ancestor (immediate or nested parent) of
60-
* the given candidate when considering mapping rules.
61-
* @param name the source name
62-
* @param candidate the candidate to check
63-
* @return {@code true} if the candidate is an ancestor of the name
65+
* Returns a {@link BiPredicate} that can be used to check if one name is an ancestor
66+
* of another when considering the mapping rules.
67+
* @return a predicate that can be used to check if one name is an ancestor of another
6468
*/
65-
boolean isAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate);
69+
default BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> getAncestorOfCheck() {
70+
return DEFAULT_ANCESTOR_OF_CHECK;
71+
}
6672

6773
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringConfigurationPropertySource.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class SpringConfigurationPropertySource implements ConfigurationPropertySource {
6969
*/
7070
SpringConfigurationPropertySource(PropertySource<?> propertySource, PropertyMapper... mappers) {
7171
Assert.notNull(propertySource, "PropertySource must not be null");
72+
Assert.isTrue(mappers.length > 0, "Mappers must contain at least one item");
7273
this.propertySource = propertySource;
7374
this.mappers = mappers;
7475
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java

Lines changed: 86 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@
1616

1717
package org.springframework.boot.context.properties.source;
1818

19-
import java.util.ArrayList;
2019
import java.util.Arrays;
21-
import java.util.Collection;
2220
import java.util.Collections;
2321
import java.util.ConcurrentModificationException;
2422
import java.util.HashMap;
2523
import java.util.Iterator;
2624
import java.util.List;
2725
import java.util.Map;
26+
import java.util.NoSuchElementException;
27+
import java.util.Objects;
28+
import java.util.function.BiPredicate;
2829
import java.util.function.Supplier;
2930
import java.util.stream.Stream;
3031

@@ -53,14 +54,27 @@
5354
class SpringIterableConfigurationPropertySource extends SpringConfigurationPropertySource
5455
implements IterableConfigurationPropertySource, CachingConfigurationPropertySource {
5556

56-
private volatile Collection<ConfigurationPropertyName> configurationPropertyNames;
57+
private volatile ConfigurationPropertyName[] configurationPropertyNames;
5758

5859
private final SoftReferenceConfigurationPropertyCache<Mappings> cache;
5960

61+
private final BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> ancestorOfCheck;
62+
6063
SpringIterableConfigurationPropertySource(EnumerablePropertySource<?> propertySource, PropertyMapper... mappers) {
6164
super(propertySource, mappers);
6265
assertEnumerablePropertySource();
6366
this.cache = new SoftReferenceConfigurationPropertyCache<>(isImmutablePropertySource());
67+
this.ancestorOfCheck = getAncestorOfCheck(mappers);
68+
}
69+
70+
private BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> getAncestorOfCheck(
71+
PropertyMapper[] mappers) {
72+
BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> ancestorOfCheck = mappers[0]
73+
.getAncestorOfCheck();
74+
for (int i = 1; i < mappers.length; i++) {
75+
ancestorOfCheck = ancestorOfCheck.or(mappers[i].getAncestorOfCheck());
76+
}
77+
return ancestorOfCheck;
6478
}
6579

6680
private void assertEnumerablePropertySource() {
@@ -100,19 +114,35 @@ public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName
100114

101115
@Override
102116
public Stream<ConfigurationPropertyName> stream() {
103-
return getConfigurationPropertyNames().stream();
117+
ConfigurationPropertyName[] names = getConfigurationPropertyNames();
118+
return Arrays.stream(names).filter(Objects::nonNull);
104119
}
105120

106121
@Override
107122
public Iterator<ConfigurationPropertyName> iterator() {
108-
return getConfigurationPropertyNames().iterator();
123+
return new ConfigurationPropertyNamesIterator(getConfigurationPropertyNames());
124+
}
125+
126+
@Override
127+
public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) {
128+
ConfigurationPropertyState result = super.containsDescendantOf(name);
129+
if (result != ConfigurationPropertyState.UNKNOWN) {
130+
return result;
131+
}
132+
ConfigurationPropertyName[] candidates = getConfigurationPropertyNames();
133+
for (ConfigurationPropertyName candidate : candidates) {
134+
if (candidate != null && this.ancestorOfCheck.test(name, candidate)) {
135+
return ConfigurationPropertyState.PRESENT;
136+
}
137+
}
138+
return ConfigurationPropertyState.ABSENT;
109139
}
110140

111-
private Collection<ConfigurationPropertyName> getConfigurationPropertyNames() {
141+
private ConfigurationPropertyName[] getConfigurationPropertyNames() {
112142
if (!isImmutablePropertySource()) {
113143
return getMappings().getConfigurationPropertyNames(getPropertySource().getPropertyNames());
114144
}
115-
Collection<ConfigurationPropertyName> configurationPropertyNames = this.configurationPropertyNames;
145+
ConfigurationPropertyName[] configurationPropertyNames = this.configurationPropertyNames;
116146
if (configurationPropertyNames == null) {
117147
configurationPropertyNames = getMappings()
118148
.getConfigurationPropertyNames(getPropertySource().getPropertyNames());
@@ -121,24 +151,6 @@ private Collection<ConfigurationPropertyName> getConfigurationPropertyNames() {
121151
return configurationPropertyNames;
122152
}
123153

124-
@Override
125-
public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) {
126-
ConfigurationPropertyState result = super.containsDescendantOf(name);
127-
if (result == ConfigurationPropertyState.UNKNOWN) {
128-
result = ConfigurationPropertyState.search(this, (candidate) -> isAncestorOf(name, candidate));
129-
}
130-
return result;
131-
}
132-
133-
private boolean isAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate) {
134-
for (PropertyMapper mapper : getMappers()) {
135-
if (mapper.isAncestorOf(name, candidate)) {
136-
return true;
137-
}
138-
}
139-
return false;
140-
}
141-
142154
private Mappings getMappings() {
143155
return this.cache.get(this::createMappings, this::updateMappings);
144156
}
@@ -170,6 +182,8 @@ protected EnumerablePropertySource<?> getPropertySource() {
170182

171183
private static class Mappings {
172184

185+
private static final ConfigurationPropertyName[] EMPTY_NAMES_ARRAY = {};
186+
173187
private final PropertyMapper[] mappers;
174188

175189
private final boolean immutable;
@@ -178,7 +192,7 @@ private static class Mappings {
178192

179193
private volatile Map<String, ConfigurationPropertyName> reverseMappings;
180194

181-
private volatile Collection<ConfigurationPropertyName> configurationPropertyNames;
195+
private volatile ConfigurationPropertyName[] configurationPropertyNames;
182196

183197
private volatile String[] lastUpdated;
184198

@@ -230,30 +244,66 @@ private void updateMappings(String[] propertyNames) {
230244
this.reverseMappings = reverseMappings;
231245
this.lastUpdated = this.immutable ? null : propertyNames;
232246
this.configurationPropertyNames = this.immutable
233-
? Collections.unmodifiableCollection(reverseMappings.values()) : null;
247+
? reverseMappings.values().toArray(new ConfigurationPropertyName[0]) : null;
234248
}
235249

236250
List<String> getMapped(ConfigurationPropertyName configurationPropertyName) {
237251
return this.mappings.getOrDefault(configurationPropertyName, Collections.emptyList());
238252
}
239253

240-
Collection<ConfigurationPropertyName> getConfigurationPropertyNames(String[] propertyNames) {
241-
Collection<ConfigurationPropertyName> names = this.configurationPropertyNames;
254+
ConfigurationPropertyName[] getConfigurationPropertyNames(String[] propertyNames) {
255+
ConfigurationPropertyName[] names = this.configurationPropertyNames;
242256
if (names != null) {
243257
return names;
244258
}
245259
Map<String, ConfigurationPropertyName> reverseMappings = this.reverseMappings;
246260
if (reverseMappings == null || reverseMappings.isEmpty()) {
247-
return Collections.emptySet();
261+
return EMPTY_NAMES_ARRAY;
262+
}
263+
names = new ConfigurationPropertyName[propertyNames.length];
264+
for (int i = 0; i < propertyNames.length; i++) {
265+
names[i] = reverseMappings.get(propertyNames[i]);
248266
}
249-
List<ConfigurationPropertyName> relevantNames = new ArrayList<>(reverseMappings.size());
250-
for (String propertyName : propertyNames) {
251-
ConfigurationPropertyName configurationPropertyName = reverseMappings.get(propertyName);
252-
if (configurationPropertyName != null) {
253-
relevantNames.add(configurationPropertyName);
267+
return names;
268+
}
269+
270+
}
271+
272+
/**
273+
* ConfigurationPropertyNames iterator backed by an array.
274+
*/
275+
private static class ConfigurationPropertyNamesIterator implements Iterator<ConfigurationPropertyName> {
276+
277+
private final ConfigurationPropertyName[] names;
278+
279+
private int index = 0;
280+
281+
ConfigurationPropertyNamesIterator(ConfigurationPropertyName[] names) {
282+
this.names = names;
283+
}
284+
285+
@Override
286+
public boolean hasNext() {
287+
skipNulls();
288+
return this.index < this.names.length;
289+
}
290+
291+
@Override
292+
public ConfigurationPropertyName next() {
293+
skipNulls();
294+
if (this.index >= this.names.length) {
295+
throw new NoSuchElementException();
296+
}
297+
return this.names[this.index++];
298+
}
299+
300+
private void skipNulls() {
301+
while (this.index < this.names.length) {
302+
if (this.names[this.index] != null) {
303+
return;
254304
}
305+
this.index++;
255306
}
256-
return relevantNames;
257307
}
258308

259309
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapper.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Collections;
2121
import java.util.List;
2222
import java.util.Locale;
23+
import java.util.function.BiPredicate;
2324

2425
import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form;
2526

@@ -103,7 +104,11 @@ private static boolean isNumber(String string) {
103104
}
104105

105106
@Override
106-
public boolean isAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate) {
107+
public BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> getAncestorOfCheck() {
108+
return this::isAncestorOf;
109+
}
110+
111+
private boolean isAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate) {
107112
return name.isAncestorOf(candidate) || isLegacyAncestorOf(name, candidate);
108113
}
109114

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SystemEnvironmentPropertyMapperTests.java

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.boot.context.properties.source;
1818

19+
import java.util.function.BiPredicate;
20+
1921
import org.junit.jupiter.api.Test;
2022

2123
import static org.assertj.core.api.Assertions.assertThat;
@@ -70,29 +72,28 @@ void underscoreWithWhitespaceShouldMapToEmptyString() {
7072
@Test
7173
void isAncestorOfConsidersLegacyNames() {
7274
ConfigurationPropertyName name = ConfigurationPropertyName.of("my.spring-boot");
73-
assertThat(getMapper().isAncestorOf(name, ConfigurationPropertyName.adapt("MY_SPRING_BOOT_PROPERTY", '_')))
74-
.isTrue();
75-
assertThat(getMapper().isAncestorOf(name, ConfigurationPropertyName.adapt("MY_SPRINGBOOT_PROPERTY", '_')))
76-
.isTrue();
77-
assertThat(getMapper().isAncestorOf(name, ConfigurationPropertyName.adapt("MY_BOOT_PROPERTY", '_'))).isFalse();
75+
BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> check = getMapper().getAncestorOfCheck();
76+
assertThat(check.test(name, ConfigurationPropertyName.adapt("MY_SPRING_BOOT_PROPERTY", '_'))).isTrue();
77+
assertThat(check.test(name, ConfigurationPropertyName.adapt("MY_SPRINGBOOT_PROPERTY", '_'))).isTrue();
78+
assertThat(check.test(name, ConfigurationPropertyName.adapt("MY_BOOT_PROPERTY", '_'))).isFalse();
7879
}
7980

8081
@Test
8182
void isAncestorOfWhenNonCanonicalSource() {
8283
ConfigurationPropertyName name = ConfigurationPropertyName.adapt("my.springBoot", '.');
83-
assertThat(getMapper().isAncestorOf(name, ConfigurationPropertyName.of("my.spring-boot.property"))).isTrue();
84-
assertThat(getMapper().isAncestorOf(name, ConfigurationPropertyName.of("my.springboot.property"))).isTrue();
85-
assertThat(getMapper().isAncestorOf(name, ConfigurationPropertyName.of("my.boot.property"))).isFalse();
84+
BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> check = getMapper().getAncestorOfCheck();
85+
assertThat(check.test(name, ConfigurationPropertyName.of("my.spring-boot.property"))).isTrue();
86+
assertThat(check.test(name, ConfigurationPropertyName.of("my.springboot.property"))).isTrue();
87+
assertThat(check.test(name, ConfigurationPropertyName.of("my.boot.property"))).isFalse();
8688
}
8789

8890
@Test
8991
void isAncestorOfWhenNonCanonicalAndDashedSource() {
9092
ConfigurationPropertyName name = ConfigurationPropertyName.adapt("my.springBoot.input-value", '.');
91-
assertThat(getMapper().isAncestorOf(name, ConfigurationPropertyName.of("my.spring-boot.input-value.property")))
92-
.isTrue();
93-
assertThat(getMapper().isAncestorOf(name, ConfigurationPropertyName.of("my.springboot.inputvalue.property")))
94-
.isTrue();
95-
assertThat(getMapper().isAncestorOf(name, ConfigurationPropertyName.of("my.boot.property"))).isFalse();
93+
BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> check = getMapper().getAncestorOfCheck();
94+
assertThat(check.test(name, ConfigurationPropertyName.of("my.spring-boot.input-value.property"))).isTrue();
95+
assertThat(check.test(name, ConfigurationPropertyName.of("my.springboot.inputvalue.property"))).isTrue();
96+
assertThat(check.test(name, ConfigurationPropertyName.of("my.boot.property"))).isFalse();
9697
}
9798

9899
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/TestPropertyMapper.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,4 @@ public ConfigurationPropertyName map(String propertySourceName) {
5353
return this.fromSource.getOrDefault(propertySourceName, ConfigurationPropertyName.EMPTY);
5454
}
5555

56-
@Override
57-
public boolean isAncestorOf(ConfigurationPropertyName name, ConfigurationPropertyName candidate) {
58-
return name.isAncestorOf(candidate);
59-
}
60-
6156
}

0 commit comments

Comments
 (0)