Skip to content

Commit 189d84d

Browse files
committed
Add ConfigurationPropertyCaching override support
Add `ConfigurationPropertyCaching.override()` method which can be used to temporarily enable caching for the duration of an operation. The `Binder` now uses this method to ensure that caching is enabled whilst a set of related binding operations are performed. Closes gh-44860
1 parent ca9c3ed commit 189d84d

File tree

6 files changed

+174
-13
lines changed

6 files changed

+174
-13
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java

+16-10
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
3636
import org.springframework.boot.context.properties.bind.Bindable.BindRestriction;
3737
import org.springframework.boot.context.properties.source.ConfigurationProperty;
38+
import org.springframework.boot.context.properties.source.ConfigurationPropertyCaching;
3839
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
3940
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
4041
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
@@ -69,6 +70,8 @@ public class Binder {
6970

7071
private final Map<BindMethod, List<DataObjectBinder>> dataObjectBinders;
7172

73+
private ConfigurationPropertyCaching configurationPropertyCaching;
74+
7275
/**
7376
* Create a new {@link Binder} instance for the specified sources. A
7477
* {@link DefaultFormattingConversionService} will be used for all conversion.
@@ -189,6 +192,7 @@ public Binder(Iterable<ConfigurationPropertySource> sources, PlaceholdersResolve
189192
Assert.notNull(source, "'sources' must not contain null elements");
190193
}
191194
this.sources = sources;
195+
this.configurationPropertyCaching = ConfigurationPropertyCaching.get(sources);
192196
this.placeholdersResolver = (placeholdersResolver != null) ? placeholdersResolver : PlaceholdersResolver.NONE;
193197
this.bindConverter = BindConverter.get(conversionServices, propertyEditorInitializer);
194198
this.defaultBindHandler = (defaultBindHandler != null) ? defaultBindHandler : BindHandler.DEFAULT;
@@ -341,17 +345,19 @@ private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandl
341345

342346
private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context,
343347
boolean allowRecursiveBinding, boolean create) {
344-
try {
345-
Bindable<T> replacementTarget = handler.onStart(name, target, context);
346-
if (replacementTarget == null) {
347-
return handleBindResult(name, target, handler, context, null, create);
348+
try (ConfigurationPropertyCaching.CacheOverride cacheOverride = this.configurationPropertyCaching.override()) {
349+
try {
350+
Bindable<T> replacementTarget = handler.onStart(name, target, context);
351+
if (replacementTarget == null) {
352+
return handleBindResult(name, target, handler, context, null, create);
353+
}
354+
target = replacementTarget;
355+
Object bound = bindObject(name, target, handler, context, allowRecursiveBinding);
356+
return handleBindResult(name, target, handler, context, bound, create);
357+
}
358+
catch (Exception ex) {
359+
return handleBindError(name, target, handler, context, ex);
348360
}
349-
target = replacementTarget;
350-
Object bound = bindObject(name, target, handler, context, allowRecursiveBinding);
351-
return handleBindResult(name, target, handler, context, bound, create);
352-
}
353-
catch (Exception ex) {
354-
return handleBindError(name, target, handler, context, ex);
355361
}
356362
}
357363

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

+21
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ public interface ConfigurationPropertyCaching {
5151
*/
5252
void clear();
5353

54+
/**
55+
* Override caching to temporarily enable it. Once caching is no longer needed the
56+
* returned {@link CacheOverride} should be closed to restore previous cache settings.
57+
* @return a {@link CacheOverride}
58+
* @since 3.5.0
59+
*/
60+
CacheOverride override();
61+
5462
/**
5563
* Get for all configuration property sources in the environment.
5664
* @param environment the spring environment
@@ -107,4 +115,17 @@ static ConfigurationPropertyCaching get(Iterable<ConfigurationPropertySource> so
107115
throw new IllegalStateException("Unable to find cache from configuration property sources");
108116
}
109117

118+
/**
119+
* {@link AutoCloseable} used to control a
120+
* {@link ConfigurationPropertyCaching#override() cache override}.
121+
*
122+
* @since 3.5.0
123+
*/
124+
interface CacheOverride extends AutoCloseable {
125+
126+
@Override
127+
void close();
128+
129+
}
130+
110131
}

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

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,8 @@
1717
package org.springframework.boot.context.properties.source;
1818

1919
import java.time.Duration;
20+
import java.util.ArrayList;
21+
import java.util.List;
2022
import java.util.function.Consumer;
2123

2224
/**
@@ -53,6 +55,13 @@ public void clear() {
5355
forEach(ConfigurationPropertyCaching::clear);
5456
}
5557

58+
@Override
59+
public CacheOverride override() {
60+
CacheOverrides override = new CacheOverrides();
61+
forEach(override::add);
62+
return override;
63+
}
64+
5665
private void forEach(Consumer<ConfigurationPropertyCaching> action) {
5766
if (this.sources != null) {
5867
for (ConfigurationPropertySource source : this.sources) {
@@ -64,4 +73,22 @@ private void forEach(Consumer<ConfigurationPropertyCaching> action) {
6473
}
6574
}
6675

76+
/**
77+
* Composite {@link CacheOverride}.
78+
*/
79+
private final class CacheOverrides implements CacheOverride {
80+
81+
private List<CacheOverride> overrides = new ArrayList<>();
82+
83+
void add(ConfigurationPropertyCaching caching) {
84+
this.overrides.add(caching.override());
85+
}
86+
87+
@Override
88+
public void close() {
89+
this.overrides.forEach(CacheOverride::close);
90+
}
91+
92+
}
93+
6794
}

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

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.lang.ref.SoftReference;
2020
import java.time.Duration;
2121
import java.time.Instant;
22+
import java.util.concurrent.atomic.AtomicBoolean;
2223
import java.util.function.Supplier;
2324
import java.util.function.UnaryOperator;
2425

@@ -33,6 +34,9 @@ class SoftReferenceConfigurationPropertyCache<T> implements ConfigurationPropert
3334

3435
private static final Duration UNLIMITED = Duration.ZERO;
3536

37+
static final CacheOverride NO_OP_OVERRIDE = () -> {
38+
};
39+
3640
private final boolean neverExpire;
3741

3842
private volatile Duration timeToLive;
@@ -65,6 +69,25 @@ public void clear() {
6569
this.lastAccessed = null;
6670
}
6771

72+
@Override
73+
public CacheOverride override() {
74+
if (this.neverExpire) {
75+
return NO_OP_OVERRIDE;
76+
}
77+
ActiveCacheOverride override = new ActiveCacheOverride(this);
78+
if (override.timeToLive() == null) {
79+
// Ensure we don't use stale data on the first access
80+
clear();
81+
}
82+
this.timeToLive = UNLIMITED;
83+
return override;
84+
}
85+
86+
void restore(ActiveCacheOverride override) {
87+
this.timeToLive = override.timeToLive();
88+
this.lastAccessed = override.lastAccessed();
89+
}
90+
6891
/**
6992
* Get a value from the cache, creating it if necessary.
7093
* @param factory a factory used to create the item if there is no reference to it.
@@ -111,4 +134,23 @@ protected void setValue(T value) {
111134
this.value = new SoftReference<>(value);
112135
}
113136

137+
/**
138+
* An active {@link CacheOverride} with a stored time-to-live.
139+
*/
140+
private record ActiveCacheOverride(SoftReferenceConfigurationPropertyCache<?> cache, Duration timeToLive,
141+
Instant lastAccessed, AtomicBoolean active) implements CacheOverride {
142+
143+
ActiveCacheOverride(SoftReferenceConfigurationPropertyCache<?> cache) {
144+
this(cache, cache.timeToLive, cache.lastAccessed, new AtomicBoolean());
145+
}
146+
147+
@Override
148+
public void close() {
149+
if (active().compareAndSet(false, true)) {
150+
this.cache.restore(this);
151+
}
152+
}
153+
154+
}
155+
114156
}

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

+65-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,8 @@
2424

2525
import org.junit.jupiter.api.Test;
2626

27+
import org.springframework.boot.context.properties.source.ConfigurationPropertyCaching.CacheOverride;
28+
2729
import static org.assertj.core.api.Assertions.assertThat;
2830

2931
/**
@@ -97,6 +99,68 @@ void clearExpiresCache() {
9799
get(this.cache).assertCounts(0, 0);
98100
this.cache.clear();
99101
get(this.cache).assertCounts(0, 1);
102+
}
103+
104+
@Test
105+
void overrideWhenNeverExpiresReturnsNoOpOverride() {
106+
TestSoftReferenceConfigurationPropertyCache cache = new TestSoftReferenceConfigurationPropertyCache(true);
107+
assertThat(cache.override()).isSameAs(SoftReferenceConfigurationPropertyCache.NO_OP_OVERRIDE);
108+
}
109+
110+
@Test
111+
void overrideEnablesCaching() {
112+
get(this.cache).assertCounts(0, 0);
113+
get(this.cache).assertCounts(0, 1);
114+
try (CacheOverride override = this.cache.override()) {
115+
get(this.cache).assertCounts(0, 2);
116+
get(this.cache).assertCounts(0, 2);
117+
get(this.cache).assertCounts(0, 2);
118+
}
119+
get(this.cache).assertCounts(0, 3);
120+
}
121+
122+
@Test
123+
void overrideWhenHasExistingTimeToLiveEnablesCaching() {
124+
this.cache.setTimeToLive(Duration.ofHours(1));
125+
get(this.cache).assertCounts(0, 0);
126+
get(this.cache).assertCounts(0, 0);
127+
tick(Duration.ofHours(2));
128+
get(this.cache).assertCounts(0, 1);
129+
try (CacheOverride override = this.cache.override()) {
130+
get(this.cache).assertCounts(0, 1);
131+
tick(Duration.ofHours(2));
132+
get(this.cache).assertCounts(0, 1);
133+
}
134+
get(this.cache).assertCounts(0, 2);
135+
get(this.cache).assertCounts(0, 2);
136+
tick(Duration.ofHours(2));
137+
get(this.cache).assertCounts(0, 3);
138+
}
139+
140+
@Test
141+
void overrideWhenDisabledDoesNotReturnStaleData() {
142+
get(this.cache).assertCounts(0, 0);
143+
get(this.cache).assertCounts(0, 1);
144+
this.cache.disable();
145+
try (CacheOverride override = this.cache.override()) {
146+
get(this.cache).assertCounts(0, 2);
147+
get(this.cache).assertCounts(0, 2);
148+
}
149+
get(this.cache).assertCounts(0, 3);
150+
}
151+
152+
@Test
153+
void overrideCanBeClosedTwiceWithoutIssue() {
154+
get(this.cache).assertCounts(0, 0);
155+
get(this.cache).assertCounts(0, 1);
156+
this.cache.disable();
157+
try (CacheOverride override = this.cache.override()) {
158+
get(this.cache).assertCounts(0, 2);
159+
get(this.cache).assertCounts(0, 2);
160+
override.close();
161+
get(this.cache).assertCounts(0, 3);
162+
}
163+
get(this.cache).assertCounts(0, 4);
100164

101165
}
102166

src/checkstyle/checkstyle-suppressions.xml

+1
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,5 @@
8484
<suppress files="MyContainers\.java" checks="InterfaceIsType" />
8585
<suppress files="SpringBootBanner\.java" checks="SpringLeadingWhitespace" />
8686
<suppress files="LoadTimeWeaverAwareConsumerContainers\.java" checks="InterfaceIsType" />
87+
<suppress files="ConfigurationPropertyCaching\.java" checks="SpringJavadoc" message="\@since"/>
8788
</suppressions>

0 commit comments

Comments
 (0)