Skip to content

Commit 8e26898

Browse files
committed
Improve thread safety in property source cache
Update `SpringIterableConfigurationPropertySource` so that they cache and cache key are not stored in different fields. Prior to this commit it was possible that the an incorrect cache could be returned from because the key and cache were out of sync. This commit also allows more lenient handling of ConcurrentModification exceptions if they are thrown during cache retrieval. Closes gh-17017 See gh-17013
1 parent e05799d commit 8e26898

File tree

2 files changed

+77
-9
lines changed

2 files changed

+77
-9
lines changed

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

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.Collections;
21+
import java.util.ConcurrentModificationException;
2122
import java.util.HashSet;
2223
import java.util.Iterator;
2324
import java.util.List;
@@ -44,8 +45,6 @@
4445
class SpringIterableConfigurationPropertySource extends SpringConfigurationPropertySource
4546
implements IterableConfigurationPropertySource {
4647

47-
private volatile Object cacheKey;
48-
4948
private volatile Cache cache;
5049

5150
SpringIterableConfigurationPropertySource(EnumerablePropertySource<?> propertySource,
@@ -131,16 +130,23 @@ private PropertyMapping[] getPropertyMappings(Cache cache) {
131130
}
132131

133132
private Cache getCache() {
134-
CacheKey cacheKey = CacheKey.get(getPropertySource());
135-
if (cacheKey == null) {
133+
CacheKey key = CacheKey.get(getPropertySource());
134+
if (key == null) {
136135
return null;
137136
}
138-
if (ObjectUtils.nullSafeEquals(cacheKey, this.cacheKey)) {
139-
return this.cache;
137+
Cache cache = this.cache;
138+
try {
139+
if (cache != null && cache.hasKeyEqualTo(key)) {
140+
return cache;
141+
}
142+
cache = new Cache(key.copy());
143+
this.cache = cache;
144+
return cache;
145+
}
146+
catch (ConcurrentModificationException ex) {
147+
// Not fatal at this point, we can continue without a cache
148+
return null;
140149
}
141-
this.cache = new Cache();
142-
this.cacheKey = cacheKey.copy();
143-
return this.cache;
144150
}
145151

146152
@Override
@@ -150,10 +156,20 @@ protected EnumerablePropertySource<?> getPropertySource() {
150156

151157
private static class Cache {
152158

159+
private final CacheKey key;
160+
153161
private List<ConfigurationPropertyName> names;
154162

155163
private PropertyMapping[] mappings;
156164

165+
Cache(CacheKey key) {
166+
this.key = key;
167+
}
168+
169+
public boolean hasKeyEqualTo(CacheKey key) {
170+
return this.key.equals(key);
171+
}
172+
157173
public List<ConfigurationPropertyName> getNames() {
158174
return this.names;
159175
}

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616

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

19+
import java.util.ConcurrentModificationException;
20+
import java.util.Iterator;
1921
import java.util.LinkedHashMap;
22+
import java.util.LinkedHashSet;
2023
import java.util.Map;
24+
import java.util.Set;
2125

2226
import org.junit.Test;
2327

@@ -169,6 +173,21 @@ public void propertySourceKeyDataChangeInvalidatesCache() {
169173
assertThat(adapter.stream().count()).isEqualTo(3);
170174
}
171175

176+
@Test
177+
public void concurrentModificationExceptionInvalidatesCache() {
178+
// gh-17013
179+
ConcurrentModificationThrowingMap<String, Object> map = new ConcurrentModificationThrowingMap<>();
180+
map.put("key1", "value1");
181+
map.put("key2", "value2");
182+
EnumerablePropertySource<?> source = new MapPropertySource("test", map);
183+
SpringIterableConfigurationPropertySource adapter = new SpringIterableConfigurationPropertySource(
184+
source, DefaultPropertyMapper.INSTANCE);
185+
assertThat(adapter.stream().count()).isEqualTo(2);
186+
map.setThrowException(true);
187+
map.put("key3", "value3");
188+
assertThat(adapter.stream().count()).isEqualTo(3);
189+
}
190+
172191
/**
173192
* Test {@link PropertySource} that's also an {@link OriginLookup}.
174193
*/
@@ -206,4 +225,37 @@ public String toString() {
206225

207226
}
208227

228+
private static class ConcurrentModificationThrowingMap<K, V>
229+
extends LinkedHashMap<K, V> {
230+
231+
private boolean throwException;
232+
233+
public void setThrowException(boolean throwException) {
234+
this.throwException = throwException;
235+
}
236+
237+
@Override
238+
public Set<K> keySet() {
239+
return new KeySet(super.keySet());
240+
}
241+
242+
private class KeySet extends LinkedHashSet<K> {
243+
244+
KeySet(Set<K> keySet) {
245+
super(keySet);
246+
}
247+
248+
@Override
249+
public Iterator<K> iterator() {
250+
if (ConcurrentModificationThrowingMap.this.throwException) {
251+
ConcurrentModificationThrowingMap.this.throwException = false;
252+
throw new ConcurrentModificationException();
253+
}
254+
return super.iterator();
255+
}
256+
257+
}
258+
259+
}
260+
209261
}

0 commit comments

Comments
 (0)