From 6f1216c3692d9ee5871e4f7ceab3262fbfbac2f6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 6 Jun 2025 08:53:14 +0100 Subject: [PATCH] Use ConditionContext ClassLoader to load OutcomeExposureContributors Fixes gh-45800 --- .../OnAvailableEndpointCondition.java | 22 +++--- .../ConditionalOnAvailableEndpointTests.java | 7 ++ ...estEndpointOutcomeExposureContributor.java | 72 +++++++++++++++++++ 3 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/WithTestEndpointOutcomeExposureContributor.java 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 4d3595da8c9..5659f895d90 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 @@ -71,12 +71,11 @@ class OnAvailableEndpointCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - 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); + return getMatchOutcome(context, conditionAnnotation, endpointAnnotation); } private Class getTarget(ConditionContext context, AnnotatedTypeMetadata metadata, @@ -109,16 +108,17 @@ class OnAvailableEndpointCondition extends SpringBootCondition { return getEndpointAnnotation(extension.getClass("endpoint")); } - private ConditionOutcome getMatchOutcome(Environment environment, + private ConditionOutcome getMatchOutcome(ConditionContext context, MergedAnnotation conditionAnnotation, MergedAnnotation endpointAnnotation) { ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnAvailableEndpoint.class); + Environment environment = context.getEnvironment(); EndpointId endpointId = EndpointId.of(environment, endpointAnnotation.getString("id")); ConditionOutcome accessOutcome = getAccessOutcome(environment, endpointAnnotation, endpointId, message); if (!accessOutcome.isMatch()) { return accessOutcome; } - ConditionOutcome exposureOutcome = getExposureOutcome(environment, conditionAnnotation, endpointAnnotation, + ConditionOutcome exposureOutcome = getExposureOutcome(context, conditionAnnotation, endpointAnnotation, endpointId, message); return (exposureOutcome != null) ? exposureOutcome : ConditionOutcome.noMatch(message.because("not exposed")); } @@ -137,11 +137,11 @@ class OnAvailableEndpointCondition extends SpringBootCondition { .accessFor(endpointId, defaultAccess); } - private ConditionOutcome getExposureOutcome(Environment environment, + private ConditionOutcome getExposureOutcome(ConditionContext context, MergedAnnotation conditionAnnotation, MergedAnnotation endpointAnnotation, EndpointId endpointId, Builder message) { Set exposures = getExposures(conditionAnnotation); - Set outcomeContributors = getExposureOutcomeContributors(environment); + Set outcomeContributors = getExposureOutcomeContributors(context); for (EndpointExposureOutcomeContributor outcomeContributor : outcomeContributors) { ConditionOutcome outcome = outcomeContributor.getExposureOutcome(endpointId, exposures, message); if (outcome != null && outcome.isMatch()) { @@ -166,7 +166,8 @@ class OnAvailableEndpointCondition extends SpringBootCondition { return result; } - private Set getExposureOutcomeContributors(Environment environment) { + private Set getExposureOutcomeContributors(ConditionContext context) { + Environment environment = context.getEnvironment(); Set contributors = exposureOutcomeContributorsCache.get(environment); if (contributors == null) { contributors = new LinkedHashSet<>(); @@ -174,15 +175,16 @@ class OnAvailableEndpointCondition extends SpringBootCondition { if (environment.getProperty(JMX_ENABLED_KEY, Boolean.class, false)) { contributors.add(new StandardExposureOutcomeContributor(environment, EndpointExposure.JMX)); } - contributors.addAll(loadExposureOutcomeContributors(environment)); + contributors.addAll(loadExposureOutcomeContributors(context.getClassLoader(), environment)); exposureOutcomeContributorsCache.put(environment, contributors); } return contributors; } - private List loadExposureOutcomeContributors(Environment environment) { + private List loadExposureOutcomeContributors(ClassLoader classLoader, + Environment environment) { ArgumentResolver argumentResolver = ArgumentResolver.of(Environment.class, environment); - return SpringFactoriesLoader.forDefaultResourceLocation() + return SpringFactoriesLoader.forDefaultResourceLocation(classLoader) .load(EndpointExposureOutcomeContributor.class, argumentResolver); } 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 9636b5ac1f0..a08986a41e4 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 @@ -294,6 +294,13 @@ class ConditionalOnAvailableEndpointTests { .run((context) -> assertThat(context).hasSingleBean(DisabledButAccessibleEndpoint.class)); } + @Test + @WithTestEndpointOutcomeExposureContributor + void exposureOutcomeContributorCanMakeEndpointAvailable() { + this.contextRunner.withPropertyValues("management.endpoints.test.exposure.include=test") + .run((context) -> assertThat(context).hasSingleBean(TestEndpoint.class)); + } + @Endpoint(id = "health") static class HealthEndpoint { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/WithTestEndpointOutcomeExposureContributor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/WithTestEndpointOutcomeExposureContributor.java new file mode 100644 index 00000000000..16edf2d1d44 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/WithTestEndpointOutcomeExposureContributor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2025 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.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +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.endpoint.EndpointId; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; +import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.testsupport.classpath.resources.WithResource; +import org.springframework.core.env.Environment; +import org.springframework.core.io.support.SpringFactoriesLoader; + +/** + * Makes a test {@link EndpointExposureOutcomeContributor} available via + * {@link SpringFactoriesLoader}. + * + * @author Andy Wilkinson + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@WithResource(name = "META-INF/spring.factories", + content = """ + org.springframework.boot.actuate.autoconfigure.endpoint.condition.EndpointExposureOutcomeContributor=\ + org.springframework.boot.actuate.autoconfigure.endpoint.condition.WithTestEndpointOutcomeExposureContributor.TestEndpointExposureOutcomeContributor + """) +public @interface WithTestEndpointOutcomeExposureContributor { + + class TestEndpointExposureOutcomeContributor implements EndpointExposureOutcomeContributor { + + private final IncludeExcludeEndpointFilter filter; + + TestEndpointExposureOutcomeContributor(Environment environment) { + this.filter = new IncludeExcludeEndpointFilter<>(ExposableEndpoint.class, environment, + "management.endpoints.test.exposure"); + } + + @Override + public ConditionOutcome getExposureOutcome(EndpointId endpointId, Set exposures, + Builder message) { + if (this.filter.match(endpointId)) { + return ConditionOutcome.match(); + } + return null; + } + + } + +}