Skip to content

Commit cb9cf45

Browse files
committed
Restore previous source in Context.withSource calls
Fix `Context.withSource` is the `Binder` to ensure that the previous source is restored once the callback has completed. This update is especially important in Spring Boot 3.5+ since indexed binding calls `withSource` multiple times. Fixes gh-46039
1 parent 05906cc commit cb9cf45

File tree

2 files changed

+53
-2
lines changed
  • spring-boot-project/spring-boot/src

2 files changed

+53
-2
lines changed

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

Lines changed: 3 additions & 1 deletion
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.
@@ -567,13 +567,15 @@ private <T> T withSource(ConfigurationPropertySource source, Supplier<T> supplie
567567
if (source == null) {
568568
return supplier.get();
569569
}
570+
ConfigurationPropertySource previous = this.source.get(0);
570571
this.source.set(0, source);
571572
this.sourcePushCount++;
572573
try {
573574
return supplier.get();
574575
}
575576
finally {
576577
this.sourcePushCount--;
578+
this.source.set(0, previous);
577579
}
578580
}
579581

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

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 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.
@@ -123,6 +123,41 @@ void bindToCollectionWhenNonSequentialShouldThrowException() {
123123
});
124124
}
125125

126+
@Test
127+
void bindToCollectionWhenNonKnownIndexedChildNotBoundThrowsException() {
128+
// gh-45994
129+
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
130+
source.put("foo[0].first", "Spring");
131+
source.put("foo[0].last", "Boot");
132+
source.put("foo[1].missing", "bad");
133+
this.sources.add(source);
134+
assertThatExceptionOfType(BindException.class)
135+
.isThrownBy(() -> this.binder.bind("foo", Bindable.listOf(Name.class)))
136+
.satisfies((ex) -> {
137+
Set<ConfigurationProperty> unbound = ((UnboundConfigurationPropertiesException) ex.getCause())
138+
.getUnboundProperties();
139+
assertThat(unbound).hasSize(1);
140+
ConfigurationProperty property = unbound.iterator().next();
141+
assertThat(property.getName()).hasToString("foo[1].missing");
142+
assertThat(property.getValue()).isEqualTo("bad");
143+
});
144+
}
145+
146+
@Test
147+
void bindToNestedCollectionWhenNonKnownIndexed() {
148+
// gh-46039
149+
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
150+
source.put("foo[0].items[0]", "a");
151+
source.put("foo[0].items[1]", "b");
152+
source.put("foo[0].string", "test");
153+
this.sources.add(source);
154+
List<ExampleCollectionBean> list = this.binder.bind("foo", Bindable.listOf(ExampleCollectionBean.class)).get();
155+
assertThat(list).hasSize(1);
156+
ExampleCollectionBean bean = list.get(0);
157+
assertThat(bean.getItems()).containsExactly("a", "b", "d");
158+
assertThat(bean.getString()).isEqualTo("test");
159+
}
160+
126161
@Test
127162
void bindToNonScalarCollectionWhenNonSequentialShouldThrowException() {
128163
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
@@ -436,6 +471,8 @@ static class ExampleCollectionBean {
436471

437472
private Set<String> itemsSet = new LinkedHashSet<>();
438473

474+
private String string;
475+
439476
List<String> getItems() {
440477
return this.items;
441478
}
@@ -452,6 +489,14 @@ void setItemsSet(Set<String> itemsSet) {
452489
this.itemsSet = itemsSet;
453490
}
454491

492+
String getString() {
493+
return this.string;
494+
}
495+
496+
void setString(String string) {
497+
this.string = string;
498+
}
499+
455500
}
456501

457502
static class ExampleCustomNoDefaultConstructorBean {
@@ -562,4 +607,8 @@ List<EnumSet<ExampleEnum>> getValues() {
562607

563608
}
564609

610+
record Name(String first, String last) {
611+
612+
}
613+
565614
}

0 commit comments

Comments
 (0)