From b7521e2fdac2bfd2d799574227fe066b43cb3496 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Mon, 18 Oct 2021 16:55:41 -0700 Subject: [PATCH] Auto-configure health web components only if endpoint is exposed over HTTP Fixes gh-28131 Co-authored-by: Phillip Webb --- .../condition/AbstractEndpointCondition.java | 124 -------------- .../ConditionalOnAvailableEndpoint.java | 14 +- .../OnAvailableEndpointCondition.java | 162 ++++++++++++++---- .../endpoint/expose/EndpointExposure.java | 51 ++++++ .../expose/IncludeExcludeEndpointFilter.java | 48 ++++-- .../jmx/JmxEndpointAutoConfiguration.java | 3 +- .../web/WebEndpointAutoConfiguration.java | 4 +- ...ndpointManagementContextConfiguration.java | 4 +- ...ndpointManagementContextConfiguration.java | 3 + ...ndpointManagementContextConfiguration.java | 3 + ...ointReactiveWebExtensionConfiguration.java | 4 +- ...althEndpointWebExtensionConfiguration.java | 5 +- .../ConditionalOnAvailableEndpointTests.java | 20 +++ ...ndpointAdditionalPathIntegrationTests.java | 12 ++ 14 files changed, 269 insertions(+), 188 deletions(-) delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/AbstractEndpointCondition.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/EndpointExposure.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/AbstractEndpointCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/AbstractEndpointCondition.java deleted file mode 100644 index b8e9a76ecd..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/AbstractEndpointCondition.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.endpoint.condition; - -import java.lang.annotation.Annotation; -import java.util.Map; -import java.util.Optional; - -import org.springframework.boot.actuate.endpoint.EndpointId; -import org.springframework.boot.actuate.endpoint.annotation.Endpoint; -import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension; -import org.springframework.boot.autoconfigure.condition.ConditionMessage; -import org.springframework.boot.autoconfigure.condition.ConditionOutcome; -import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.core.env.Environment; -import org.springframework.core.type.AnnotatedTypeMetadata; -import org.springframework.core.type.MethodMetadata; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.ConcurrentReferenceHashMap; - -/** - * Base class for {@link Endpoint @Endpoint} related {@link SpringBootCondition} - * implementations. - * - * @author Stephane Nicoll - * @author Andy Wilkinson - * @author Phillip Webb - */ -abstract class AbstractEndpointCondition extends SpringBootCondition { - - private static final String ENABLED_BY_DEFAULT_KEY = "management.endpoints.enabled-by-default"; - - private static final ConcurrentReferenceHashMap> enabledByDefaultCache = new ConcurrentReferenceHashMap<>(); - - AnnotationAttributes getEndpointAttributes(Class annotationClass, ConditionContext context, - AnnotatedTypeMetadata metadata) { - return getEndpointAttributes(getEndpointType(annotationClass, context, metadata)); - } - - protected ConditionOutcome getEnablementOutcome(ConditionContext context, AnnotatedTypeMetadata metadata, - Class annotationClass) { - Environment environment = context.getEnvironment(); - AnnotationAttributes attributes = getEndpointAttributes(annotationClass, context, metadata); - EndpointId id = EndpointId.of(environment, attributes.getString("id")); - String key = "management.endpoint." + id.toLowerCaseString() + ".enabled"; - Boolean userDefinedEnabled = environment.getProperty(key, Boolean.class); - if (userDefinedEnabled != null) { - return new ConditionOutcome(userDefinedEnabled, ConditionMessage.forCondition(annotationClass) - .because("found property " + key + " with value " + userDefinedEnabled)); - } - Boolean userDefinedDefault = isEnabledByDefault(environment); - if (userDefinedDefault != null) { - return new ConditionOutcome(userDefinedDefault, ConditionMessage.forCondition(annotationClass).because( - "no property " + key + " found so using user defined default from " + ENABLED_BY_DEFAULT_KEY)); - } - boolean endpointDefault = attributes.getBoolean("enableByDefault"); - return new ConditionOutcome(endpointDefault, ConditionMessage.forCondition(annotationClass) - .because("no property " + key + " found so using endpoint default")); - } - - protected Class getEndpointType(Class annotationClass, ConditionContext context, - AnnotatedTypeMetadata metadata) { - Map attributes = metadata.getAnnotationAttributes(annotationClass.getName()); - if (attributes != null && attributes.containsKey("endpoint")) { - Class target = (Class) attributes.get("endpoint"); - if (target != Void.class) { - return target; - } - } - Assert.state(metadata instanceof MethodMetadata && metadata.isAnnotated(Bean.class.getName()), - "EndpointCondition must be used on @Bean methods when the endpoint is not specified"); - MethodMetadata methodMetadata = (MethodMetadata) metadata; - try { - return ClassUtils.forName(methodMetadata.getReturnTypeName(), context.getClassLoader()); - } - catch (Throwable ex) { - throw new IllegalStateException("Failed to extract endpoint id for " - + methodMetadata.getDeclaringClassName() + "." + methodMetadata.getMethodName(), ex); - } - } - - protected AnnotationAttributes getEndpointAttributes(Class type) { - MergedAnnotations annotations = MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY); - MergedAnnotation endpoint = annotations.get(Endpoint.class); - if (endpoint.isPresent()) { - return endpoint.asAnnotationAttributes(); - } - MergedAnnotation extension = annotations.get(EndpointExtension.class); - Assert.state(extension.isPresent(), "No endpoint is specified and the return type of the @Bean method is " - + "neither an @Endpoint, nor an @EndpointExtension"); - return getEndpointAttributes(extension.getClass("endpoint")); - } - - private Boolean isEnabledByDefault(Environment environment) { - Optional enabledByDefault = enabledByDefaultCache.get(environment); - if (enabledByDefault == null) { - enabledByDefault = Optional.ofNullable(environment.getProperty(ENABLED_BY_DEFAULT_KEY, Boolean.class)); - enabledByDefaultCache.put(environment, enabledByDefault); - } - return enabledByDefault.orElse(null); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpoint.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpoint.java index 61d8b93d5b..bb6a2ea820 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpoint.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpoint.java @@ -22,6 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension; import org.springframework.context.annotation.Conditional; @@ -29,8 +30,9 @@ import org.springframework.core.env.Environment; /** * {@link Conditional @Conditional} that checks whether an endpoint is available. An - * endpoint is considered available if it is both enabled and exposed. Matches enablement - * according to the endpoints specific {@link Environment} property, falling back to + * endpoint is considered available if it is both enabled and exposed on the specified + * technologies. Matches enablement according to the endpoints specific + * {@link Environment} property, falling back to * {@code management.endpoints.enabled-by-default} or failing that * {@link Endpoint#enableByDefault()}. Matches exposure according to any of the * {@code management.endpoints.web.exposure.} or @@ -112,4 +114,12 @@ public @interface ConditionalOnAvailableEndpoint { */ Class endpoint() default Void.class; + /** + * Technologies to check the exposure of the endpoint on while considering it to be + * available. + * @return the technologies to check + * @since 2.6.0 + */ + EndpointExposure[] exposure() default {}; + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java index adc73887f0..57dd39535f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java @@ -16,20 +16,34 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.condition; +import java.util.Arrays; +import java.util.EnumSet; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter; -import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter.DefaultIncludes; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; /** @@ -40,66 +54,140 @@ import org.springframework.util.ConcurrentReferenceHashMap; * @author Phillip Webb * @see ConditionalOnAvailableEndpoint */ -class OnAvailableEndpointCondition extends AbstractEndpointCondition { +class OnAvailableEndpointCondition extends SpringBootCondition { private static final String JMX_ENABLED_KEY = "spring.jmx.enabled"; - private static final Map> exposuresCache = new ConcurrentReferenceHashMap<>(); + private static final String ENABLED_BY_DEFAULT_KEY = "management.endpoints.enabled-by-default"; + + private static final Map> exposureFiltersCache = new ConcurrentReferenceHashMap<>(); + + private static final ConcurrentReferenceHashMap> enabledByDefaultCache = new ConcurrentReferenceHashMap<>(); @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - ConditionOutcome enablementOutcome = getEnablementOutcome(context, metadata, - ConditionalOnAvailableEndpoint.class); + Environment environment = context.getEnvironment(); + MergedAnnotation conditionAnnotation = metadata.getAnnotations() + .get(ConditionalOnAvailableEndpoint.class); + Class target = getTarget(context, metadata, conditionAnnotation); + MergedAnnotation endpointAnnotation = getEndpointAnnotation(target); + return getMatchOutcome(environment, conditionAnnotation, endpointAnnotation); + } + + private Class getTarget(ConditionContext context, AnnotatedTypeMetadata metadata, + MergedAnnotation condition) { + Class target = condition.getClass("endpoint"); + if (target != Void.class) { + return target; + } + Assert.state(metadata instanceof MethodMetadata && metadata.isAnnotated(Bean.class.getName()), + "EndpointCondition must be used on @Bean methods when the endpoint is not specified"); + MethodMetadata methodMetadata = (MethodMetadata) metadata; + try { + return ClassUtils.forName(methodMetadata.getReturnTypeName(), context.getClassLoader()); + } + catch (Throwable ex) { + throw new IllegalStateException("Failed to extract endpoint id for " + + methodMetadata.getDeclaringClassName() + "." + methodMetadata.getMethodName(), ex); + } + } + + protected MergedAnnotation getEndpointAnnotation(Class target) { + MergedAnnotations annotations = MergedAnnotations.from(target, SearchStrategy.TYPE_HIERARCHY); + MergedAnnotation endpoint = annotations.get(Endpoint.class); + if (endpoint.isPresent()) { + return endpoint; + } + MergedAnnotation extension = annotations.get(EndpointExtension.class); + Assert.state(extension.isPresent(), "No endpoint is specified and the return type of the @Bean method is " + + "neither an @Endpoint, nor an @EndpointExtension"); + return getEndpointAnnotation(extension.getClass("endpoint")); + } + + private ConditionOutcome getMatchOutcome(Environment environment, + MergedAnnotation conditionAnnotation, + MergedAnnotation endpointAnnotation) { + ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnAvailableEndpoint.class); + EndpointId endpointId = EndpointId.of(environment, endpointAnnotation.getString("id")); + ConditionOutcome enablementOutcome = getEnablementOutcome(environment, endpointAnnotation, endpointId, message); if (!enablementOutcome.isMatch()) { return enablementOutcome; } - ConditionMessage message = enablementOutcome.getConditionMessage(); - Environment environment = context.getEnvironment(); if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) { - return new ConditionOutcome(true, message.andCondition(ConditionalOnAvailableEndpoint.class) - .because("application is running on Cloud Foundry")); + return ConditionOutcome.match(message.because("application is running on Cloud Foundry")); } - EndpointId id = EndpointId.of(environment, - getEndpointAttributes(ConditionalOnAvailableEndpoint.class, context, metadata).getString("id")); - Set exposures = getExposures(environment); - for (Exposure exposure : exposures) { - if (exposure.isExposed(id)) { - return new ConditionOutcome(true, - message.andCondition(ConditionalOnAvailableEndpoint.class) - .because("marked as exposed by a 'management.endpoints." + exposure.getPrefix() - + ".exposure' property")); + Set exposuresToCheck = getExposuresToCheck(conditionAnnotation); + Set exposureFilters = getExposureFilters(environment); + for (ExposureFilter exposureFilter : exposureFilters) { + if (exposuresToCheck.contains(exposureFilter.getExposure()) && exposureFilter.isExposed(endpointId)) { + return ConditionOutcome.match(message.because("marked as exposed by a 'management.endpoints." + + exposureFilter.getExposure().name().toLowerCase() + ".exposure' property")); } } - return new ConditionOutcome(false, message.andCondition(ConditionalOnAvailableEndpoint.class) - .because("no 'management.endpoints' property marked it as exposed")); + return ConditionOutcome.noMatch(message.because("no 'management.endpoints' property marked it as exposed")); } - private Set getExposures(Environment environment) { - Set exposures = exposuresCache.get(environment); - if (exposures == null) { - exposures = new HashSet<>(2); + private ConditionOutcome getEnablementOutcome(Environment environment, + MergedAnnotation endpointAnnotation, EndpointId endpointId, ConditionMessage.Builder message) { + String key = "management.endpoint." + endpointId.toLowerCaseString() + ".enabled"; + Boolean userDefinedEnabled = environment.getProperty(key, Boolean.class); + if (userDefinedEnabled != null) { + return new ConditionOutcome(userDefinedEnabled, + message.because("found property " + key + " with value " + userDefinedEnabled)); + } + Boolean userDefinedDefault = isEnabledByDefault(environment); + if (userDefinedDefault != null) { + return new ConditionOutcome(userDefinedDefault, message.because( + "no property " + key + " found so using user defined default from " + ENABLED_BY_DEFAULT_KEY)); + } + boolean endpointDefault = endpointAnnotation.getBoolean("enableByDefault"); + return new ConditionOutcome(endpointDefault, + message.because("no property " + key + " found so using endpoint default of " + endpointDefault)); + } + + private Boolean isEnabledByDefault(Environment environment) { + Optional enabledByDefault = enabledByDefaultCache.get(environment); + if (enabledByDefault == null) { + enabledByDefault = Optional.ofNullable(environment.getProperty(ENABLED_BY_DEFAULT_KEY, Boolean.class)); + enabledByDefaultCache.put(environment, enabledByDefault); + } + return enabledByDefault.orElse(null); + } + + private Set getExposuresToCheck( + MergedAnnotation conditionAnnotation) { + EndpointExposure[] exposure = conditionAnnotation.getEnumArray("exposure", EndpointExposure.class); + return (exposure.length == 0) ? EnumSet.allOf(EndpointExposure.class) + : new LinkedHashSet<>(Arrays.asList(exposure)); + } + + private Set getExposureFilters(Environment environment) { + Set exposureFilters = exposureFiltersCache.get(environment); + if (exposureFilters == null) { + exposureFilters = new HashSet<>(2); if (environment.getProperty(JMX_ENABLED_KEY, Boolean.class, false)) { - exposures.add(new Exposure(environment, "jmx", DefaultIncludes.JMX)); + exposureFilters.add(new ExposureFilter(environment, EndpointExposure.JMX)); } - exposures.add(new Exposure(environment, "web", DefaultIncludes.WEB)); - exposuresCache.put(environment, exposures); + exposureFilters.add(new ExposureFilter(environment, EndpointExposure.WEB)); + exposureFiltersCache.put(environment, exposureFilters); } - return exposures; + return exposureFilters; } - static class Exposure extends IncludeExcludeEndpointFilter> { + static final class ExposureFilter extends IncludeExcludeEndpointFilter> { - private final String prefix; + private final EndpointExposure exposure; - @SuppressWarnings({ "rawtypes", "unchecked" }) - Exposure(Environment environment, String prefix, DefaultIncludes defaultIncludes) { - super((Class) ExposableEndpoint.class, environment, "management.endpoints." + prefix + ".exposure", - defaultIncludes); - this.prefix = prefix; + @SuppressWarnings({ "unchecked", "rawtypes" }) + private ExposureFilter(Environment environment, EndpointExposure exposure) { + super((Class) ExposableEndpoint.class, environment, + "management.endpoints." + exposure.name().toLowerCase() + ".exposure", + exposure.getDefaultIncludes()); + this.exposure = exposure; } - String getPrefix() { - return this.prefix; + EndpointExposure getExposure() { + return this.exposure; } boolean isExposed(EndpointId id) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/EndpointExposure.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/EndpointExposure.java new file mode 100644 index 0000000000..a678eb7456 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/EndpointExposure.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.expose; + +/** + * Technologies that can be used to expose an endpoint. + * + * @author Phillip Webb + * @since 2.6.0 + */ +public enum EndpointExposure { + + /** + * Exposed via JMX endpoint. + */ + JMX("*"), + + /** + * Exposed via a web endpoint. + */ + WEB("health"); + + private final String[] defaultIncludes; + + EndpointExposure(String... defaultIncludes) { + this.defaultIncludes = defaultIncludes; + } + + /** + * Return the default set of include patterns. + * @return the default includes + */ + public String[] getDefaultIncludes() { + return this.defaultIncludes; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilter.java index 3b71edbf70..48d9f87b24 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilter.java @@ -58,10 +58,13 @@ public class IncludeExcludeEndpointFilter> implem * @param environment the environment containing the properties * @param prefix the property prefix to bind * @param defaultIncludes the default {@code includes} to use when none are specified. + * @deprecated since 2.6.0 for removal in 2.8.0 in favor of + * {@link #IncludeExcludeEndpointFilter(Class, Environment, String, String[])} */ + @Deprecated public IncludeExcludeEndpointFilter(Class endpointType, Environment environment, String prefix, - String... defaultIncludes) { - this(endpointType, environment, prefix, new EndpointPatterns(defaultIncludes)); + DefaultIncludes defaultIncludes) { + this(endpointType, environment, prefix, DefaultIncludes.patterns(defaultIncludes)); } /** @@ -74,21 +77,8 @@ public class IncludeExcludeEndpointFilter> implem * @param defaultIncludes the default {@code includes} to use when none are specified. */ public IncludeExcludeEndpointFilter(Class endpointType, Environment environment, String prefix, - DefaultIncludes defaultIncludes) { - this(endpointType, environment, prefix, DefaultIncludes.patterns(defaultIncludes)); - } - - private IncludeExcludeEndpointFilter(Class endpointType, Environment environment, String prefix, - EndpointPatterns defaultIncludes) { - Assert.notNull(endpointType, "EndpointType must not be null"); - Assert.notNull(environment, "Environment must not be null"); - Assert.hasText(prefix, "Prefix must not be empty"); - Assert.notNull(defaultIncludes, "DefaultIncludes must not be null"); - Binder binder = Binder.get(environment); - this.endpointType = endpointType; - this.include = new EndpointPatterns(bind(binder, prefix + ".include")); - this.defaultIncludes = defaultIncludes; - this.exclude = new EndpointPatterns(bind(binder, prefix + ".exclude")); + String... defaultIncludes) { + this(endpointType, environment, prefix, new EndpointPatterns(defaultIncludes)); } /** @@ -113,12 +103,27 @@ public class IncludeExcludeEndpointFilter> implem * @param include the include patterns * @param exclude the exclude patterns * @param defaultIncludes the default {@code includes} to use when none are specified. + * @deprecated since 2.6.0 for removal in 2.8.0 in favor of + * {@link #IncludeExcludeEndpointFilter(Class, Environment, String, String[])} */ public IncludeExcludeEndpointFilter(Class endpointType, Collection include, Collection exclude, DefaultIncludes defaultIncludes) { this(endpointType, include, exclude, DefaultIncludes.patterns(defaultIncludes)); } + private IncludeExcludeEndpointFilter(Class endpointType, Environment environment, String prefix, + EndpointPatterns defaultIncludes) { + Assert.notNull(endpointType, "EndpointType must not be null"); + Assert.notNull(environment, "Environment must not be null"); + Assert.hasText(prefix, "Prefix must not be empty"); + Assert.notNull(defaultIncludes, "DefaultIncludes must not be null"); + Binder binder = Binder.get(environment); + this.endpointType = endpointType; + this.include = new EndpointPatterns(bind(binder, prefix + ".include")); + this.defaultIncludes = defaultIncludes; + this.exclude = new EndpointPatterns(bind(binder, prefix + ".exclude")); + } + private IncludeExcludeEndpointFilter(Class endpointType, Collection include, Collection exclude, EndpointPatterns defaultIncludes) { Assert.notNull(endpointType, "EndpointType Type must not be null"); @@ -146,8 +151,9 @@ public class IncludeExcludeEndpointFilter> implem * Return {@code true} if the filter matches. * @param endpointId the endpoint ID to check * @return {@code true} if the filter matches + * @since 2.6.0 */ - protected final boolean match(EndpointId endpointId) { + public final boolean match(EndpointId endpointId) { return isIncluded(endpointId) && !isExcluded(endpointId); } @@ -167,7 +173,9 @@ public class IncludeExcludeEndpointFilter> implem /** * Default include patterns that can be used. + * @deprecated since 2.6.0 for removal in 2.8.0 in favor of {@link EndpointExposure}. */ + @Deprecated public enum DefaultIncludes { /** @@ -232,6 +240,10 @@ public class IncludeExcludeEndpointFilter> implem return this.matchesAll || this.endpointIds.contains(endpointId); } + static EndpointPatterns forExposure(EndpointExposure exposure) { + return (exposure != null) ? new EndpointPatterns(exposure.getDefaultIncludes()) : null; + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java index 4615c69d85..35e50b199b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.LazyInitializationExcludeFilter; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter; import org.springframework.boot.actuate.endpoint.EndpointFilter; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; @@ -108,7 +109,7 @@ public class JmxEndpointAutoConfiguration { public IncludeExcludeEndpointFilter jmxIncludeExcludePropertyEndpointFilter() { JmxEndpointProperties.Exposure exposure = this.properties.getExposure(); return new IncludeExcludeEndpointFilter<>(ExposableJmxEndpoint.class, exposure.getInclude(), - exposure.getExclude(), "*"); + exposure.getExclude(), EndpointExposure.JMX.getDefaultIncludes()); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java index 387e82f3d1..ec4a8ae0a8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java @@ -22,8 +22,8 @@ import java.util.stream.Collectors; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter; -import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter.DefaultIncludes; import org.springframework.boot.actuate.endpoint.EndpointFilter; import org.springframework.boot.actuate.endpoint.EndpointsSupplier; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; @@ -116,7 +116,7 @@ public class WebEndpointAutoConfiguration { public IncludeExcludeEndpointFilter webExposeExcludePropertyEndpointFilter() { WebEndpointProperties.Exposure exposure = this.properties.getExposure(); return new IncludeExcludeEndpointFilter<>(ExposableWebEndpoint.class, exposure.getInclude(), - exposure.getExclude(), DefaultIncludes.WEB); + exposure.getExclude(), EndpointExposure.WEB.getDefaultIncludes()); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index c72b50e0bb..d898280c73 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -27,6 +27,8 @@ import java.util.stream.Collectors; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.model.Resource; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.autoconfigure.web.jersey.ManagementContextResourceConfigCustomizer; @@ -86,8 +88,8 @@ class JerseyWebEndpointManagementContextConfiguration { } @Bean - @ConditionalOnBean(HealthEndpoint.class) @ConditionalOnManagementPort(ManagementPortType.DIFFERENT) + @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar jerseyDifferentPortAdditionalHealthEndpointPathsResourcesRegistrar( WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups healthEndpointGroups) { Collection webEndpoints = webEndpointsSupplier.getEndpoints(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index ac2deabe07..6d9d5dc3bc 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; @@ -91,6 +93,7 @@ public class WebFluxEndpointManagementContextConfiguration { @Bean @ConditionalOnManagementPort(ManagementPortType.DIFFERENT) + @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) @ConditionalOnBean(HealthEndpoint.class) public AdditionalHealthEndpointPathsWebFluxHandlerMapping managementHealthEndpointWebFluxHandlerMapping( WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java index 9985b6e6b1..27155ec8f8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; @@ -95,6 +97,7 @@ public class WebMvcEndpointManagementContextConfiguration { @Bean @ConditionalOnManagementPort(ManagementPortType.DIFFERENT) @ConditionalOnBean(HealthEndpoint.class) + @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) public AdditionalHealthEndpointPathsWebMvcHandlerMapping managementHealthEndpointWebMvcHandlerMapping( WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) { Collection webEndpoints = webEndpointsSupplier.getEndpoints(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java index 5836f6d992..6fd895cade 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java @@ -18,6 +18,8 @@ package org.springframework.boot.actuate.autoconfigure.health; import java.util.Collection; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; @@ -43,7 +45,7 @@ import org.springframework.context.annotation.Configuration; */ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.REACTIVE) -@ConditionalOnBean(HealthEndpoint.class) +@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) class HealthEndpointReactiveWebExtensionConfiguration { @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java index 26da1253e3..71e8997f7a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java @@ -27,6 +27,8 @@ import org.glassfish.jersey.server.model.Resource; import org.glassfish.jersey.servlet.ServletContainer; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; @@ -62,7 +64,7 @@ import org.springframework.web.servlet.DispatcherServlet; */ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) -@ConditionalOnBean(HealthEndpoint.class) +@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) class HealthEndpointWebExtensionConfiguration { @Bean @@ -79,7 +81,6 @@ class HealthEndpointWebExtensionConfiguration { .findFirst().get(); } - @Configuration(proxyBeanMethods = false) @ConditionalOnBean(DispatcherServlet.class) static class MvcAdditionalHealthEndpointPathsConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpointTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpointTests.java index 49f18df9d4..530e27f061 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpointTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.condition; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; import org.springframework.boot.actuate.endpoint.EndpointFilter; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; @@ -196,6 +197,14 @@ class ConditionalOnAvailableEndpointTests { .run((context) -> assertThat(context).hasSingleBean(DashedEndpoint.class)); } + @Test + void outcomeWhenEndpointNotExposedOnSpecifiedTechnology() { + this.contextRunner.withUserConfiguration(ExposureEndpointConfiguration.class) + .withPropertyValues("spring.jmx.enabled=true", "management.endpoints.jmx.exposure.include=test", + "management.endpoints.web.exposure.exclude=test") + .run((context) -> assertThat(context).doesNotHaveBean("unexposed")); + } + @Endpoint(id = "health") static class HealthEndpoint { @@ -314,4 +323,15 @@ class ConditionalOnAvailableEndpointTests { } + @Configuration(proxyBeanMethods = false) + static class ExposureEndpointConfiguration { + + @Bean + @ConditionalOnAvailableEndpoint(endpoint = TestEndpoint.class, exposure = EndpointExposure.WEB) + String unexposed() { + return "unexposed"; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java index 0a73565b58..dddc5eaf07 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java @@ -79,6 +79,18 @@ abstract class AbstractHealthEndpointAdditionalPathIntegrationTests client.get().uri("/healthz").accept(MediaType.APPLICATION_JSON) + .exchange().expectStatus().isNotFound(), "local.server.port")); + } + private void testResponse(WebTestClient client) { client.get().uri("/healthz").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody() .jsonPath("status").isEqualTo("UP").jsonPath("components.diskSpace").exists();