Skip to content

Commit c66781a

Browse files
committed
Set using collection copies when possible
Update `Map` and `Collection` binders to create a copy of the existing collection whenever possible. Prior to this commit the binder would always mutate the existing value and then call the setter with the same instance. This could cause issues if the setter expected a different instance. Fixes gh-12322
1 parent 6e2ecb8 commit c66781a

File tree

4 files changed

+131
-20
lines changed

4 files changed

+131
-20
lines changed

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,27 @@ protected Collection<Object> merge(Supplier<?> existing,
6565
try {
6666
existingCollection.clear();
6767
existingCollection.addAll(additional);
68-
return existingCollection;
68+
return copyIfPossible(existingCollection);
6969
}
7070
catch (UnsupportedOperationException ex) {
7171
return createNewCollection(additional);
7272
}
7373
}
7474

75-
private Collection<Object> createNewCollection(Collection<Object> additional) {
76-
Collection<Object> merged = CollectionFactory
77-
.createCollection(additional.getClass(), additional.size());
78-
merged.addAll(additional);
79-
return merged;
75+
private Collection<Object> copyIfPossible(Collection<Object> collection) {
76+
try {
77+
return createNewCollection(collection);
78+
}
79+
catch (Exception ex) {
80+
return collection;
81+
}
82+
}
83+
84+
private Collection<Object> createNewCollection(Collection<Object> collection) {
85+
Collection<Object> result = CollectionFactory
86+
.createCollection(collection.getClass(), collection.size());
87+
result.addAll(collection);
88+
return result;
8089
}
8190

8291
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,19 @@ protected Map<Object, Object> merge(Supplier<?> existing,
9191
return additional;
9292
}
9393
existingMap.putAll(additional);
94-
return existingMap;
94+
return copyIfPossible(existingMap);
95+
}
96+
97+
private Map<Object, Object> copyIfPossible(Map<Object, Object> map) {
98+
try {
99+
Map<Object, Object> result = CollectionFactory.createMap(map.getClass(),
100+
map.size());
101+
result.putAll(map);
102+
return result;
103+
}
104+
catch (Exception ex) {
105+
return map;
106+
}
95107
}
96108

97109
private class EntryBinder {

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

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ public void bindToCollectionWhenHasExistingCollectionShouldReplaceAllContents()
172172
List<Integer> result = this.binder
173173
.bind("foo", INTEGER_LIST.withExistingValue(existing)).get();
174174
assertThat(result).isExactlyInstanceOf(LinkedList.class);
175-
assertThat(result).isSameAs(existing);
176175
assertThat(result).containsExactly(1);
177176
}
178177

@@ -309,7 +308,20 @@ public void bindToCollectionWithNoDefaultConstructor() {
309308
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
310309
source.put("foo.items", "a,b,c,c");
311310
this.sources.add(source);
312-
ExampleCustomBean result = this.binder.bind("foo", ExampleCustomBean.class).get();
311+
ExampleCustomNoDefaultConstructorBean result = this.binder
312+
.bind("foo", ExampleCustomNoDefaultConstructorBean.class).get();
313+
assertThat(result.getItems()).hasSize(4);
314+
assertThat(result.getItems()).containsExactly("a", "b", "c", "c");
315+
}
316+
317+
@Test
318+
public void bindToCollectionWithDefaultConstructor() {
319+
// gh-12322
320+
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
321+
source.put("foo.items", "a,b,c,c");
322+
this.sources.add(source);
323+
ExampleCustomWithDefaultConstructorBean result = this.binder
324+
.bind("foo", ExampleCustomWithDefaultConstructorBean.class).get();
313325
assertThat(result.getItems()).hasSize(4);
314326
assertThat(result.getItems()).containsExactly("a", "b", "c", "c");
315327
}
@@ -415,31 +427,46 @@ public void setItemsSet(Set<String> itemsSet) {
415427
}
416428
}
417429

418-
public static class ExampleCustomBean {
430+
public static class ExampleCustomNoDefaultConstructorBean {
419431

420-
private MyCustomList items = new MyCustomList(Collections.singletonList("foo"));
432+
private MyCustomNoDefaultConstructorList items = new MyCustomNoDefaultConstructorList(
433+
Collections.singletonList("foo"));
421434

422-
public MyCustomList getItems() {
435+
public MyCustomNoDefaultConstructorList getItems() {
423436
return this.items;
424437
}
425438

426-
public void setItems(MyCustomList items) {
439+
public void setItems(MyCustomNoDefaultConstructorList items) {
427440
this.items = items;
428441
}
429-
}
430442

431-
public static class MyCustomList extends ArrayList<String> {
443+
}
432444

433-
private List<String> items;
445+
public static class MyCustomNoDefaultConstructorList extends ArrayList<String> {
434446

435-
public MyCustomList(List<String> items) {
436-
this.items = items;
447+
public MyCustomNoDefaultConstructorList(List<String> items) {
448+
addAll(items);
437449
}
438450

439-
public List<String> getItems() {
451+
}
452+
453+
public static class ExampleCustomWithDefaultConstructorBean {
454+
455+
private MyCustomWithDefaultConstructorList items = new MyCustomWithDefaultConstructorList();
456+
457+
public MyCustomWithDefaultConstructorList getItems() {
440458
return this.items;
441459
}
442460

461+
public void setItems(MyCustomWithDefaultConstructorList items) {
462+
this.items.clear();
463+
this.items.addAll(items);
464+
}
465+
466+
}
467+
468+
public static class MyCustomWithDefaultConstructorList extends ArrayList<String> {
469+
443470
}
444471

445472
public static class BeanWithNestedCollection {

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

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,6 @@ public void bindToMapWhenHasExistingMapShouldReplaceOnlyNewContents() {
261261
.withExistingValue(existing);
262262
Map<String, Integer> result = this.binder.bind("foo", target).get();
263263
assertThat(result).isExactlyInstanceOf(HashMap.class);
264-
assertThat(result).isSameAs(existing);
265264
assertThat(result).hasSize(2);
266265
assertThat(result).containsEntry("bar", 1);
267266
assertThat(result).containsEntry("baz", 1001);
@@ -595,6 +594,27 @@ public void bindToMapWithPropertyEditorForValue() {
595594
assertThat(map).containsExactly(entry("bar", RuntimeException.class));
596595
}
597596

597+
@Test
598+
public void bindToMapWithNoDefaultConstructor() {
599+
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
600+
source.put("foo.items.a", "b");
601+
this.sources.add(source);
602+
ExampleCustomNoDefaultConstructorBean result = this.binder
603+
.bind("foo", ExampleCustomNoDefaultConstructorBean.class).get();
604+
assertThat(result.getItems()).containsOnly(entry("foo", "bar"), entry("a", "b"));
605+
}
606+
607+
@Test
608+
public void bindToMapWithDefaultConstructor() {
609+
// gh-12322
610+
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
611+
source.put("foo.items.a", "b");
612+
this.sources.add(source);
613+
ExampleCustomWithDefaultConstructorBean result = this.binder
614+
.bind("foo", ExampleCustomWithDefaultConstructorBean.class).get();
615+
assertThat(result.getItems()).containsExactly(entry("a", "b"));
616+
}
617+
598618
private <K, V> Bindable<Map<K, V>> getMapBindable(Class<K> keyGeneric,
599619
ResolvableType valueType) {
600620
ResolvableType keyType = ResolvableType.forClass(keyGeneric);
@@ -657,4 +677,47 @@ public Map<String, String> convert(String s) {
657677

658678
}
659679

680+
public static class ExampleCustomNoDefaultConstructorBean {
681+
682+
private MyCustomNoDefaultConstructorList items = new MyCustomNoDefaultConstructorList(
683+
Collections.singletonMap("foo", "bar"));
684+
685+
public MyCustomNoDefaultConstructorList getItems() {
686+
return this.items;
687+
}
688+
689+
public void setItems(MyCustomNoDefaultConstructorList items) {
690+
this.items = items;
691+
}
692+
693+
}
694+
695+
public static class MyCustomNoDefaultConstructorList extends HashMap<String, String> {
696+
697+
public MyCustomNoDefaultConstructorList(Map<String, String> items) {
698+
putAll(items);
699+
}
700+
701+
}
702+
703+
public static class ExampleCustomWithDefaultConstructorBean {
704+
705+
private MyCustomWithDefaultConstructorList items = new MyCustomWithDefaultConstructorList();
706+
707+
public MyCustomWithDefaultConstructorList getItems() {
708+
return this.items;
709+
}
710+
711+
public void setItems(MyCustomWithDefaultConstructorList items) {
712+
this.items.clear();
713+
this.items.putAll(items);
714+
}
715+
716+
}
717+
718+
public static class MyCustomWithDefaultConstructorList
719+
extends HashMap<String, String> {
720+
721+
}
722+
660723
}

0 commit comments

Comments
 (0)