From 6fe696b5cfae75b5098d030ab757bc995e6df949 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 24 Jun 2025 11:31:46 -0700 Subject: [PATCH] Restore binding of system environment using legacy names Remove `Binder` optimization introduced in commit 8f14dca164 so that legacy format names in environment variables can be used again. Fixes gh-46184 --- .../bind/IndexedElementsBinder.java | 13 +++---- .../bind/CollectionBinderTests.java | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java index 571a5d52066..de5890e688e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java @@ -107,7 +107,6 @@ abstract class IndexedElementsBinder extends AggregateBinder { AggregateElementBinder elementBinder, IndexedCollectionSupplier collection, ResolvableType elementType) { Set knownIndexedChildren = Collections.emptySet(); if (source instanceof IterableConfigurationPropertySource iterableSource) { - source = iterableSource.filter(root::isAncestorOf); knownIndexedChildren = getKnownIndexedChildren(iterableSource, root); } for (int i = 0; i < Integer.MAX_VALUE; i++) { @@ -124,10 +123,10 @@ abstract class IndexedElementsBinder extends AggregateBinder { } } - private Set getKnownIndexedChildren(IterableConfigurationPropertySource filteredSource, + private Set getKnownIndexedChildren(IterableConfigurationPropertySource source, ConfigurationPropertyName root) { Set knownIndexedChildren = new HashSet<>(); - for (ConfigurationPropertyName name : filteredSource) { + for (ConfigurationPropertyName name : source.filter(root::isAncestorOf)) { ConfigurationPropertyName choppedName = name.chop(root.getNumberOfElements() + 1); if (choppedName.isLastElementIndexed()) { knownIndexedChildren.add(choppedName.getLastElement(Form.UNIFORM)); @@ -136,17 +135,17 @@ abstract class IndexedElementsBinder extends AggregateBinder { return knownIndexedChildren; } - private void assertNoUnboundChildren(Set unboundIndexedChildren, - IterableConfigurationPropertySource filteredSource, ConfigurationPropertyName root) { + private void assertNoUnboundChildren(Set unboundIndexedChildren, IterableConfigurationPropertySource source, + ConfigurationPropertyName root) { if (unboundIndexedChildren.isEmpty()) { return; } Set unboundProperties = new TreeSet<>(); - for (ConfigurationPropertyName name : filteredSource) { + for (ConfigurationPropertyName name : source.filter(root::isAncestorOf)) { ConfigurationPropertyName choppedName = name.chop(root.getNumberOfElements() + 1); if (choppedName.isLastElementIndexed() && unboundIndexedChildren.contains(choppedName.getLastElement(Form.UNIFORM))) { - unboundProperties.add(filteredSource.getConfigurationProperty(name)); + unboundProperties.add(source.getConfigurationProperty(name)); } } if (!unboundProperties.isEmpty()) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java index de679afe4f7..3b8d79c4406 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/CollectionBinderTests.java @@ -19,9 +19,11 @@ package org.springframework.boot.context.properties.bind; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Test; @@ -33,6 +35,7 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyS import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; import org.springframework.core.ResolvableType; import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.env.SystemEnvironmentPropertySource; import org.springframework.test.context.support.TestPropertySourceUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -465,6 +468,36 @@ class CollectionBinderTests { assertThat(result.getValues().get(0)).containsExactly(ExampleEnum.FOO_BAR, ExampleEnum.BAR_BAZ); } + @Test + void bindToWellFormedSystemEnvironmentVariableProperty() { + // gh-46184 + Map map = new LinkedHashMap<>(); + map.put("FOO_THENAMES_0_FIRST", "spring"); + map.put("FOO_THENAMES_0_LAST", "boot"); + map.put("FOO_THENAMES_1_FIRST", "binding"); + map.put("FOO_THENAMES_1_LAST", "test"); + SystemEnvironmentPropertySource propertySource = new SystemEnvironmentPropertySource( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map); + this.sources.add(ConfigurationPropertySource.from(propertySource)); + BeanWithCamelCaseNameList result = this.binder.bind("foo", BeanWithCamelCaseNameList.class).get(); + assertThat(result.theNames()).containsExactly(new Name("spring", "boot"), new Name("binding", "test")); + } + + @Test + void bindToLegacySystemEnvironmentVariableProperty() { + // gh-46184 + Map map = new LinkedHashMap<>(); + map.put("FOO_THE_NAMES_0_FIRST", "spring"); + map.put("FOO_THE_NAMES_0_LAST", "boot"); + map.put("FOO_THE_NAMES_1_FIRST", "binding"); + map.put("FOO_THE_NAMES_1_LAST", "test"); + SystemEnvironmentPropertySource propertySource = new SystemEnvironmentPropertySource( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map); + this.sources.add(ConfigurationPropertySource.from(propertySource)); + BeanWithCamelCaseNameList result = this.binder.bind("foo", BeanWithCamelCaseNameList.class).get(); + assertThat(result.theNames()).containsExactly(new Name("spring", "boot"), new Name("binding", "test")); + } + static class ExampleCollectionBean { private final List items = new ArrayList<>(); @@ -607,6 +640,10 @@ class CollectionBinderTests { } + record BeanWithCamelCaseNameList(List theNames) { + + } + record Name(String first, String last) { }