diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java index a4d7e253719..995c8b17d26 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -35,6 +35,7 @@ import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.context.properties.bind.Bindable.BindRestriction; import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyCaching; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; @@ -69,6 +70,8 @@ public class Binder { private final Map> dataObjectBinders; + private ConfigurationPropertyCaching configurationPropertyCaching; + /** * Create a new {@link Binder} instance for the specified sources. A * {@link DefaultFormattingConversionService} will be used for all conversion. @@ -189,6 +192,7 @@ public class Binder { Assert.notNull(source, "'sources' must not contain null elements"); } this.sources = sources; + this.configurationPropertyCaching = ConfigurationPropertyCaching.get(sources); this.placeholdersResolver = (placeholdersResolver != null) ? placeholdersResolver : PlaceholdersResolver.NONE; this.bindConverter = BindConverter.get(conversionServices, propertyEditorInitializer); this.defaultBindHandler = (defaultBindHandler != null) ? defaultBindHandler : BindHandler.DEFAULT; @@ -341,17 +345,19 @@ public class Binder { private T bind(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context, boolean allowRecursiveBinding, boolean create) { - try { - Bindable replacementTarget = handler.onStart(name, target, context); - if (replacementTarget == null) { - return handleBindResult(name, target, handler, context, null, create); + try (ConfigurationPropertyCaching.CacheOverride cacheOverride = this.configurationPropertyCaching.override()) { + try { + Bindable replacementTarget = handler.onStart(name, target, context); + if (replacementTarget == null) { + return handleBindResult(name, target, handler, context, null, create); + } + target = replacementTarget; + Object bound = bindObject(name, target, handler, context, allowRecursiveBinding); + return handleBindResult(name, target, handler, context, bound, create); + } + catch (Exception ex) { + return handleBindError(name, target, handler, context, ex); } - target = replacementTarget; - Object bound = bindObject(name, target, handler, context, allowRecursiveBinding); - return handleBindResult(name, target, handler, context, bound, create); - } - catch (Exception ex) { - return handleBindError(name, target, handler, context, ex); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyCaching.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyCaching.java index e464a9f0b30..b24e5218423 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyCaching.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyCaching.java @@ -51,6 +51,14 @@ public interface ConfigurationPropertyCaching { */ void clear(); + /** + * Override caching to temporarily enable it. Once caching is no longer needed the + * returned {@link CacheOverride} should be closed to restore previous cache settings. + * @return a {@link CacheOverride} + * @since 3.5.0 + */ + CacheOverride override(); + /** * Get for all configuration property sources in the environment. * @param environment the spring environment @@ -107,4 +115,17 @@ public interface ConfigurationPropertyCaching { throw new IllegalStateException("Unable to find cache from configuration property sources"); } + /** + * {@link AutoCloseable} used to control a + * {@link ConfigurationPropertyCaching#override() cache override}. + * + * @since 3.5.0 + */ + interface CacheOverride extends AutoCloseable { + + @Override + void close(); + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesCaching.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesCaching.java index 60df84baecf..dd8785ddb11 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesCaching.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesCaching.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2025 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. @@ -17,6 +17,8 @@ package org.springframework.boot.context.properties.source; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.function.Consumer; /** @@ -53,6 +55,13 @@ class ConfigurationPropertySourcesCaching implements ConfigurationPropertyCachin forEach(ConfigurationPropertyCaching::clear); } + @Override + public CacheOverride override() { + CacheOverrides override = new CacheOverrides(); + forEach(override::add); + return override; + } + private void forEach(Consumer action) { if (this.sources != null) { for (ConfigurationPropertySource source : this.sources) { @@ -64,4 +73,22 @@ class ConfigurationPropertySourcesCaching implements ConfigurationPropertyCachin } } + /** + * Composite {@link CacheOverride}. + */ + private final class CacheOverrides implements CacheOverride { + + private List overrides = new ArrayList<>(); + + void add(ConfigurationPropertyCaching caching) { + this.overrides.add(caching.override()); + } + + @Override + public void close() { + this.overrides.forEach(CacheOverride::close); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCache.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCache.java index d71b35e842f..5d596709e5d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCache.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2025 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. @@ -19,6 +19,7 @@ package org.springframework.boot.context.properties.source; import java.lang.ref.SoftReference; import java.time.Duration; import java.time.Instant; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -33,6 +34,9 @@ class SoftReferenceConfigurationPropertyCache implements ConfigurationPropert private static final Duration UNLIMITED = Duration.ZERO; + static final CacheOverride NO_OP_OVERRIDE = () -> { + }; + private final boolean neverExpire; private volatile Duration timeToLive; @@ -65,6 +69,25 @@ class SoftReferenceConfigurationPropertyCache implements ConfigurationPropert this.lastAccessed = null; } + @Override + public CacheOverride override() { + if (this.neverExpire) { + return NO_OP_OVERRIDE; + } + ActiveCacheOverride override = new ActiveCacheOverride(this); + if (override.timeToLive() == null) { + // Ensure we don't use stale data on the first access + clear(); + } + this.timeToLive = UNLIMITED; + return override; + } + + void restore(ActiveCacheOverride override) { + this.timeToLive = override.timeToLive(); + this.lastAccessed = override.lastAccessed(); + } + /** * Get a value from the cache, creating it if necessary. * @param factory a factory used to create the item if there is no reference to it. @@ -111,4 +134,23 @@ class SoftReferenceConfigurationPropertyCache implements ConfigurationPropert this.value = new SoftReference<>(value); } + /** + * An active {@link CacheOverride} with a stored time-to-live. + */ + private record ActiveCacheOverride(SoftReferenceConfigurationPropertyCache cache, Duration timeToLive, + Instant lastAccessed, AtomicBoolean active) implements CacheOverride { + + ActiveCacheOverride(SoftReferenceConfigurationPropertyCache cache) { + this(cache, cache.timeToLive, cache.lastAccessed, new AtomicBoolean()); + } + + @Override + public void close() { + if (active().compareAndSet(false, true)) { + this.cache.restore(this); + } + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCacheTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCacheTests.java index 24717704b54..f709922869d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCacheTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SoftReferenceConfigurationPropertyCacheTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2025 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. @@ -24,6 +24,8 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; +import org.springframework.boot.context.properties.source.ConfigurationPropertyCaching.CacheOverride; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -97,6 +99,68 @@ class SoftReferenceConfigurationPropertyCacheTests { get(this.cache).assertCounts(0, 0); this.cache.clear(); get(this.cache).assertCounts(0, 1); + } + + @Test + void overrideWhenNeverExpiresReturnsNoOpOverride() { + TestSoftReferenceConfigurationPropertyCache cache = new TestSoftReferenceConfigurationPropertyCache(true); + assertThat(cache.override()).isSameAs(SoftReferenceConfigurationPropertyCache.NO_OP_OVERRIDE); + } + + @Test + void overrideEnablesCaching() { + get(this.cache).assertCounts(0, 0); + get(this.cache).assertCounts(0, 1); + try (CacheOverride override = this.cache.override()) { + get(this.cache).assertCounts(0, 2); + get(this.cache).assertCounts(0, 2); + get(this.cache).assertCounts(0, 2); + } + get(this.cache).assertCounts(0, 3); + } + + @Test + void overrideWhenHasExistingTimeToLiveEnablesCaching() { + this.cache.setTimeToLive(Duration.ofHours(1)); + get(this.cache).assertCounts(0, 0); + get(this.cache).assertCounts(0, 0); + tick(Duration.ofHours(2)); + get(this.cache).assertCounts(0, 1); + try (CacheOverride override = this.cache.override()) { + get(this.cache).assertCounts(0, 1); + tick(Duration.ofHours(2)); + get(this.cache).assertCounts(0, 1); + } + get(this.cache).assertCounts(0, 2); + get(this.cache).assertCounts(0, 2); + tick(Duration.ofHours(2)); + get(this.cache).assertCounts(0, 3); + } + + @Test + void overrideWhenDisabledDoesNotReturnStaleData() { + get(this.cache).assertCounts(0, 0); + get(this.cache).assertCounts(0, 1); + this.cache.disable(); + try (CacheOverride override = this.cache.override()) { + get(this.cache).assertCounts(0, 2); + get(this.cache).assertCounts(0, 2); + } + get(this.cache).assertCounts(0, 3); + } + + @Test + void overrideCanBeClosedTwiceWithoutIssue() { + get(this.cache).assertCounts(0, 0); + get(this.cache).assertCounts(0, 1); + this.cache.disable(); + try (CacheOverride override = this.cache.override()) { + get(this.cache).assertCounts(0, 2); + get(this.cache).assertCounts(0, 2); + override.close(); + get(this.cache).assertCounts(0, 3); + } + get(this.cache).assertCounts(0, 4); } diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index b1d096a8711..6e3f687b89f 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -84,4 +84,5 @@ +