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
This commit is contained in:
@@ -46,6 +46,7 @@ import org.springframework.core.convert.ConverterNotFoundException;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
|
||||
/**
|
||||
* A container object which Binds objects from one or more
|
||||
@@ -70,6 +71,8 @@ public class Binder {
|
||||
|
||||
private final Map<BindMethod, List<DataObjectBinder>> dataObjectBinders;
|
||||
|
||||
private final Map<Object, Object> cache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private ConfigurationPropertyCaching configurationPropertyCaching;
|
||||
|
||||
/**
|
||||
@@ -635,6 +638,10 @@ public class Binder {
|
||||
return Binder.this.bindConverter;
|
||||
}
|
||||
|
||||
Map<Object, Object> getCache() {
|
||||
return Binder.this.cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Binder getBinder() {
|
||||
return Binder.this;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -28,6 +28,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
@@ -50,13 +51,16 @@ import org.springframework.core.ResolvableType;
|
||||
*/
|
||||
class JavaBeanBinder implements DataObjectBinder {
|
||||
|
||||
private static final String HAS_KNOWN_BINDABLE_PROPERTIES_CACHE = JavaBeanBinder.class.getName()
|
||||
+ ".HAS_KNOWN_BINDABLE_PROPERTIES_CACHE";
|
||||
|
||||
static final JavaBeanBinder INSTANCE = new JavaBeanBinder();
|
||||
|
||||
@Override
|
||||
public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context,
|
||||
DataObjectPropertyBinder propertyBinder) {
|
||||
boolean hasKnownBindableProperties = target.getValue() != null && hasKnownBindableProperties(name, context);
|
||||
Bean<T> bean = Bean.get(target, hasKnownBindableProperties);
|
||||
Bean<T> bean = Bean.get(target, context, hasKnownBindableProperties);
|
||||
if (bean == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -73,6 +77,16 @@ class JavaBeanBinder implements DataObjectBinder {
|
||||
}
|
||||
|
||||
private boolean hasKnownBindableProperties(ConfigurationPropertyName name, Context context) {
|
||||
Map<ConfigurationPropertyName, Boolean> cache = getHasKnownBindablePropertiesCache(context);
|
||||
Boolean hasKnownBindableProperties = cache.get(name);
|
||||
if (hasKnownBindableProperties == null) {
|
||||
hasKnownBindableProperties = computeHasKnownBindableProperties(name, context);
|
||||
cache.put(name, hasKnownBindableProperties);
|
||||
}
|
||||
return hasKnownBindableProperties;
|
||||
}
|
||||
|
||||
private boolean computeHasKnownBindableProperties(ConfigurationPropertyName name, Context context) {
|
||||
for (ConfigurationPropertySource source : context.getSources()) {
|
||||
if (source.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT) {
|
||||
return true;
|
||||
@@ -81,6 +95,16 @@ class JavaBeanBinder implements DataObjectBinder {
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<ConfigurationPropertyName, Boolean> getHasKnownBindablePropertiesCache(Context context) {
|
||||
Object cache = context.getCache().get(HAS_KNOWN_BINDABLE_PROPERTIES_CACHE);
|
||||
if (cache == null) {
|
||||
cache = new ConcurrentHashMap<ConfigurationPropertyName, Boolean>();
|
||||
context.getCache().put(HAS_KNOWN_BINDABLE_PROPERTIES_CACHE, cache);
|
||||
}
|
||||
return (Map<ConfigurationPropertyName, Boolean>) cache;
|
||||
}
|
||||
|
||||
private <T> boolean bind(DataObjectPropertyBinder propertyBinder, Bean<T> bean, BeanSupplier<T> beanSupplier,
|
||||
Context context) {
|
||||
boolean bound = false;
|
||||
@@ -236,8 +260,6 @@ class JavaBeanBinder implements DataObjectBinder {
|
||||
*/
|
||||
static class Bean<T> extends BeanProperties {
|
||||
|
||||
private static Bean<?> cached;
|
||||
|
||||
Bean(ResolvableType type, Class<?> resolvedType) {
|
||||
super(type, resolvedType);
|
||||
}
|
||||
@@ -257,7 +279,7 @@ class JavaBeanBinder implements DataObjectBinder {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> Bean<T> get(Bindable<T> bindable, boolean canCallGetValue) {
|
||||
static <T> Bean<T> get(Bindable<T> bindable, Context context, boolean canCallGetValue) {
|
||||
ResolvableType type = bindable.getType();
|
||||
Class<?> resolvedType = type.resolve(Object.class);
|
||||
Supplier<T> value = bindable.getValue();
|
||||
@@ -269,14 +291,26 @@ class JavaBeanBinder implements DataObjectBinder {
|
||||
if (instance == null && !isInstantiable(resolvedType)) {
|
||||
return null;
|
||||
}
|
||||
Bean<?> bean = Bean.cached;
|
||||
if (bean == null || !bean.isOfType(type, resolvedType)) {
|
||||
Map<CacheKey, Bean<?>> cache = getCache(context);
|
||||
CacheKey cacheKey = new CacheKey(type, resolvedType);
|
||||
Bean<?> bean = cache.get(cacheKey);
|
||||
if (bean == null) {
|
||||
bean = new Bean<>(type, resolvedType);
|
||||
cached = bean;
|
||||
cache.put(cacheKey, bean);
|
||||
}
|
||||
return (Bean<T>) bean;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<CacheKey, Bean<?>> getCache(Context context) {
|
||||
Map<CacheKey, Bean<?>> cache = (Map<CacheKey, Bean<?>>) context.getCache().get(Bean.class);
|
||||
if (cache == null) {
|
||||
cache = new ConcurrentHashMap<>();
|
||||
context.getCache().put(Bean.class, cache);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
private static boolean isInstantiable(Class<?> type) {
|
||||
if (type.isInterface()) {
|
||||
return false;
|
||||
@@ -290,11 +324,8 @@ class JavaBeanBinder implements DataObjectBinder {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOfType(ResolvableType type, Class<?> resolvedType) {
|
||||
if (getType().hasGenerics() || type.hasGenerics()) {
|
||||
return getType().equals(type);
|
||||
}
|
||||
return getResolvedType() != null && getResolvedType().equals(resolvedType);
|
||||
private record CacheKey(ResolvableType type, Class<?> resolvedType) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -29,6 +29,7 @@ import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import kotlin.reflect.KFunction;
|
||||
@@ -73,7 +74,7 @@ class ValueObjectBinder implements DataObjectBinder {
|
||||
@Override
|
||||
public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Binder.Context context,
|
||||
DataObjectPropertyBinder propertyBinder) {
|
||||
ValueObject<T> valueObject = ValueObject.get(target, this.constructorProvider, context, Discoverer.LENIENT);
|
||||
ValueObject<T> valueObject = ValueObject.get(target, context, this.constructorProvider, Discoverer.LENIENT);
|
||||
if (valueObject == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -94,7 +95,7 @@ class ValueObjectBinder implements DataObjectBinder {
|
||||
|
||||
@Override
|
||||
public <T> T create(Bindable<T> target, Binder.Context context) {
|
||||
ValueObject<T> valueObject = ValueObject.get(target, this.constructorProvider, context, Discoverer.LENIENT);
|
||||
ValueObject<T> valueObject = ValueObject.get(target, context, this.constructorProvider, Discoverer.LENIENT);
|
||||
if (valueObject == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -109,7 +110,7 @@ class ValueObjectBinder implements DataObjectBinder {
|
||||
@Override
|
||||
public <T> void onUnableToCreateInstance(Bindable<T> target, Context context, RuntimeException exception) {
|
||||
try {
|
||||
ValueObject.get(target, this.constructorProvider, context, Discoverer.STRICT);
|
||||
ValueObject.get(target, context, this.constructorProvider, Discoverer.STRICT);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
exception.addSuppressed(ex);
|
||||
@@ -191,6 +192,8 @@ class ValueObjectBinder implements DataObjectBinder {
|
||||
*/
|
||||
private abstract static class ValueObject<T> {
|
||||
|
||||
private static final Object NONE = new Object();
|
||||
|
||||
private final Constructor<T> constructor;
|
||||
|
||||
protected ValueObject(Constructor<T> constructor) {
|
||||
@@ -204,24 +207,53 @@ class ValueObjectBinder implements DataObjectBinder {
|
||||
abstract List<ConstructorParameter> getConstructorParameters();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> ValueObject<T> get(Bindable<T> bindable, BindConstructorProvider constructorProvider,
|
||||
Binder.Context context, ParameterNameDiscoverer parameterNameDiscoverer) {
|
||||
Class<T> type = (Class<T>) bindable.getType().resolve();
|
||||
if (type == null || type.isEnum() || Modifier.isAbstract(type.getModifiers())) {
|
||||
static <T> ValueObject<T> get(Bindable<T> bindable, Binder.Context context,
|
||||
BindConstructorProvider constructorProvider, ParameterNameDiscoverer parameterNameDiscoverer) {
|
||||
Class<T> resolvedType = (Class<T>) bindable.getType().resolve();
|
||||
if (resolvedType == null || resolvedType.isEnum() || Modifier.isAbstract(resolvedType.getModifiers())) {
|
||||
return null;
|
||||
}
|
||||
Map<CacheKey, Object> cache = getCache(context);
|
||||
CacheKey cacheKey = new CacheKey(bindable, constructorProvider, parameterNameDiscoverer);
|
||||
Object valueObject = cache.get(cacheKey);
|
||||
if (valueObject == null) {
|
||||
valueObject = get(bindable, context, constructorProvider, parameterNameDiscoverer, resolvedType);
|
||||
cache.put(cacheKey, (valueObject != null) ? valueObject : NONE);
|
||||
}
|
||||
return (valueObject != NONE) ? (ValueObject<T>) valueObject : null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> ValueObject<T> get(Bindable<T> bindable, Binder.Context context,
|
||||
BindConstructorProvider constructorProvider, ParameterNameDiscoverer parameterNameDiscoverer,
|
||||
Class<T> resolvedType) {
|
||||
Constructor<?> bindConstructor = constructorProvider.getBindConstructor(bindable,
|
||||
context.isNestedConstructorBinding());
|
||||
if (bindConstructor == null) {
|
||||
return null;
|
||||
}
|
||||
if (KotlinDetector.isKotlinType(type)) {
|
||||
if (KotlinDetector.isKotlinType(resolvedType)) {
|
||||
return KotlinValueObject.get((Constructor<T>) bindConstructor, bindable.getType(),
|
||||
parameterNameDiscoverer);
|
||||
}
|
||||
return DefaultValueObject.get(bindConstructor, bindable.getType(), parameterNameDiscoverer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<CacheKey, Object> getCache(Context context) {
|
||||
Map<CacheKey, Object> cache = (Map<CacheKey, Object>) context.getCache().get(ValueObject.class);
|
||||
if (cache == null) {
|
||||
cache = new ConcurrentHashMap<>();
|
||||
context.getCache().put(ValueObject.class, cache);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
private record CacheKey(Bindable<?> bindable, BindConstructorProvider constructorProvider,
|
||||
ParameterNameDiscoverer parameterNameDiscoverer) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user