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
This commit is contained in:
parent
ca9c3ed96d
commit
189d84d49d
|
|
@ -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<BindMethod, List<DataObjectBinder>> 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> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context,
|
||||
boolean allowRecursiveBinding, boolean create) {
|
||||
try {
|
||||
Bindable<T> 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<T> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ConfigurationPropertyCaching> 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<CacheOverride> overrides = new ArrayList<>();
|
||||
|
||||
void add(ConfigurationPropertyCaching caching) {
|
||||
this.overrides.add(caching.override());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.overrides.forEach(CacheOverride::close);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T> 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<T> 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<T> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,4 +84,5 @@
|
|||
<suppress files="MyContainers\.java" checks="InterfaceIsType" />
|
||||
<suppress files="SpringBootBanner\.java" checks="SpringLeadingWhitespace" />
|
||||
<suppress files="LoadTimeWeaverAwareConsumerContainers\.java" checks="InterfaceIsType" />
|
||||
<suppress files="ConfigurationPropertyCaching\.java" checks="SpringJavadoc" message="\@since"/>
|
||||
</suppressions>
|
||||
|
|
|
|||
Loading…
Reference in New Issue