Skip to content

Commit 486d0e9

Browse files
committed
Merge branch 'main' into 4.0.x
2 parents 7057388 + ccdd79f commit 486d0e9

File tree

7 files changed

+216
-69
lines changed

7 files changed

+216
-69
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfiguration.java

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,24 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.web.server;
1818

19+
import java.util.Map;
20+
1921
import org.springframework.beans.factory.SmartInitializingSingleton;
2022
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextFactory;
2123
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType;
2224
import org.springframework.boot.autoconfigure.AutoConfiguration;
2325
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
2426
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2527
import org.springframework.boot.context.properties.EnableConfigurationProperties;
28+
import org.springframework.boot.origin.Origin;
29+
import org.springframework.boot.origin.OriginLookup;
2630
import org.springframework.context.annotation.Bean;
2731
import org.springframework.context.annotation.Configuration;
2832
import org.springframework.context.support.AbstractApplicationContext;
2933
import org.springframework.core.Ordered;
3034
import org.springframework.core.env.ConfigurableEnvironment;
35+
import org.springframework.core.env.EnumerablePropertySource;
3136
import org.springframework.core.env.Environment;
32-
import org.springframework.core.env.PropertySource;
3337
import org.springframework.util.Assert;
3438

3539
/**
@@ -84,17 +88,7 @@ private void verifyAddressConfiguration() {
8488
* @param environment the environment
8589
*/
8690
private void addLocalManagementPortPropertyAlias(ConfigurableEnvironment environment) {
87-
environment.getPropertySources().addLast(new PropertySource<>("Management Server") {
88-
89-
@Override
90-
public Object getProperty(String name) {
91-
if ("local.management.port".equals(name)) {
92-
return environment.getProperty("local.server.port");
93-
}
94-
return null;
95-
}
96-
97-
});
91+
environment.getPropertySources().addLast(new LocalManagementPortPropertySource(environment));
9892
}
9993

10094
@Configuration(proxyBeanMethods = false)
@@ -117,4 +111,45 @@ static ChildManagementContextInitializer childManagementContextInitializer(
117111

118112
}
119113

114+
/**
115+
* {@link EnumerablePropertySource} providing {@code local.management.port} support.
116+
*/
117+
static class LocalManagementPortPropertySource extends EnumerablePropertySource<Object>
118+
implements OriginLookup<String> {
119+
120+
private static final Map<String, String> PROPERTY_MAPPINGS = Map.of("local.management.port",
121+
"local.server.port");
122+
123+
private static final String[] PROPERTY_NAMES = PROPERTY_MAPPINGS.keySet().toArray(String[]::new);
124+
125+
private final Environment environment;
126+
127+
LocalManagementPortPropertySource(Environment environment) {
128+
super("Management Server");
129+
this.environment = environment;
130+
}
131+
132+
@Override
133+
public String[] getPropertyNames() {
134+
return PROPERTY_NAMES;
135+
}
136+
137+
@Override
138+
public Object getProperty(String name) {
139+
String mapped = PROPERTY_MAPPINGS.get(name);
140+
return (mapped != null) ? this.environment.getProperty(mapped) : null;
141+
}
142+
143+
@Override
144+
public Origin getOrigin(String key) {
145+
return null;
146+
}
147+
148+
@Override
149+
public boolean isImmutable() {
150+
return true;
151+
}
152+
153+
}
154+
120155
}

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

Lines changed: 36 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.util.Collection;
21-
import java.util.List;
21+
import java.util.Collections;
22+
import java.util.HashSet;
23+
import java.util.Set;
2224
import java.util.TreeSet;
2325
import java.util.function.Supplier;
24-
import java.util.stream.Collectors;
2526

2627
import org.springframework.boot.context.properties.bind.Binder.Context;
2728
import org.springframework.boot.context.properties.source.ConfigurationProperty;
@@ -30,8 +31,6 @@
3031
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
3132
import org.springframework.boot.context.properties.source.IterableConfigurationPropertySource;
3233
import org.springframework.core.ResolvableType;
33-
import org.springframework.util.LinkedMultiValueMap;
34-
import org.springframework.util.MultiValueMap;
3534

3635
/**
3736
* Base class for {@link AggregateBinder AggregateBinders} that read a sequential run of
@@ -106,65 +105,57 @@ private void bindValue(Bindable<?> target, Collection<Object> collection, Resolv
106105

107106
private void bindIndexed(ConfigurationPropertySource source, ConfigurationPropertyName root,
108107
AggregateElementBinder elementBinder, IndexedCollectionSupplier collection, ResolvableType elementType) {
109-
int firstUnboundIndex = 0;
110-
boolean hasBindingGap = false;
108+
Set<String> knownIndexedChildren = Collections.emptySet();
109+
if (source instanceof IterableConfigurationPropertySource iterableSource) {
110+
source = iterableSource.filter(root::isAncestorOf);
111+
knownIndexedChildren = getKnownIndexedChildren(iterableSource, root);
112+
}
111113
for (int i = 0; i < Integer.MAX_VALUE; i++) {
112114
ConfigurationPropertyName name = appendIndex(root, i);
113115
Object value = elementBinder.bind(name, Bindable.of(elementType), source);
114-
if (value != null) {
115-
collection.get().add(value);
116-
hasBindingGap = hasBindingGap || firstUnboundIndex > 0;
117-
continue;
118-
}
119-
firstUnboundIndex = (firstUnboundIndex <= 0) ? i : firstUnboundIndex;
120-
if (i - firstUnboundIndex > 10) {
116+
if (value == null) {
121117
break;
122118
}
119+
knownIndexedChildren.remove(name.getLastElement(Form.UNIFORM));
120+
collection.get().add(value);
123121
}
124-
if (hasBindingGap) {
125-
assertNoUnboundChildren(source, root, firstUnboundIndex);
122+
if (source instanceof IterableConfigurationPropertySource iterableSource) {
123+
assertNoUnboundChildren(knownIndexedChildren, iterableSource, root);
126124
}
127125
}
128126

129-
private ConfigurationPropertyName appendIndex(ConfigurationPropertyName root, int i) {
130-
return root.append((i < INDEXES.length) ? INDEXES[i] : "[" + i + "]");
131-
}
132-
133-
private void assertNoUnboundChildren(ConfigurationPropertySource source, ConfigurationPropertyName root,
134-
int firstUnboundIndex) {
135-
MultiValueMap<String, ConfigurationPropertyName> knownIndexedChildren = getKnownIndexedChildren(source, root);
136-
for (int i = 0; i < firstUnboundIndex; i++) {
137-
ConfigurationPropertyName name = appendIndex(root, i);
138-
knownIndexedChildren.remove(name.getLastElement(Form.UNIFORM));
127+
private Set<String> getKnownIndexedChildren(IterableConfigurationPropertySource filteredSource,
128+
ConfigurationPropertyName root) {
129+
Set<String> knownIndexedChildren = new HashSet<>();
130+
for (ConfigurationPropertyName name : filteredSource) {
131+
ConfigurationPropertyName choppedName = name.chop(root.getNumberOfElements() + 1);
132+
if (choppedName.isLastElementIndexed()) {
133+
knownIndexedChildren.add(choppedName.getLastElement(Form.UNIFORM));
134+
}
139135
}
140-
assertNoUnboundChildren(source, knownIndexedChildren);
136+
return knownIndexedChildren;
141137
}
142138

143-
private MultiValueMap<String, ConfigurationPropertyName> getKnownIndexedChildren(ConfigurationPropertySource source,
144-
ConfigurationPropertyName root) {
145-
MultiValueMap<String, ConfigurationPropertyName> children = new LinkedMultiValueMap<>();
146-
if (!(source instanceof IterableConfigurationPropertySource iterableSource)) {
147-
return children;
139+
private void assertNoUnboundChildren(Set<String> unboundIndexedChildren,
140+
IterableConfigurationPropertySource filteredSource, ConfigurationPropertyName root) {
141+
if (unboundIndexedChildren.isEmpty()) {
142+
return;
148143
}
149-
for (ConfigurationPropertyName name : iterableSource.filter(root::isAncestorOf)) {
144+
Set<ConfigurationProperty> unboundProperties = new TreeSet<>();
145+
for (ConfigurationPropertyName name : filteredSource) {
150146
ConfigurationPropertyName choppedName = name.chop(root.getNumberOfElements() + 1);
151-
if (choppedName.isLastElementIndexed()) {
152-
String key = choppedName.getLastElement(Form.UNIFORM);
153-
children.add(key, name);
147+
if (choppedName.isLastElementIndexed()
148+
&& unboundIndexedChildren.contains(choppedName.getLastElement(Form.UNIFORM))) {
149+
unboundProperties.add(filteredSource.getConfigurationProperty(name));
154150
}
155151
}
156-
return children;
152+
if (!unboundProperties.isEmpty()) {
153+
throw new UnboundConfigurationPropertiesException(unboundProperties);
154+
}
157155
}
158156

159-
private void assertNoUnboundChildren(ConfigurationPropertySource source,
160-
MultiValueMap<String, ConfigurationPropertyName> children) {
161-
if (!children.isEmpty()) {
162-
throw new UnboundConfigurationPropertiesException(children.values()
163-
.stream()
164-
.flatMap(List::stream)
165-
.map(source::getConfigurationProperty)
166-
.collect(Collectors.toCollection(TreeSet::new)));
167-
}
157+
private ConfigurationPropertyName appendIndex(ConfigurationPropertyName root, int i) {
158+
return root.append((i < INDEXES.length) ? INDEXES[i] : "[" + i + "]");
168159
}
169160

170161
private <C> C convert(Object value, ResolvableType type, Annotation... annotations) {

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyState.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,27 @@ static <T> ConfigurationPropertyState search(Iterable<T> source, Predicate<T> pr
6666
return ABSENT;
6767
}
6868

69+
/**
70+
* Search the given iterable using a predicate to determine if content is
71+
* {@link #PRESENT} or {@link #ABSENT}.
72+
* @param <T> the data type
73+
* @param source the source iterable to search
74+
* @param startInclusive the first index to cover
75+
* @param endExclusive index immediately past the last index to cover
76+
* @param predicate the predicate used to test for presence
77+
* @return {@link #PRESENT} if the iterable contains a matching item, otherwise
78+
* {@link #ABSENT}.
79+
*/
80+
static <T> ConfigurationPropertyState search(T[] source, int startInclusive, int endExclusive,
81+
Predicate<T> predicate) {
82+
Assert.notNull(source, "'source' must not be null");
83+
Assert.notNull(predicate, "'predicate' must not be null");
84+
for (int i = startInclusive; i < endExclusive; i++) {
85+
if (predicate.test(source[i])) {
86+
return PRESENT;
87+
}
88+
}
89+
return ABSENT;
90+
}
91+
6992
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/FilteredIterableConfigurationPropertiesSource.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 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.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.context.properties.source;
1818

19+
import java.util.Arrays;
1920
import java.util.function.Predicate;
2021
import java.util.stream.Stream;
2122

@@ -28,13 +29,41 @@
2829
class FilteredIterableConfigurationPropertiesSource extends FilteredConfigurationPropertiesSource
2930
implements IterableConfigurationPropertySource {
3031

32+
private ConfigurationPropertyName[] filteredNames;
33+
34+
private int numerOfFilteredNames;
35+
3136
FilteredIterableConfigurationPropertiesSource(IterableConfigurationPropertySource source,
3237
Predicate<ConfigurationPropertyName> filter) {
3338
super(source, filter);
39+
ConfigurationPropertyName[] filterableNames = getFilterableNames(source);
40+
if (filterableNames != null) {
41+
this.filteredNames = new ConfigurationPropertyName[filterableNames.length];
42+
this.numerOfFilteredNames = 0;
43+
for (ConfigurationPropertyName name : filterableNames) {
44+
if (filter.test(name)) {
45+
this.filteredNames[this.numerOfFilteredNames++] = name;
46+
}
47+
}
48+
}
49+
}
50+
51+
private ConfigurationPropertyName[] getFilterableNames(IterableConfigurationPropertySource source) {
52+
if (source instanceof SpringIterableConfigurationPropertySource springPropertySource
53+
&& springPropertySource.isImmutablePropertySource()) {
54+
return springPropertySource.getConfigurationPropertyNames();
55+
}
56+
if (source instanceof FilteredIterableConfigurationPropertiesSource filteredSource) {
57+
return filteredSource.filteredNames;
58+
}
59+
return null;
3460
}
3561

3662
@Override
3763
public Stream<ConfigurationPropertyName> stream() {
64+
if (this.filteredNames != null) {
65+
return Arrays.stream(this.filteredNames, 0, this.numerOfFilteredNames);
66+
}
3867
return getSource().stream().filter(getFilter());
3968
}
4069

@@ -45,6 +74,10 @@ protected IterableConfigurationPropertySource getSource() {
4574

4675
@Override
4776
public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) {
77+
if (this.filteredNames != null) {
78+
return ConfigurationPropertyState.search(this.filteredNames, 0, this.numerOfFilteredNames,
79+
name::isAncestorOf);
80+
}
4881
return ConfigurationPropertyState.search(this, name::isAncestorOf);
4982
}
5083

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ private boolean ancestorOfCheck(ConfigurationPropertyName name) {
169169
return false;
170170
}
171171

172-
private ConfigurationPropertyName[] getConfigurationPropertyNames() {
172+
ConfigurationPropertyName[] getConfigurationPropertyNames() {
173173
if (!isImmutablePropertySource()) {
174174
return getCache().getConfigurationPropertyNames(getPropertySource().getPropertyNames());
175175
}
@@ -197,7 +197,7 @@ private Cache updateCache(Cache cache) {
197197
return cache;
198198
}
199199

200-
private boolean isImmutablePropertySource() {
200+
boolean isImmutablePropertySource() {
201201
EnumerablePropertySource<?> source = getPropertySource();
202202
if (source instanceof OriginLookup<?> originLookup) {
203203
return originLookup.isImmutable();

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

Lines changed: 25 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,26 @@ 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+
126146
@Test
127147
void bindToNonScalarCollectionWhenNonSequentialShouldThrowException() {
128148
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
@@ -562,4 +582,8 @@ List<EnumSet<ExampleEnum>> getValues() {
562582

563583
}
564584

585+
record Name(String first, String last) {
586+
587+
}
588+
565589
}

0 commit comments

Comments
 (0)