Skip to content

Commit 54afa69

Browse files
committed
Add Binder cache and use in JavaBeanBinder and ValueObjectBinder
Introduce a general purpose cache in the `Binder` and make use of it in `JavaBeanBinder` and `ValueObjectBinder` to reuse potentially expensive operations. Closes gh-44861
1 parent 189d84d commit 54afa69

File tree

3 files changed

+92
-22
lines changed

3 files changed

+92
-22
lines changed

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

+7
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.springframework.core.env.Environment;
4747
import org.springframework.format.support.DefaultFormattingConversionService;
4848
import org.springframework.util.Assert;
49+
import org.springframework.util.ConcurrentReferenceHashMap;
4950

5051
/**
5152
* A container object which Binds objects from one or more
@@ -70,6 +71,8 @@ public class Binder {
7071

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

74+
private final Map<Object, Object> cache = new ConcurrentReferenceHashMap<>();
75+
7376
private ConfigurationPropertyCaching configurationPropertyCaching;
7477

7578
/**
@@ -635,6 +638,10 @@ BindConverter getConverter() {
635638
return Binder.this.bindConverter;
636639
}
637640

641+
Map<Object, Object> getCache() {
642+
return Binder.this.cache;
643+
}
644+
638645
@Override
639646
public Binder getBinder() {
640647
return Binder.this;

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

+44-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 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.
@@ -28,6 +28,7 @@
2828
import java.util.LinkedHashSet;
2929
import java.util.Map;
3030
import java.util.Set;
31+
import java.util.concurrent.ConcurrentHashMap;
3132
import java.util.function.BiConsumer;
3233
import java.util.function.Function;
3334
import java.util.function.Supplier;
@@ -50,13 +51,16 @@
5051
*/
5152
class JavaBeanBinder implements DataObjectBinder {
5253

54+
private static final String HAS_KNOWN_BINDABLE_PROPERTIES_CACHE = JavaBeanBinder.class.getName()
55+
+ ".HAS_KNOWN_BINDABLE_PROPERTIES_CACHE";
56+
5357
static final JavaBeanBinder INSTANCE = new JavaBeanBinder();
5458

5559
@Override
5660
public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context,
5761
DataObjectPropertyBinder propertyBinder) {
5862
boolean hasKnownBindableProperties = target.getValue() != null && hasKnownBindableProperties(name, context);
59-
Bean<T> bean = Bean.get(target, hasKnownBindableProperties);
63+
Bean<T> bean = Bean.get(target, context, hasKnownBindableProperties);
6064
if (bean == null) {
6165
return null;
6266
}
@@ -73,6 +77,16 @@ public <T> T create(Bindable<T> target, Context context) {
7377
}
7478

7579
private boolean hasKnownBindableProperties(ConfigurationPropertyName name, Context context) {
80+
Map<ConfigurationPropertyName, Boolean> cache = getHasKnownBindablePropertiesCache(context);
81+
Boolean hasKnownBindableProperties = cache.get(name);
82+
if (hasKnownBindableProperties == null) {
83+
hasKnownBindableProperties = computeHasKnownBindableProperties(name, context);
84+
cache.put(name, hasKnownBindableProperties);
85+
}
86+
return hasKnownBindableProperties;
87+
}
88+
89+
private boolean computeHasKnownBindableProperties(ConfigurationPropertyName name, Context context) {
7690
for (ConfigurationPropertySource source : context.getSources()) {
7791
if (source.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT) {
7892
return true;
@@ -81,6 +95,16 @@ private boolean hasKnownBindableProperties(ConfigurationPropertyName name, Conte
8195
return false;
8296
}
8397

98+
@SuppressWarnings("unchecked")
99+
private Map<ConfigurationPropertyName, Boolean> getHasKnownBindablePropertiesCache(Context context) {
100+
Object cache = context.getCache().get(HAS_KNOWN_BINDABLE_PROPERTIES_CACHE);
101+
if (cache == null) {
102+
cache = new ConcurrentHashMap<ConfigurationPropertyName, Boolean>();
103+
context.getCache().put(HAS_KNOWN_BINDABLE_PROPERTIES_CACHE, cache);
104+
}
105+
return (Map<ConfigurationPropertyName, Boolean>) cache;
106+
}
107+
84108
private <T> boolean bind(DataObjectPropertyBinder propertyBinder, Bean<T> bean, BeanSupplier<T> beanSupplier,
85109
Context context) {
86110
boolean bound = false;
@@ -236,8 +260,6 @@ static BeanProperties of(Bindable<?> bindable) {
236260
*/
237261
static class Bean<T> extends BeanProperties {
238262

239-
private static Bean<?> cached;
240-
241263
Bean(ResolvableType type, Class<?> resolvedType) {
242264
super(type, resolvedType);
243265
}
@@ -257,7 +279,7 @@ BeanSupplier<T> getSupplier(Bindable<T> target) {
257279
}
258280

259281
@SuppressWarnings("unchecked")
260-
static <T> Bean<T> get(Bindable<T> bindable, boolean canCallGetValue) {
282+
static <T> Bean<T> get(Bindable<T> bindable, Context context, boolean canCallGetValue) {
261283
ResolvableType type = bindable.getType();
262284
Class<?> resolvedType = type.resolve(Object.class);
263285
Supplier<T> value = bindable.getValue();
@@ -269,14 +291,26 @@ static <T> Bean<T> get(Bindable<T> bindable, boolean canCallGetValue) {
269291
if (instance == null && !isInstantiable(resolvedType)) {
270292
return null;
271293
}
272-
Bean<?> bean = Bean.cached;
273-
if (bean == null || !bean.isOfType(type, resolvedType)) {
294+
Map<CacheKey, Bean<?>> cache = getCache(context);
295+
CacheKey cacheKey = new CacheKey(type, resolvedType);
296+
Bean<?> bean = cache.get(cacheKey);
297+
if (bean == null) {
274298
bean = new Bean<>(type, resolvedType);
275-
cached = bean;
299+
cache.put(cacheKey, bean);
276300
}
277301
return (Bean<T>) bean;
278302
}
279303

304+
@SuppressWarnings("unchecked")
305+
private static Map<CacheKey, Bean<?>> getCache(Context context) {
306+
Map<CacheKey, Bean<?>> cache = (Map<CacheKey, Bean<?>>) context.getCache().get(Bean.class);
307+
if (cache == null) {
308+
cache = new ConcurrentHashMap<>();
309+
context.getCache().put(Bean.class, cache);
310+
}
311+
return cache;
312+
}
313+
280314
private static boolean isInstantiable(Class<?> type) {
281315
if (type.isInterface()) {
282316
return false;
@@ -290,11 +324,8 @@ private static boolean isInstantiable(Class<?> type) {
290324
}
291325
}
292326

293-
private boolean isOfType(ResolvableType type, Class<?> resolvedType) {
294-
if (getType().hasGenerics() || type.hasGenerics()) {
295-
return getType().equals(type);
296-
}
297-
return getResolvedType() != null && getResolvedType().equals(resolvedType);
327+
private record CacheKey(ResolvableType type, Class<?> resolvedType) {
328+
298329
}
299330

300331
}

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

+41-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 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.
@@ -29,6 +29,7 @@
2929
import java.util.List;
3030
import java.util.Map;
3131
import java.util.Optional;
32+
import java.util.concurrent.ConcurrentHashMap;
3233
import java.util.function.Consumer;
3334

3435
import kotlin.reflect.KFunction;
@@ -73,7 +74,7 @@ class ValueObjectBinder implements DataObjectBinder {
7374
@Override
7475
public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Binder.Context context,
7576
DataObjectPropertyBinder propertyBinder) {
76-
ValueObject<T> valueObject = ValueObject.get(target, this.constructorProvider, context, Discoverer.LENIENT);
77+
ValueObject<T> valueObject = ValueObject.get(target, context, this.constructorProvider, Discoverer.LENIENT);
7778
if (valueObject == null) {
7879
return null;
7980
}
@@ -94,7 +95,7 @@ public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Binder.Con
9495

9596
@Override
9697
public <T> T create(Bindable<T> target, Binder.Context context) {
97-
ValueObject<T> valueObject = ValueObject.get(target, this.constructorProvider, context, Discoverer.LENIENT);
98+
ValueObject<T> valueObject = ValueObject.get(target, context, this.constructorProvider, Discoverer.LENIENT);
9899
if (valueObject == null) {
99100
return null;
100101
}
@@ -109,7 +110,7 @@ public <T> T create(Bindable<T> target, Binder.Context context) {
109110
@Override
110111
public <T> void onUnableToCreateInstance(Bindable<T> target, Context context, RuntimeException exception) {
111112
try {
112-
ValueObject.get(target, this.constructorProvider, context, Discoverer.STRICT);
113+
ValueObject.get(target, context, this.constructorProvider, Discoverer.STRICT);
113114
}
114115
catch (Exception ex) {
115116
exception.addSuppressed(ex);
@@ -191,6 +192,8 @@ private boolean isAggregate(Class<?> type) {
191192
*/
192193
private abstract static class ValueObject<T> {
193194

195+
private static final Object NONE = new Object();
196+
194197
private final Constructor<T> constructor;
195198

196199
protected ValueObject(Constructor<T> constructor) {
@@ -204,24 +207,53 @@ T instantiate(List<Object> args) {
204207
abstract List<ConstructorParameter> getConstructorParameters();
205208

206209
@SuppressWarnings("unchecked")
207-
static <T> ValueObject<T> get(Bindable<T> bindable, BindConstructorProvider constructorProvider,
208-
Binder.Context context, ParameterNameDiscoverer parameterNameDiscoverer) {
209-
Class<T> type = (Class<T>) bindable.getType().resolve();
210-
if (type == null || type.isEnum() || Modifier.isAbstract(type.getModifiers())) {
210+
static <T> ValueObject<T> get(Bindable<T> bindable, Binder.Context context,
211+
BindConstructorProvider constructorProvider, ParameterNameDiscoverer parameterNameDiscoverer) {
212+
Class<T> resolvedType = (Class<T>) bindable.getType().resolve();
213+
if (resolvedType == null || resolvedType.isEnum() || Modifier.isAbstract(resolvedType.getModifiers())) {
211214
return null;
212215
}
216+
Map<CacheKey, Object> cache = getCache(context);
217+
CacheKey cacheKey = new CacheKey(bindable, constructorProvider, parameterNameDiscoverer);
218+
Object valueObject = cache.get(cacheKey);
219+
if (valueObject == null) {
220+
valueObject = get(bindable, context, constructorProvider, parameterNameDiscoverer, resolvedType);
221+
cache.put(cacheKey, (valueObject != null) ? valueObject : NONE);
222+
}
223+
return (valueObject != NONE) ? (ValueObject<T>) valueObject : null;
224+
}
225+
226+
@SuppressWarnings("unchecked")
227+
private static <T> ValueObject<T> get(Bindable<T> bindable, Binder.Context context,
228+
BindConstructorProvider constructorProvider, ParameterNameDiscoverer parameterNameDiscoverer,
229+
Class<T> resolvedType) {
213230
Constructor<?> bindConstructor = constructorProvider.getBindConstructor(bindable,
214231
context.isNestedConstructorBinding());
215232
if (bindConstructor == null) {
216233
return null;
217234
}
218-
if (KotlinDetector.isKotlinType(type)) {
235+
if (KotlinDetector.isKotlinType(resolvedType)) {
219236
return KotlinValueObject.get((Constructor<T>) bindConstructor, bindable.getType(),
220237
parameterNameDiscoverer);
221238
}
222239
return DefaultValueObject.get(bindConstructor, bindable.getType(), parameterNameDiscoverer);
223240
}
224241

242+
@SuppressWarnings("unchecked")
243+
private static Map<CacheKey, Object> getCache(Context context) {
244+
Map<CacheKey, Object> cache = (Map<CacheKey, Object>) context.getCache().get(ValueObject.class);
245+
if (cache == null) {
246+
cache = new ConcurrentHashMap<>();
247+
context.getCache().put(ValueObject.class, cache);
248+
}
249+
return cache;
250+
}
251+
252+
private record CacheKey(Bindable<?> bindable, BindConstructorProvider constructorProvider,
253+
ParameterNameDiscoverer parameterNameDiscoverer) {
254+
255+
}
256+
225257
}
226258

227259
/**

0 commit comments

Comments
 (0)