From 4ffbe6c9e8881c12f3e619c243a0bc4d0267d0e1 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 6 Mar 2019 22:03:46 +0100 Subject: [PATCH] Add @ConditionalOnExposedEndpoint condition Prior to this commit, Actuator `Endpoint` instantiations would be guarded by `@ConditionalOnEnabledEnpoint` condition annotations. This feature saves resources as disabled endpoints aren't unnecessarily instantiated. By default, only `"health"` and `"info"` endpoints are exposed over the web and all endpoints are exposed over JMX. As of gh-16090, JMX is now disabled by default. This is an opportunity to avoid instantiating endpoints if they won't be exposed at all, which is more likely due to the exposure defaults. This commit adds a new `@ConditionalOnExposedEndpoint` conditional annotation that checks the `Environment` for configuration properties under `"management.endpoints.web.exposure.*"` and `"management.endpoints.jmx.exposure.*"`. In the case of JMX, an additional check is perfomed, checking that JMX is enabled first. The rules implemented in the condition itself are following the ones described in `ExposeExcludePropertyEndpointFilter`. See gh-16093 --- .../condition/AbstractEndpointCondition.java | 87 +++++++ .../ConditionalOnExposedEndpoint.java | 115 +++++++++ .../condition/OnEnabledEndpointCondition.java | 59 +---- .../condition/OnExposedEndpointCondition.java | 145 +++++++++++ .../ConditionalOnExposedEndpointTests.java | 244 ++++++++++++++++++ 5 files changed, 594 insertions(+), 56 deletions(-) create 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/condition/ConditionalOnExposedEndpoint.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnExposedEndpointCondition.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnExposedEndpointTests.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 new file mode 100644 index 0000000000..276086e709 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/AbstractEndpointCondition.java @@ -0,0 +1,87 @@ +/* + * 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 + * + * http://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.util.Map; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Base class for {@link Endpoint} related {@link SpringBootCondition} implementations. + * + * @author Stephane Nicoll + * @author Andy Wilkinson + * @author Phillip Webb + */ +abstract class AbstractEndpointCondition extends SpringBootCondition { + + AnnotationAttributes getEndpointAttributes(Class annotationClass, + ConditionContext context, AnnotatedTypeMetadata metadata) { + return getEndpointAttributes(getEndpointType(annotationClass, context, metadata)); + } + + 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); + } + } + + AnnotationAttributes getEndpointAttributes(Class type) { + AnnotationAttributes attributes = AnnotatedElementUtils + .findMergedAnnotationAttributes(type, Endpoint.class, true, true); + if (attributes != null) { + return attributes; + } + attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(type, + EndpointExtension.class, false, true); + Assert.state(attributes != null, + "No endpoint is specified and the return type of the @Bean method is " + + "neither an @Endpoint, nor an @EndpointExtension"); + return getEndpointAttributes(attributes.getClass("endpoint")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnExposedEndpoint.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnExposedEndpoint.java new file mode 100644 index 0000000000..fff18ff577 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnExposedEndpoint.java @@ -0,0 +1,115 @@ +/* + * 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 + * + * http://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.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension; +import org.springframework.context.annotation.Conditional; +import org.springframework.core.env.Environment; + +/** + * {@link Conditional} that checks whether an endpoint is exposed or not. Matches + * according to the endpoint exposure configuration {@link Environment} properties. This + * is designed as a companion annotation to {@link ConditionalOnEnabledEndpoint}. + *

+ * For a given {@link Endpoint}, the condition will match if: + *

+ * + * When placed on a {@code @Bean} method, the endpoint defaults to the return type of the + * factory method: + * + *
+ * @Configuration
+ * public class MyConfiguration {
+ *
+ *     @ConditionalOnExposedEndpoint
+ *     @Bean
+ *     public MyEndpoint myEndpoint() {
+ *         ...
+ *     }
+ *
+ * }
+ *

+ * It is also possible to use the same mechanism for extensions: + * + *

+ * @Configuration
+ * public class MyConfiguration {
+ *
+ *     @ConditionalOnExposedEndpoint
+ *     @Bean
+ *     public MyEndpointWebExtension myEndpointWebExtension() {
+ *         ...
+ *     }
+ *
+ * }
+ *

+ * In the sample above, {@code MyEndpointWebExtension} will be created if the endpoint is + * enabled as defined by the rules above. {@code MyEndpointWebExtension} must be a regular + * extension that refers to an endpoint, something like: + * + *

+ * @EndpointWebExtension(endpoint = MyEndpoint.class)
+ * public class MyEndpointWebExtension {
+ *
+ * }
+ *

+ * Alternatively, the target endpoint can be manually specified for components that should + * only be created when a given endpoint is enabled: + * + *

+ * @Configuration
+ * public class MyConfiguration {
+ *
+ *     @ConditionalOnExposedEndpoint(endpoint = MyEndpoint.class)
+ *     @Bean
+ *     public MyComponent myComponent() {
+ *         ...
+ *     }
+ *
+ * }
+ * + * @author Brian Clozel + * @since 2.2.0 + * @see Endpoint + * @see ConditionalOnEnabledEndpoint + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Documented +@Conditional(OnExposedEndpointCondition.class) +public @interface ConditionalOnExposedEndpoint { + + /** + * The endpoint type that should be checked. Inferred when the return type of the + * {@code @Bean} method is either an {@link Endpoint} or an {@link EndpointExtension}. + * @return the endpoint type to check + */ + Class endpoint() default Void.class; + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnEnabledEndpointCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnEnabledEndpointCondition.java index 953c93a22a..a80ba2bde9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnEnabledEndpointCondition.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnEnabledEndpointCondition.java @@ -16,24 +16,15 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.condition; -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.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAttributes; 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; /** @@ -44,7 +35,7 @@ import org.springframework.util.ConcurrentReferenceHashMap; * @author Phillip Webb * @see ConditionalOnEnabledEndpoint */ -class OnEnabledEndpointCondition extends SpringBootCondition { +class OnEnabledEndpointCondition extends AbstractEndpointCondition { private static final String ENABLED_BY_DEFAULT_KEY = "management.endpoints.enabled-by-default"; @@ -54,7 +45,8 @@ class OnEnabledEndpointCondition extends SpringBootCondition { public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment environment = context.getEnvironment(); - AnnotationAttributes attributes = getEndpointAttributes(context, metadata); + AnnotationAttributes attributes = getEndpointAttributes( + ConditionalOnEnabledEndpoint.class, context, metadata); EndpointId id = EndpointId.of(attributes.getString("id")); String key = "management.endpoint." + id.toLowerCaseString() + ".enabled"; Boolean userDefinedEnabled = environment.getProperty(key, Boolean.class); @@ -88,49 +80,4 @@ class OnEnabledEndpointCondition extends SpringBootCondition { return enabledByDefault.orElse(null); } - private AnnotationAttributes getEndpointAttributes(ConditionContext context, - AnnotatedTypeMetadata metadata) { - return getEndpointAttributes(getEndpointType(context, metadata)); - } - - private Class getEndpointType(ConditionContext context, - AnnotatedTypeMetadata metadata) { - Map attributes = metadata - .getAnnotationAttributes(ConditionalOnEnabledEndpoint.class.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()), - "OnEnabledEndpointCondition 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) { - AnnotationAttributes attributes = AnnotatedElementUtils - .findMergedAnnotationAttributes(type, Endpoint.class, true, true); - if (attributes != null) { - return attributes; - } - attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(type, - EndpointExtension.class, false, true); - Assert.state(attributes != null, - "No endpoint is specified and the return type of the @Bean method is " - + "neither an @Endpoint, nor an @EndpointExtension"); - return getEndpointAttributes(attributes.getClass("endpoint")); - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnExposedEndpointCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnExposedEndpointCondition.java new file mode 100644 index 0000000000..38fe0db79f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnExposedEndpointCondition.java @@ -0,0 +1,145 @@ +/* + * 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 + * + * http://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.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.boot.actuate.endpoint.EndpointId; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * A condition that checks if an endpoint is exposed. + * + * @author Brian Clozel + * @see ConditionalOnExposedEndpoint + */ +class OnExposedEndpointCondition extends AbstractEndpointCondition { + + private static final String JMX_ENABLED_KEY = "spring.jmx.enabled"; + + private static final ConcurrentReferenceHashMap> endpointExposureCache = new ConcurrentReferenceHashMap<>(); + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + Environment environment = context.getEnvironment(); + if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) { + return new ConditionOutcome(true, + ConditionMessage.forCondition(ConditionalOnExposedEndpoint.class) + .because("application is running on Cloud Foundry")); + } + AnnotationAttributes attributes = getEndpointAttributes( + ConditionalOnExposedEndpoint.class, context, metadata); + EndpointId id = EndpointId.of(attributes.getString("id")); + Set exposureInformations = getExposureInformation( + environment); + for (ExposureInformation exposureInformation : exposureInformations) { + if (exposureInformation.isExposed(id)) { + return new ConditionOutcome(true, + ConditionMessage.forCondition(ConditionalOnExposedEndpoint.class) + .because("marked as exposed by a 'management.endpoints." + + exposureInformation.getPrefix() + + ".exposure' property")); + } + } + return new ConditionOutcome(false, + ConditionMessage.forCondition(ConditionalOnExposedEndpoint.class).because( + "no 'management.endpoints' property marked it as exposed")); + } + + private Set getExposureInformation(Environment environment) { + Set exposureInformations = endpointExposureCache + .get(environment); + if (exposureInformations == null) { + exposureInformations = new HashSet<>(2); + Binder binder = Binder.get(environment); + if (environment.getProperty(JMX_ENABLED_KEY, Boolean.class, false)) { + exposureInformations.add(new ExposureInformation(binder, "jmx", "*")); + } + exposureInformations + .add(new ExposureInformation(binder, "web", "info", "health")); + endpointExposureCache.put(environment, exposureInformations); + } + return exposureInformations; + } + + static class ExposureInformation { + + private final String prefix; + + private final Set include; + + private final Set exclude; + + private final Set exposeDefaults; + + ExposureInformation(Binder binder, String prefix, String... exposeDefaults) { + this.prefix = prefix; + this.include = bind(binder, + "management.endpoints." + prefix + ".exposure.include"); + this.exclude = bind(binder, + "management.endpoints." + prefix + ".exposure.exclude"); + this.exposeDefaults = new HashSet<>(Arrays.asList(exposeDefaults)); + } + + private Set bind(Binder binder, String name) { + List values = binder.bind(name, Bindable.listOf(String.class)) + .orElse(Collections.emptyList()); + Set result = new HashSet<>(values.size()); + for (String value : values) { + result.add("*".equals(value) ? "*" + : EndpointId.fromPropertyValue(value).toLowerCaseString()); + } + return result; + } + + String getPrefix() { + return this.prefix; + } + + boolean isExposed(EndpointId endpointId) { + String id = endpointId.toLowerCaseString(); + if (!this.exclude.isEmpty()) { + if (this.exclude.contains("*") || this.exclude.contains(id)) { + return false; + } + } + if (this.include.isEmpty()) { + if (this.exposeDefaults.contains("*") + || this.exposeDefaults.contains(id)) { + return true; + } + } + return this.include.contains("*") || this.include.contains(id); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnExposedEndpointTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnExposedEndpointTests.java new file mode 100644 index 0000000000..2385d8f3ac --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnExposedEndpointTests.java @@ -0,0 +1,244 @@ +/* + * 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 + * + * http://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 org.junit.Test; + +import org.springframework.boot.actuate.endpoint.EndpointFilter; +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.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConditionalOnExposedEndpoint}. + * + * @author Brian Clozel + */ +public class ConditionalOnExposedEndpointTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(AllEndpointsConfiguration.class); + + @Test + public void outcomeShouldMatchDefaults() { + this.contextRunner.run((context) -> assertThat(context).hasBean("info") + .hasBean("health").doesNotHaveBean("spring").doesNotHaveBean("test")); + } + + @Test + public void outcomeWhenIncludeAllWebShouldMatch() { + this.contextRunner + .withPropertyValues("management.endpoints.web.exposure.include=*") + .run((context) -> assertThat(context).hasBean("info").hasBean("health") + .hasBean("test").hasBean("spring")); + } + + @Test + public void outcomeWhenIncludeAllJmxButJmxDisabledShouldMatchDefaults() { + this.contextRunner + .withPropertyValues("management.endpoints.jmx.exposure.include=*") + .run((context) -> assertThat(context).hasBean("info").hasBean("health") + .doesNotHaveBean("spring").doesNotHaveBean("test")); + } + + @Test + public void outcomeWhenIncludeAllJmxAndJmxEnabledShouldMatch() { + this.contextRunner + .withPropertyValues("management.endpoints.jmx.exposure.include=*", + "spring.jmx.enabled=true") + .run((context) -> assertThat(context).hasBean("info").hasBean("health") + .hasBean("test").hasBean("spring")); + } + + @Test + public void outcomeWhenIncludeAllWebAndExcludeMatchesShouldNotMatch() { + this.contextRunner + .withPropertyValues("management.endpoints.web.exposure.include=*", + "management.endpoints.web.exposure.exclude=spring,info") + .run((context) -> assertThat(context).hasBean("health").hasBean("test") + .doesNotHaveBean("info").doesNotHaveBean("spring")); + } + + @Test + public void outcomeWhenIncludeMatchesAndExcludeMatchesShouldNotMatch() { + this.contextRunner.withPropertyValues( + "management.endpoints.web.exposure.include=info,health,spring,test", + "management.endpoints.web.exposure.exclude=spring,info") + .run((context) -> assertThat(context).hasBean("health").hasBean("test") + .doesNotHaveBean("info").doesNotHaveBean("spring")); + } + + @Test + public void outcomeWhenIncludeMatchesShouldMatch() { + this.contextRunner + .withPropertyValues("management.endpoints.web.exposure.include=spring") + .run((context) -> assertThat(context).hasBean("spring") + .doesNotHaveBean("health").doesNotHaveBean("info") + .doesNotHaveBean("test")); + } + + @Test + public void outcomeWhenIncludeMatchesWithCaseShouldMatch() { + this.contextRunner + .withPropertyValues("management.endpoints.web.exposure.include=sPRing") + .run((context) -> assertThat(context).hasBean("spring") + .doesNotHaveBean("health").doesNotHaveBean("info") + .doesNotHaveBean("test")); + } + + @Test + public void outcomeWhenIncludeMatchesAndExcludeAllShouldNotMatch() { + this.contextRunner.withPropertyValues( + "management.endpoints.web.exposure.include=info,health,spring,test", + "management.endpoints.web.exposure.exclude=*") + .run((context) -> assertThat(context).doesNotHaveBean("health") + .doesNotHaveBean("info").doesNotHaveBean("spring") + .doesNotHaveBean("test")); + } + + @Test + public void outcomeWhenIncludeMatchesShoulMatchWithExtensionsAndComponents() { + this.contextRunner + .withUserConfiguration( + ComponentEnabledIfEndpointIsExposedConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=spring") + .run((context) -> assertThat(context).hasBean("spring") + .hasBean("springComponent").hasBean("springExtension") + .doesNotHaveBean("info").doesNotHaveBean("health") + .doesNotHaveBean("test")); + } + + @Test + public void outcomeWithNoEndpointReferenceShouldFail() { + this.contextRunner + .withUserConfiguration( + ComponentWithNoEndpointReferenceConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=*") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure().getCause().getMessage()) + .contains( + "No endpoint is specified and the return type of the @Bean method " + + "is neither an @Endpoint, nor an @EndpointExtension"); + }); + } + + @Test + public void outcomeOnCloudFoundryShouldMatchAll() { + this.contextRunner + .withPropertyValues("VCAP_APPLICATION:---") + .run((context) -> assertThat(context).hasBean("info") + .hasBean("health").hasBean("spring").hasBean("test")); + } + + @Endpoint(id = "health") + static class HealthEndpoint { + + } + + @Endpoint(id = "info") + static class InfoEndpoint { + + } + + @Endpoint(id = "spring") + static class SpringEndpoint { + + } + + @Endpoint(id = "test") + static class TestEndpoint { + + } + + @EndpointExtension(endpoint = SpringEndpoint.class, filter = TestFilter.class) + static class SpringEndpointExtension { + + } + + static class TestFilter implements EndpointFilter> { + + @Override + public boolean match(ExposableEndpoint endpoint) { + return true; + } + + } + + @Configuration + static class AllEndpointsConfiguration { + + @Bean + @ConditionalOnExposedEndpoint + public HealthEndpoint health() { + return new HealthEndpoint(); + } + + @Bean + @ConditionalOnExposedEndpoint + public InfoEndpoint info() { + return new InfoEndpoint(); + } + + @Bean + @ConditionalOnExposedEndpoint + public SpringEndpoint spring() { + return new SpringEndpoint(); + } + + @Bean + @ConditionalOnExposedEndpoint + public TestEndpoint test() { + return new TestEndpoint(); + } + + } + + @Configuration + static class ComponentEnabledIfEndpointIsExposedConfiguration { + + @Bean + @ConditionalOnExposedEndpoint(endpoint = SpringEndpoint.class) + public String springComponent() { + return "springComponent"; + } + + @Bean + @ConditionalOnExposedEndpoint + public SpringEndpointExtension springExtension() { + return new SpringEndpointExtension(); + } + + } + + @Configuration + static class ComponentWithNoEndpointReferenceConfiguration { + + @Bean + @ConditionalOnExposedEndpoint + public String springcomp() { + return "springcomp"; + } + + } + +}