Skip to content

Commit b7521e2

Browse files
mbhavephilwebb
andcommitted
Auto-configure health web components only if endpoint is exposed over HTTP
Fixes gh-28131 Co-authored-by: Phillip Webb <[email protected]>
1 parent 42d21a8 commit b7521e2

14 files changed

+269
-188
lines changed

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

Lines changed: 0 additions & 124 deletions
This file was deleted.

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,17 @@
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
2424

25+
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
2526
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
2627
import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
2728
import org.springframework.context.annotation.Conditional;
2829
import org.springframework.core.env.Environment;
2930

3031
/**
3132
* {@link Conditional @Conditional} that checks whether an endpoint is available. An
32-
* endpoint is considered available if it is both enabled and exposed. Matches enablement
33-
* according to the endpoints specific {@link Environment} property, falling back to
33+
* 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
3436
* {@code management.endpoints.enabled-by-default} or failing that
3537
* {@link Endpoint#enableByDefault()}. Matches exposure according to any of the
3638
* {@code management.endpoints.web.exposure.<id>} or
@@ -112,4 +114,12 @@
112114
*/
113115
Class<?> endpoint() default Void.class;
114116

117+
/**
118+
* Technologies to check the exposure of the endpoint on while considering it to be
119+
* available.
120+
* @return the technologies to check
121+
* @since 2.6.0
122+
*/
123+
EndpointExposure[] exposure() default {};
124+
115125
}

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

Lines changed: 125 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,34 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.endpoint.condition;
1818

19+
import java.util.Arrays;
20+
import java.util.EnumSet;
1921
import java.util.HashSet;
22+
import java.util.LinkedHashSet;
2023
import java.util.Map;
24+
import java.util.Optional;
2125
import java.util.Set;
2226

27+
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
2328
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter;
24-
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter.DefaultIncludes;
2529
import org.springframework.boot.actuate.endpoint.EndpointId;
2630
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
31+
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
32+
import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
2733
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
2834
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
35+
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
2936
import org.springframework.boot.cloud.CloudPlatform;
37+
import org.springframework.context.annotation.Bean;
3038
import org.springframework.context.annotation.ConditionContext;
39+
import org.springframework.core.annotation.MergedAnnotation;
40+
import org.springframework.core.annotation.MergedAnnotations;
41+
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
3142
import org.springframework.core.env.Environment;
3243
import org.springframework.core.type.AnnotatedTypeMetadata;
44+
import org.springframework.core.type.MethodMetadata;
45+
import org.springframework.util.Assert;
46+
import org.springframework.util.ClassUtils;
3347
import org.springframework.util.ConcurrentReferenceHashMap;
3448

3549
/**
@@ -40,66 +54,140 @@
4054
* @author Phillip Webb
4155
* @see ConditionalOnAvailableEndpoint
4256
*/
43-
class OnAvailableEndpointCondition extends AbstractEndpointCondition {
57+
class OnAvailableEndpointCondition extends SpringBootCondition {
4458

4559
private static final String JMX_ENABLED_KEY = "spring.jmx.enabled";
4660

47-
private static final Map<Environment, Set<Exposure>> exposuresCache = new ConcurrentReferenceHashMap<>();
61+
private static final String ENABLED_BY_DEFAULT_KEY = "management.endpoints.enabled-by-default";
62+
63+
private static final Map<Environment, Set<ExposureFilter>> exposureFiltersCache = new ConcurrentReferenceHashMap<>();
64+
65+
private static final ConcurrentReferenceHashMap<Environment, Optional<Boolean>> enabledByDefaultCache = new ConcurrentReferenceHashMap<>();
4866

4967
@Override
5068
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
51-
ConditionOutcome enablementOutcome = getEnablementOutcome(context, metadata,
52-
ConditionalOnAvailableEndpoint.class);
69+
Environment environment = context.getEnvironment();
70+
MergedAnnotation<ConditionalOnAvailableEndpoint> conditionAnnotation = metadata.getAnnotations()
71+
.get(ConditionalOnAvailableEndpoint.class);
72+
Class<?> target = getTarget(context, metadata, conditionAnnotation);
73+
MergedAnnotation<Endpoint> endpointAnnotation = getEndpointAnnotation(target);
74+
return getMatchOutcome(environment, conditionAnnotation, endpointAnnotation);
75+
}
76+
77+
private Class<?> getTarget(ConditionContext context, AnnotatedTypeMetadata metadata,
78+
MergedAnnotation<ConditionalOnAvailableEndpoint> condition) {
79+
Class<?> target = condition.getClass("endpoint");
80+
if (target != Void.class) {
81+
return target;
82+
}
83+
Assert.state(metadata instanceof MethodMetadata && metadata.isAnnotated(Bean.class.getName()),
84+
"EndpointCondition must be used on @Bean methods when the endpoint is not specified");
85+
MethodMetadata methodMetadata = (MethodMetadata) metadata;
86+
try {
87+
return ClassUtils.forName(methodMetadata.getReturnTypeName(), context.getClassLoader());
88+
}
89+
catch (Throwable ex) {
90+
throw new IllegalStateException("Failed to extract endpoint id for "
91+
+ methodMetadata.getDeclaringClassName() + "." + methodMetadata.getMethodName(), ex);
92+
}
93+
}
94+
95+
protected MergedAnnotation<Endpoint> getEndpointAnnotation(Class<?> target) {
96+
MergedAnnotations annotations = MergedAnnotations.from(target, SearchStrategy.TYPE_HIERARCHY);
97+
MergedAnnotation<Endpoint> endpoint = annotations.get(Endpoint.class);
98+
if (endpoint.isPresent()) {
99+
return endpoint;
100+
}
101+
MergedAnnotation<EndpointExtension> extension = annotations.get(EndpointExtension.class);
102+
Assert.state(extension.isPresent(), "No endpoint is specified and the return type of the @Bean method is "
103+
+ "neither an @Endpoint, nor an @EndpointExtension");
104+
return getEndpointAnnotation(extension.getClass("endpoint"));
105+
}
106+
107+
private ConditionOutcome getMatchOutcome(Environment environment,
108+
MergedAnnotation<ConditionalOnAvailableEndpoint> conditionAnnotation,
109+
MergedAnnotation<Endpoint> endpointAnnotation) {
110+
ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnAvailableEndpoint.class);
111+
EndpointId endpointId = EndpointId.of(environment, endpointAnnotation.getString("id"));
112+
ConditionOutcome enablementOutcome = getEnablementOutcome(environment, endpointAnnotation, endpointId, message);
53113
if (!enablementOutcome.isMatch()) {
54114
return enablementOutcome;
55115
}
56-
ConditionMessage message = enablementOutcome.getConditionMessage();
57-
Environment environment = context.getEnvironment();
58116
if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) {
59-
return new ConditionOutcome(true, message.andCondition(ConditionalOnAvailableEndpoint.class)
60-
.because("application is running on Cloud Foundry"));
117+
return ConditionOutcome.match(message.because("application is running on Cloud Foundry"));
61118
}
62-
EndpointId id = EndpointId.of(environment,
63-
getEndpointAttributes(ConditionalOnAvailableEndpoint.class, context, metadata).getString("id"));
64-
Set<Exposure> exposures = getExposures(environment);
65-
for (Exposure exposure : exposures) {
66-
if (exposure.isExposed(id)) {
67-
return new ConditionOutcome(true,
68-
message.andCondition(ConditionalOnAvailableEndpoint.class)
69-
.because("marked as exposed by a 'management.endpoints." + exposure.getPrefix()
70-
+ ".exposure' property"));
119+
Set<EndpointExposure> exposuresToCheck = getExposuresToCheck(conditionAnnotation);
120+
Set<ExposureFilter> exposureFilters = getExposureFilters(environment);
121+
for (ExposureFilter exposureFilter : exposureFilters) {
122+
if (exposuresToCheck.contains(exposureFilter.getExposure()) && exposureFilter.isExposed(endpointId)) {
123+
return ConditionOutcome.match(message.because("marked as exposed by a 'management.endpoints."
124+
+ exposureFilter.getExposure().name().toLowerCase() + ".exposure' property"));
71125
}
72126
}
73-
return new ConditionOutcome(false, message.andCondition(ConditionalOnAvailableEndpoint.class)
74-
.because("no 'management.endpoints' property marked it as exposed"));
127+
return ConditionOutcome.noMatch(message.because("no 'management.endpoints' property marked it as exposed"));
128+
}
129+
130+
private ConditionOutcome getEnablementOutcome(Environment environment,
131+
MergedAnnotation<Endpoint> endpointAnnotation, EndpointId endpointId, ConditionMessage.Builder message) {
132+
String key = "management.endpoint." + endpointId.toLowerCaseString() + ".enabled";
133+
Boolean userDefinedEnabled = environment.getProperty(key, Boolean.class);
134+
if (userDefinedEnabled != null) {
135+
return new ConditionOutcome(userDefinedEnabled,
136+
message.because("found property " + key + " with value " + userDefinedEnabled));
137+
}
138+
Boolean userDefinedDefault = isEnabledByDefault(environment);
139+
if (userDefinedDefault != null) {
140+
return new ConditionOutcome(userDefinedDefault, message.because(
141+
"no property " + key + " found so using user defined default from " + ENABLED_BY_DEFAULT_KEY));
142+
}
143+
boolean endpointDefault = endpointAnnotation.getBoolean("enableByDefault");
144+
return new ConditionOutcome(endpointDefault,
145+
message.because("no property " + key + " found so using endpoint default of " + endpointDefault));
146+
}
147+
148+
private Boolean isEnabledByDefault(Environment environment) {
149+
Optional<Boolean> enabledByDefault = enabledByDefaultCache.get(environment);
150+
if (enabledByDefault == null) {
151+
enabledByDefault = Optional.ofNullable(environment.getProperty(ENABLED_BY_DEFAULT_KEY, Boolean.class));
152+
enabledByDefaultCache.put(environment, enabledByDefault);
153+
}
154+
return enabledByDefault.orElse(null);
155+
}
156+
157+
private Set<EndpointExposure> getExposuresToCheck(
158+
MergedAnnotation<ConditionalOnAvailableEndpoint> conditionAnnotation) {
159+
EndpointExposure[] exposure = conditionAnnotation.getEnumArray("exposure", EndpointExposure.class);
160+
return (exposure.length == 0) ? EnumSet.allOf(EndpointExposure.class)
161+
: new LinkedHashSet<>(Arrays.asList(exposure));
75162
}
76163

77-
private Set<Exposure> getExposures(Environment environment) {
78-
Set<Exposure> exposures = exposuresCache.get(environment);
79-
if (exposures == null) {
80-
exposures = new HashSet<>(2);
164+
private Set<ExposureFilter> getExposureFilters(Environment environment) {
165+
Set<ExposureFilter> exposureFilters = exposureFiltersCache.get(environment);
166+
if (exposureFilters == null) {
167+
exposureFilters = new HashSet<>(2);
81168
if (environment.getProperty(JMX_ENABLED_KEY, Boolean.class, false)) {
82-
exposures.add(new Exposure(environment, "jmx", DefaultIncludes.JMX));
169+
exposureFilters.add(new ExposureFilter(environment, EndpointExposure.JMX));
83170
}
84-
exposures.add(new Exposure(environment, "web", DefaultIncludes.WEB));
85-
exposuresCache.put(environment, exposures);
171+
exposureFilters.add(new ExposureFilter(environment, EndpointExposure.WEB));
172+
exposureFiltersCache.put(environment, exposureFilters);
86173
}
87-
return exposures;
174+
return exposureFilters;
88175
}
89176

90-
static class Exposure extends IncludeExcludeEndpointFilter<ExposableEndpoint<?>> {
177+
static final class ExposureFilter extends IncludeExcludeEndpointFilter<ExposableEndpoint<?>> {
91178

92-
private final String prefix;
179+
private final EndpointExposure exposure;
93180

94-
@SuppressWarnings({ "rawtypes", "unchecked" })
95-
Exposure(Environment environment, String prefix, DefaultIncludes defaultIncludes) {
96-
super((Class) ExposableEndpoint.class, environment, "management.endpoints." + prefix + ".exposure",
97-
defaultIncludes);
98-
this.prefix = prefix;
181+
@SuppressWarnings({ "unchecked", "rawtypes" })
182+
private ExposureFilter(Environment environment, EndpointExposure exposure) {
183+
super((Class) ExposableEndpoint.class, environment,
184+
"management.endpoints." + exposure.name().toLowerCase() + ".exposure",
185+
exposure.getDefaultIncludes());
186+
this.exposure = exposure;
99187
}
100188

101-
String getPrefix() {
102-
return this.prefix;
189+
EndpointExposure getExposure() {
190+
return this.exposure;
103191
}
104192

105193
boolean isExposed(EndpointId id) {

0 commit comments

Comments
 (0)