Skip to content

Commit 73f71d5

Browse files
wilkinsonaphilwebb
andcommitted
Rework Cloud Foundry actuator support behind a pluggable abstraction
Deprecate `EndpointExposure.CLOUD_FOUNDRY` and introduce an alternative implementation based on a pluggable abstraction. The new `EndpointExposureOutcomeContributor` interface may now be used to influence `@OnAvailableEndpointCondition` exposure results. Several infrastructure beans that previously used the condition have been refactored to always be registered, but tolerate missing endpoints. A new smoke test application has been added that demonstrates how the abstraction can be used by a third-party. Closes gh-41135 Co-authored-by: Phillip Webb <[email protected]>
1 parent cbb7383 commit 73f71d5

File tree

30 files changed

+695
-119
lines changed

30 files changed

+695
-119
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 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.
@@ -52,7 +52,7 @@ public CachesEndpoint cachesEndpoint(Map<String, CacheManager> cacheManagers) {
5252
@Bean
5353
@ConditionalOnMissingBean
5454
@ConditionalOnBean(CachesEndpoint.class)
55-
@ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
55+
@ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB)
5656
public CachesEndpointWebExtension cachesEndpointWebExtension(CachesEndpoint cachesEndpoint) {
5757
return new CachesEndpointWebExtension(cachesEndpoint);
5858
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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.actuate.autoconfigure.cloudfoundry;
18+
19+
import java.util.Set;
20+
21+
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.EndpointExposureOutcomeContributor;
22+
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
23+
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter;
24+
import org.springframework.boot.actuate.endpoint.EndpointId;
25+
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
26+
import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder;
27+
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
28+
import org.springframework.boot.cloud.CloudPlatform;
29+
import org.springframework.core.env.Environment;
30+
31+
/**
32+
* {@link EndpointExposureOutcomeContributor} to expose {@link EndpointExposure#WEB web}
33+
* endpoints for Cloud Foundry.
34+
*
35+
* @author Phillip Webb
36+
*/
37+
class CloudFoundryEndpointExposureOutcomeContributor implements EndpointExposureOutcomeContributor {
38+
39+
private static final String PROPERTY = "management.endpoints.cloud-foundry.exposure";
40+
41+
private final IncludeExcludeEndpointFilter<?> filter;
42+
43+
CloudFoundryEndpointExposureOutcomeContributor(Environment environment) {
44+
this.filter = (!CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) ? null
45+
: new IncludeExcludeEndpointFilter<>(ExposableEndpoint.class, environment, PROPERTY, "*");
46+
}
47+
48+
@Override
49+
public ConditionOutcome getExposureOutcome(EndpointId endpointId, Set<EndpointExposure> exposures,
50+
Builder message) {
51+
if (exposures.contains(EndpointExposure.WEB) && this.filter != null && this.filter.match(endpointId)) {
52+
return ConditionOutcome.match(message.because("marked as exposed by a '" + PROPERTY + "' property"));
53+
}
54+
return null;
55+
}
56+
57+
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 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.
@@ -55,7 +55,7 @@ public ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoi
5555
@Bean
5656
@ConditionalOnMissingBean
5757
@ConditionalOnBean(ConfigurationPropertiesReportEndpoint.class)
58-
@ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
58+
@ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB)
5959
public ConfigurationPropertiesReportEndpointWebExtension configurationPropertiesReportEndpointWebExtension(
6060
ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint,
6161
ConfigurationPropertiesReportEndpointProperties properties) {

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpoint.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 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.
@@ -31,15 +31,18 @@
3131
/**
3232
* {@link Conditional @Conditional} that checks whether an endpoint is available. An
3333
* endpoint is considered available if it is both enabled and exposed on the specified
34-
* technologies. Matches enablement according to the endpoints specific
35-
* {@link Environment} property, falling back to
36-
* {@code management.endpoints.enabled-by-default} or failing that
37-
* {@link Endpoint#enableByDefault()}. Matches exposure according to any of the
38-
* {@code management.endpoints.web.exposure.<id>} or
39-
* {@code management.endpoints.jmx.exposure.<id>} specific properties or failing that to
40-
* whether the application runs on
41-
* {@link org.springframework.boot.cloud.CloudPlatform#CLOUD_FOUNDRY}. Both those
42-
* conditions should match for the endpoint to be considered available.
34+
* technologies.
35+
* <p>
36+
* Matches enablement according to the endpoints specific {@link Environment} property,
37+
* falling back to {@code management.endpoints.enabled-by-default} or failing that
38+
* {@link Endpoint#enableByDefault()}.
39+
* <p>
40+
* Matches exposure according to any of the {@code management.endpoints.web.exposure.<id>}
41+
* or {@code management.endpoints.jmx.exposure.<id>} specific properties or failing that
42+
* to whether any {@link EndpointExposureOutcomeContributor} exposes the endpoint.
43+
* <p>
44+
* Both enablement and exposure conditions should match for the endpoint to be considered
45+
* available.
4346
* <p>
4447
* When placed on a {@code @Bean} method, the endpoint defaults to the return type of the
4548
* factory method:
@@ -97,6 +100,8 @@
97100
*
98101
* @author Brian Clozel
99102
* @author Stephane Nicoll
103+
* @author Andy Wilkinson
104+
* @author Phillip Webb
100105
* @since 2.2.0
101106
* @see Endpoint
102107
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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.actuate.autoconfigure.endpoint.condition;
18+
19+
import java.util.Set;
20+
21+
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
22+
import org.springframework.boot.actuate.endpoint.EndpointId;
23+
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
24+
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
25+
import org.springframework.core.env.Environment;
26+
27+
/**
28+
* Contributor loaded from the {@code spring.factories} file and used by
29+
* {@link ConditionalOnAvailableEndpoint @ConditionalOnAvailableEndpoint} to determine if
30+
* an endpoint is exposed. If any contributor returns a {@link ConditionOutcome#isMatch()
31+
* matching} {@link ConditionOutcome} then the endpoint is considered exposed.
32+
* <p>
33+
* Implementations may declare a constructor that accepts an {@link Environment} argument.
34+
*
35+
* @author Andy Wilkinson
36+
* @author Phillip Webb
37+
* @since 3.4.0
38+
*/
39+
public interface EndpointExposureOutcomeContributor {
40+
41+
/**
42+
* Return if the given endpoint is exposed for the given set of exposure technologies.
43+
* @param endpointId the endpoint ID
44+
* @param exposures the exposure technologies to check
45+
* @param message the condition message builder
46+
* @return a {@link ConditionOutcome#isMatch() matching} {@link ConditionOutcome} if
47+
* the endpoint is exposed or {@code null} if the contributor should not apply
48+
*/
49+
ConditionOutcome getExposureOutcome(EndpointId endpointId, Set<EndpointExposure> exposures,
50+
ConditionMessage.Builder message);
51+
52+
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java

Lines changed: 74 additions & 49 deletions
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.
@@ -17,9 +17,10 @@
1717
package org.springframework.boot.actuate.autoconfigure.endpoint.condition;
1818

1919
import java.util.Arrays;
20+
import java.util.Collection;
2021
import java.util.EnumSet;
21-
import java.util.HashSet;
2222
import java.util.LinkedHashSet;
23+
import java.util.List;
2324
import java.util.Map;
2425
import java.util.Optional;
2526
import java.util.Set;
@@ -31,15 +32,17 @@
3132
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
3233
import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
3334
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
35+
import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder;
3436
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
3537
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
36-
import org.springframework.boot.cloud.CloudPlatform;
3738
import org.springframework.context.annotation.Bean;
3839
import org.springframework.context.annotation.ConditionContext;
3940
import org.springframework.core.annotation.MergedAnnotation;
4041
import org.springframework.core.annotation.MergedAnnotations;
4142
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
4243
import org.springframework.core.env.Environment;
44+
import org.springframework.core.io.support.SpringFactoriesLoader;
45+
import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver;
4346
import org.springframework.core.type.AnnotatedTypeMetadata;
4447
import org.springframework.core.type.MethodMetadata;
4548
import org.springframework.util.Assert;
@@ -52,6 +55,7 @@
5255
* @author Brian Clozel
5356
* @author Stephane Nicoll
5457
* @author Phillip Webb
58+
* @author Andy Wilkinson
5559
* @see ConditionalOnAvailableEndpoint
5660
*/
5761
class OnAvailableEndpointCondition extends SpringBootCondition {
@@ -60,7 +64,7 @@ class OnAvailableEndpointCondition extends SpringBootCondition {
6064

6165
private static final String ENABLED_BY_DEFAULT_KEY = "management.endpoints.enabled-by-default";
6266

63-
private static final Map<Environment, Set<ExposureFilter>> exposureFiltersCache = new ConcurrentReferenceHashMap<>();
67+
private static final Map<Environment, Set<EndpointExposureOutcomeContributor>> exposureOutcomeContributorsCache = new ConcurrentReferenceHashMap<>();
6468

6569
private static final ConcurrentReferenceHashMap<Environment, Optional<Boolean>> enabledByDefaultCache = new ConcurrentReferenceHashMap<>();
6670

@@ -110,18 +114,10 @@ private ConditionOutcome getMatchOutcome(Environment environment,
110114
ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnAvailableEndpoint.class);
111115
EndpointId endpointId = EndpointId.of(environment, endpointAnnotation.getString("id"));
112116
ConditionOutcome enablementOutcome = getEnablementOutcome(environment, endpointAnnotation, endpointId, message);
113-
if (!enablementOutcome.isMatch()) {
114-
return enablementOutcome;
115-
}
116-
Set<EndpointExposure> exposuresToCheck = getExposuresToCheck(conditionAnnotation);
117-
Set<ExposureFilter> exposureFilters = getExposureFilters(environment);
118-
for (ExposureFilter exposureFilter : exposureFilters) {
119-
if (exposuresToCheck.contains(exposureFilter.getExposure()) && exposureFilter.isExposed(endpointId)) {
120-
return ConditionOutcome.match(message.because("marked as exposed by a 'management.endpoints."
121-
+ exposureFilter.getExposure().name().toLowerCase() + ".exposure' property"));
122-
}
123-
}
124-
return ConditionOutcome.noMatch(message.because("no 'management.endpoints' property marked it as exposed"));
117+
ConditionOutcome exposureOutcome = (!enablementOutcome.isMatch()) ? null
118+
: getExposureOutcome(environment, conditionAnnotation, endpointAnnotation, endpointId, message);
119+
return (exposureOutcome != null) ? exposureOutcome
120+
: ConditionOutcome.noMatch(message.because("not enabled or exposed"));
125121
}
126122

127123
private ConditionOutcome getEnablementOutcome(Environment environment,
@@ -148,54 +144,83 @@ private Boolean isEnabledByDefault(Environment environment) {
148144
return enabledByDefault.orElse(null);
149145
}
150146

151-
private Set<EndpointExposure> getExposuresToCheck(
152-
MergedAnnotation<ConditionalOnAvailableEndpoint> conditionAnnotation) {
153-
EndpointExposure[] exposure = conditionAnnotation.getEnumArray("exposure", EndpointExposure.class);
154-
return (exposure.length == 0) ? EnumSet.allOf(EndpointExposure.class)
155-
: new LinkedHashSet<>(Arrays.asList(exposure));
147+
private ConditionOutcome getExposureOutcome(Environment environment,
148+
MergedAnnotation<ConditionalOnAvailableEndpoint> conditionAnnotation,
149+
MergedAnnotation<Endpoint> endpointAnnotation, EndpointId endpointId, Builder message) {
150+
Set<EndpointExposure> exposures = getExposures(conditionAnnotation);
151+
Set<EndpointExposureOutcomeContributor> outcomeContributors = getExposureOutcomeContributors(environment);
152+
for (EndpointExposureOutcomeContributor outcomeContributor : outcomeContributors) {
153+
ConditionOutcome outcome = outcomeContributor.getExposureOutcome(endpointId, exposures, message);
154+
if (outcome != null && outcome.isMatch()) {
155+
return outcome;
156+
}
157+
}
158+
return null;
159+
}
160+
161+
private Set<EndpointExposure> getExposures(MergedAnnotation<ConditionalOnAvailableEndpoint> conditionAnnotation) {
162+
EndpointExposure[] exposures = conditionAnnotation.getEnumArray("exposure", EndpointExposure.class);
163+
return replaceCloudFoundryExposure(
164+
(exposures.length == 0) ? EnumSet.allOf(EndpointExposure.class) : Arrays.asList(exposures));
165+
}
166+
167+
@SuppressWarnings("removal")
168+
private Set<EndpointExposure> replaceCloudFoundryExposure(Collection<EndpointExposure> exposures) {
169+
Set<EndpointExposure> result = EnumSet.copyOf(exposures);
170+
if (result.remove(EndpointExposure.CLOUD_FOUNDRY)) {
171+
result.add(EndpointExposure.WEB);
172+
}
173+
return result;
156174
}
157175

158-
private Set<ExposureFilter> getExposureFilters(Environment environment) {
159-
Set<ExposureFilter> exposureFilters = exposureFiltersCache.get(environment);
160-
if (exposureFilters == null) {
161-
exposureFilters = new HashSet<>(2);
176+
private Set<EndpointExposureOutcomeContributor> getExposureOutcomeContributors(Environment environment) {
177+
Set<EndpointExposureOutcomeContributor> contributors = exposureOutcomeContributorsCache.get(environment);
178+
if (contributors == null) {
179+
contributors = new LinkedHashSet<>();
180+
contributors.add(new StandardExposureOutcomeContributor(environment, EndpointExposure.WEB));
162181
if (environment.getProperty(JMX_ENABLED_KEY, Boolean.class, false)) {
163-
exposureFilters.add(new ExposureFilter(environment, EndpointExposure.JMX));
164-
}
165-
if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) {
166-
exposureFilters.add(new ExposureFilter(environment, EndpointExposure.CLOUD_FOUNDRY));
182+
contributors.add(new StandardExposureOutcomeContributor(environment, EndpointExposure.JMX));
167183
}
168-
exposureFilters.add(new ExposureFilter(environment, EndpointExposure.WEB));
169-
exposureFiltersCache.put(environment, exposureFilters);
184+
contributors.addAll(loadExposureOutcomeContributors(environment));
185+
exposureOutcomeContributorsCache.put(environment, contributors);
170186
}
171-
return exposureFilters;
187+
return contributors;
172188
}
173189

174-
static final class ExposureFilter extends IncludeExcludeEndpointFilter<ExposableEndpoint<?>> {
190+
private List<EndpointExposureOutcomeContributor> loadExposureOutcomeContributors(Environment environment) {
191+
ArgumentResolver argumentResolver = ArgumentResolver.of(Environment.class, environment);
192+
return SpringFactoriesLoader.forDefaultResourceLocation()
193+
.load(EndpointExposureOutcomeContributor.class, argumentResolver);
194+
}
195+
196+
/**
197+
* Standard {@link EndpointExposureOutcomeContributor}.
198+
*/
199+
private static class StandardExposureOutcomeContributor implements EndpointExposureOutcomeContributor {
175200

176201
private final EndpointExposure exposure;
177202

178-
@SuppressWarnings({ "unchecked", "rawtypes" })
179-
private ExposureFilter(Environment environment, EndpointExposure exposure) {
180-
super((Class) ExposableEndpoint.class, environment,
181-
"management.endpoints." + getCanonicalName(exposure) + ".exposure", exposure.getDefaultIncludes());
182-
this.exposure = exposure;
203+
private final String property;
183204

184-
}
205+
private final IncludeExcludeEndpointFilter<?> filter;
185206

186-
private static String getCanonicalName(EndpointExposure exposure) {
187-
if (EndpointExposure.CLOUD_FOUNDRY.equals(exposure)) {
188-
return "cloud-foundry";
189-
}
190-
return exposure.name().toLowerCase();
191-
}
207+
StandardExposureOutcomeContributor(Environment environment, EndpointExposure exposure) {
208+
this.exposure = exposure;
209+
String name = exposure.name().toLowerCase().replace('_', '-');
210+
this.property = "management.endpoints." + name + ".exposure";
211+
this.filter = new IncludeExcludeEndpointFilter<>(ExposableEndpoint.class, environment, this.property,
212+
exposure.getDefaultIncludes());
192213

193-
EndpointExposure getExposure() {
194-
return this.exposure;
195214
}
196215

197-
boolean isExposed(EndpointId id) {
198-
return super.match(id);
216+
@Override
217+
public ConditionOutcome getExposureOutcome(EndpointId endpointId, Set<EndpointExposure> exposures,
218+
ConditionMessage.Builder message) {
219+
if (exposures.contains(this.exposure) && this.filter.match(endpointId)) {
220+
return ConditionOutcome
221+
.match(message.because("marked as exposed by a '" + this.property + "' property"));
222+
}
223+
return null;
199224
}
200225

201226
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/EndpointExposure.java

Lines changed: 4 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-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.
@@ -37,7 +37,10 @@ public enum EndpointExposure {
3737
/**
3838
* Exposed on Cloud Foundry over `/cloudfoundryapplication`.
3939
* @since 2.6.4
40+
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of using
41+
* {@link EndpointExposure#WEB}
4042
*/
43+
@Deprecated(since = "3.4.0", forRemoval = true)
4144
CLOUD_FOUNDRY("*");
4245

4346
private final String[] defaultIncludes;

0 commit comments

Comments
 (0)