Skip to content

Commit f665910

Browse files
committed
Use generic type for binder cache comparisons
Update `JavaBeanBinder` so that previously cached beans are compared using full generic type information. Prior to this commit binding would fail if a class with the same resolved type, but different generics was in the cache. Fixes gh-16821
1 parent 7407b22 commit f665910

File tree

2 files changed

+64
-16
lines changed

2 files changed

+64
-16
lines changed

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

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,16 @@ private static class Bean<T> {
102102

103103
private static Bean<?> cached;
104104

105-
private final Class<?> type;
105+
private final ResolvableType type;
106106

107-
private final ResolvableType resolvableType;
107+
private final Class<?> resolvedType;
108108

109109
private final Map<String, BeanProperty> properties = new LinkedHashMap<>();
110110

111-
Bean(ResolvableType resolvableType, Class<?> type) {
112-
this.resolvableType = resolvableType;
111+
Bean(ResolvableType type, Class<?> resolvedType) {
113112
this.type = type;
114-
putProperties(type);
113+
this.resolvedType = resolvedType;
114+
putProperties(resolvedType);
115115
}
116116

117117
private void putProperties(Class<?> type) {
@@ -155,7 +155,7 @@ private void addMethodIfPossible(Method method, String prefix, int parameterCoun
155155
}
156156

157157
private BeanProperty getBeanProperty(String name) {
158-
return new BeanProperty(name, this.resolvableType);
158+
return new BeanProperty(name, this.type);
159159
}
160160

161161
private void addField(Field field) {
@@ -165,10 +165,6 @@ private void addField(Field field) {
165165
}
166166
}
167167

168-
public Class<?> getType() {
169-
return this.type;
170-
}
171-
172168
public Map<String, BeanProperty> getProperties() {
173169
return this.properties;
174170
}
@@ -181,27 +177,36 @@ public BeanSupplier<T> getSupplier(Bindable<T> target) {
181177
instance = target.getValue().get();
182178
}
183179
if (instance == null) {
184-
instance = (T) BeanUtils.instantiateClass(this.type);
180+
instance = (T) BeanUtils.instantiateClass(this.resolvedType);
185181
}
186182
return instance;
187183
});
188184
}
189185

186+
private boolean isOfDifferentType(ResolvableType targetType) {
187+
if (this.type.hasGenerics() || targetType.hasGenerics()) {
188+
return !this.type.equals(targetType);
189+
}
190+
return this.resolvedType == null
191+
|| !this.resolvedType.equals(targetType.resolve());
192+
}
193+
190194
@SuppressWarnings("unchecked")
191195
public static <T> Bean<T> get(Bindable<T> bindable, boolean canCallGetValue) {
192-
Class<?> type = bindable.getType().resolve(Object.class);
196+
ResolvableType type = bindable.getType();
197+
Class<?> resolvedType = type.resolve(Object.class);
193198
Supplier<T> value = bindable.getValue();
194199
T instance = null;
195200
if (canCallGetValue && value != null) {
196201
instance = value.get();
197-
type = (instance != null) ? instance.getClass() : type;
202+
resolvedType = (instance != null) ? instance.getClass() : resolvedType;
198203
}
199-
if (instance == null && !isInstantiable(type)) {
204+
if (instance == null && !isInstantiable(resolvedType)) {
200205
return null;
201206
}
202207
Bean<?> bean = Bean.cached;
203-
if (bean == null || !type.equals(bean.getType())) {
204-
bean = new Bean<>(bindable.getType(), type);
208+
if (bean == null || bean.isOfDifferentType(type)) {
209+
bean = new Bean<>(type, resolvedType);
205210
cached = bean;
206211
}
207212
return (Bean<T>) bean;

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,19 @@ public void bindToClassShouldIgnoreStaticAccessors() {
492492
assertThat(bean.getCounter()).isEqualTo(42);
493493
}
494494

495+
@Test
496+
public void bindToClassShouldCacheWithGenerics() {
497+
// gh-16821
498+
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
499+
source.put("foo.integers[a].value", "1");
500+
source.put("foo.booleans[b].value", "true");
501+
this.sources.add(source);
502+
ExampleWithGenericMap bean = this.binder
503+
.bind("foo", Bindable.of(ExampleWithGenericMap.class)).get();
504+
assertThat(bean.getIntegers().get("a").getValue()).isEqualTo(1);
505+
assertThat(bean.getBooleans().get("b").getValue()).isEqualTo(true);
506+
}
507+
495508
public static class ExampleValueBean {
496509

497510
private int intValue;
@@ -912,4 +925,34 @@ public void setValue(Class<? extends Throwable> value) {
912925

913926
}
914927

928+
public static class ExampleWithGenericMap {
929+
930+
private final Map<String, GenericValue<Integer>> integers = new LinkedHashMap<>();
931+
932+
private final Map<String, GenericValue<Boolean>> booleans = new LinkedHashMap<>();
933+
934+
public Map<String, GenericValue<Integer>> getIntegers() {
935+
return this.integers;
936+
}
937+
938+
public Map<String, GenericValue<Boolean>> getBooleans() {
939+
return this.booleans;
940+
}
941+
942+
}
943+
944+
public static class GenericValue<T> {
945+
946+
private T value;
947+
948+
public T getValue() {
949+
return this.value;
950+
}
951+
952+
public void setValue(T value) {
953+
this.value = value;
954+
}
955+
956+
}
957+
915958
}

0 commit comments

Comments
 (0)