Skip to content

Commit ca799f7

Browse files
committed
Tolerate actuator endpoints with the same id
Closes gh-39249
1 parent c3b710a commit ca799f7

File tree

5 files changed

+142
-9
lines changed

5 files changed

+142
-9
lines changed

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java

+20-7
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-2024 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.
@@ -59,6 +59,7 @@
5959
* @author Kris De Volder
6060
* @author Jonas Keßler
6161
* @author Scott Frederick
62+
* @author Moritz Halbritter
6263
* @since 1.2.0
6364
*/
6465
@SupportedAnnotationTypes({ ConfigurationMetadataAnnotationProcessor.AUTO_CONFIGURATION_ANNOTATION,
@@ -291,18 +292,30 @@ private void processEndpoint(AnnotationMirror annotation, TypeElement element) {
291292
return; // Can't process that endpoint
292293
}
293294
String endpointKey = ItemMetadata.newItemMetadataPrefix("management.endpoint.", endpointId);
294-
Boolean enabledByDefault = (Boolean) elementValues.get("enableByDefault");
295+
boolean enabledByDefault = (boolean) elementValues.getOrDefault("enableByDefault", true);
295296
String type = this.metadataEnv.getTypeUtils().getQualifiedName(element);
296-
this.metadataCollector.add(ItemMetadata.newGroup(endpointKey, type, type, null));
297-
this.metadataCollector.add(ItemMetadata.newProperty(endpointKey, "enabled", Boolean.class.getName(), type, null,
298-
String.format("Whether to enable the %s endpoint.", endpointId),
299-
(enabledByDefault != null) ? enabledByDefault : true, null));
297+
this.metadataCollector.addIfAbsent(ItemMetadata.newGroup(endpointKey, type, type, null));
298+
this.metadataCollector.add(
299+
ItemMetadata.newProperty(endpointKey, "enabled", Boolean.class.getName(), type, null,
300+
"Whether to enable the %s endpoint.".formatted(endpointId), enabledByDefault, null),
301+
(existing) -> checkEnabledValueMatchesExisting(existing, enabledByDefault, type));
300302
if (hasMainReadOperation(element)) {
301-
this.metadataCollector.add(ItemMetadata.newProperty(endpointKey, "cache.time-to-live",
303+
this.metadataCollector.addIfAbsent(ItemMetadata.newProperty(endpointKey, "cache.time-to-live",
302304
Duration.class.getName(), type, null, "Maximum time that a response can be cached.", "0ms", null));
303305
}
304306
}
305307

308+
private void checkEnabledValueMatchesExisting(ItemMetadata existing, boolean enabledByDefault, String sourceType) {
309+
boolean existingDefaultValue = (boolean) existing.getDefaultValue();
310+
if (enabledByDefault == existingDefaultValue) {
311+
return;
312+
}
313+
throw new IllegalStateException(
314+
"Existing property '%s' from type %s has a conflicting value. Existing value: %b, new value from type %s: %b"
315+
.formatted(existing.getName(), existing.getSourceType(), existingDefaultValue, sourceType,
316+
enabledByDefault));
317+
}
318+
306319
private boolean hasMainReadOperation(TypeElement element) {
307320
for (ExecutableElement method : ElementFilter.methodsIn(element.getEnclosedElements())) {
308321
if (this.metadataEnv.getReadOperationAnnotation(method) != null

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataCollector.java

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2024 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.
@@ -20,6 +20,7 @@
2020
import java.util.LinkedHashSet;
2121
import java.util.List;
2222
import java.util.Set;
23+
import java.util.function.Consumer;
2324

2425
import javax.annotation.processing.ProcessingEnvironment;
2526
import javax.annotation.processing.RoundEnvironment;
@@ -35,6 +36,7 @@
3536
*
3637
* @author Andy Wilkinson
3738
* @author Kris De Volder
39+
* @author Moritz Halbritter
3840
* @since 1.2.2
3941
*/
4042
public class MetadataCollector {
@@ -76,6 +78,24 @@ public void add(ItemMetadata metadata) {
7678
this.metadataItems.add(metadata);
7779
}
7880

81+
public void add(ItemMetadata metadata, Consumer<ItemMetadata> onConflict) {
82+
ItemMetadata existing = find(metadata.getName());
83+
if (existing != null) {
84+
onConflict.accept(existing);
85+
return;
86+
}
87+
add(metadata);
88+
}
89+
90+
public boolean addIfAbsent(ItemMetadata metadata) {
91+
ItemMetadata existing = find(metadata.getName());
92+
if (existing != null) {
93+
return false;
94+
}
95+
add(metadata);
96+
return true;
97+
}
98+
7999
public boolean hasSimilarGroup(ItemMetadata metadata) {
80100
if (!metadata.isOfItemType(ItemMetadata.ItemType.GROUP)) {
81101
throw new IllegalStateException("item " + metadata + " must be a group");
@@ -105,6 +125,13 @@ public ConfigurationMetadata getMetadata() {
105125
return metadata;
106126
}
107127

128+
private ItemMetadata find(String name) {
129+
return this.metadataItems.stream()
130+
.filter((candidate) -> name.equals(candidate.getName()))
131+
.findFirst()
132+
.orElse(null);
133+
}
134+
108135
private boolean shouldBeMerged(ItemMetadata itemMetadata) {
109136
String sourceType = itemMetadata.getSourceType();
110137
return (sourceType != null && !deletedInCurrentBuild(sourceType) && !processedInCurrentBuild(sourceType));

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java

+23-1
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-2024 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.
@@ -27,16 +27,20 @@
2727
import org.springframework.boot.configurationsample.endpoint.DisabledEndpoint;
2828
import org.springframework.boot.configurationsample.endpoint.EnabledEndpoint;
2929
import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint;
30+
import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint2;
31+
import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint3;
3032
import org.springframework.boot.configurationsample.endpoint.SpecificEndpoint;
3133
import org.springframework.boot.configurationsample.endpoint.incremental.IncrementalEndpoint;
3234

3335
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
3437

3538
/**
3639
* Metadata generation tests for Actuator endpoints.
3740
*
3841
* @author Stephane Nicoll
3942
* @author Scott Frederick
43+
* @author Moritz Halbritter
4044
*/
4145
class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests {
4246

@@ -148,6 +152,24 @@ void incrementalEndpointBuildEnableSpecificEndpoint() {
148152
assertThat(metadata.getItems()).hasSize(3);
149153
}
150154

155+
@Test
156+
void shouldTolerateEndpointWithSameId() {
157+
ConfigurationMetadata metadata = compile(SimpleEndpoint.class, SimpleEndpoint2.class);
158+
assertThat(metadata).has(Metadata.withGroup("management.endpoint.simple").fromSource(SimpleEndpoint.class));
159+
assertThat(metadata).has(enabledFlag("simple", "simple", true));
160+
assertThat(metadata).has(cacheTtl("simple"));
161+
assertThat(metadata.getItems()).hasSize(3);
162+
}
163+
164+
@Test
165+
void shouldFailIfEndpointWithSameIdButWithConflictingEnabledByDefaultSetting() {
166+
assertThatRuntimeException().isThrownBy(() -> compile(SimpleEndpoint.class, SimpleEndpoint3.class))
167+
.havingRootCause()
168+
.isInstanceOf(IllegalStateException.class)
169+
.withMessage(
170+
"Existing property 'management.endpoint.simple.enabled' from type org.springframework.boot.configurationsample.endpoint.SimpleEndpoint has a conflicting value. Existing value: true, new value from type org.springframework.boot.configurationsample.endpoint.SimpleEndpoint3: false");
171+
}
172+
151173
private Metadata.MetadataItemCondition enabledFlag(String endpointId, String endpointSuffix, Boolean defaultValue) {
152174
return Metadata.withEnabledFlag("management.endpoint." + endpointSuffix + ".enabled")
153175
.withDefaultValue(defaultValue)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.configurationsample.endpoint;
18+
19+
import org.springframework.boot.configurationsample.Endpoint;
20+
import org.springframework.boot.configurationsample.ReadOperation;
21+
22+
/**
23+
* A simple endpoint with no default override, with the same id as {@link SimpleEndpoint}.
24+
*
25+
* @author Moritz Halbritter
26+
*/
27+
@Endpoint(id = "simple")
28+
public class SimpleEndpoint2 {
29+
30+
@ReadOperation
31+
public String invoke() {
32+
return "test";
33+
}
34+
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.configurationsample.endpoint;
18+
19+
import org.springframework.boot.configurationsample.Endpoint;
20+
import org.springframework.boot.configurationsample.ReadOperation;
21+
22+
/**
23+
* A simple endpoint with no default override, with the same id as {@link SimpleEndpoint},
24+
* but not enabled by default.
25+
*
26+
* @author Moritz Halbritter
27+
*/
28+
@Endpoint(id = "simple", enableByDefault = false)
29+
public class SimpleEndpoint3 {
30+
31+
@ReadOperation
32+
public String invoke() {
33+
return "test";
34+
}
35+
36+
}

0 commit comments

Comments
 (0)