From 9c6d1d2944f34fb229970f99eaac64d9c59797b4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Feb 2026 09:51:52 +0000 Subject: [PATCH] Make additional health paths back off without health module Fixes gh-49196 --- module/spring-boot-webflux/build.gradle | 5 ++ ...ndpointManagementContextConfiguration.java | 37 +++++++++------ ...dContextConfigurationIntegrationTests.java | 9 ++++ ...ndpointManagementContextConfiguration.java | 46 +++++++++++-------- ...ntManagementContextConfigurationTests.java | 7 +++ 5 files changed, 70 insertions(+), 34 deletions(-) diff --git a/module/spring-boot-webflux/build.gradle b/module/spring-boot-webflux/build.gradle index f0ccc333326..b57a7c5d42a 100644 --- a/module/spring-boot-webflux/build.gradle +++ b/module/spring-boot-webflux/build.gradle @@ -63,3 +63,8 @@ dependencies { tasks.named("compileTestJava") { options.nullability.checking = "tests" } + +tasks.named("test") { + jvmArgs += "--add-opens=java.base/java.net=ALL-UNNAMED" +} + diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/actuate/web/WebFluxEndpointManagementContextConfiguration.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/actuate/web/WebFluxEndpointManagementContextConfiguration.java index 0f726c93790..12431281083 100644 --- a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/actuate/web/WebFluxEndpointManagementContextConfiguration.java +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/actuate/web/WebFluxEndpointManagementContextConfiguration.java @@ -58,6 +58,7 @@ import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroups; import org.springframework.boot.webflux.actuate.endpoint.web.AdditionalHealthEndpointPathsWebFluxHandlerMapping; import org.springframework.boot.webflux.actuate.endpoint.web.WebFluxEndpointHandlerMapping; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; import org.springframework.core.codec.Encoder; import org.springframework.core.env.Environment; @@ -113,21 +114,6 @@ public class WebFluxEndpointManagementContextConfiguration { || ManagementPortType.get(environment) == ManagementPortType.DIFFERENT); } - @Bean - @ConditionalOnManagementPort(ManagementPortType.DIFFERENT) - @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) - @ConditionalOnBean(HealthEndpoint.class) - public AdditionalHealthEndpointPathsWebFluxHandlerMapping managementHealthEndpointWebFluxHandlerMapping( - WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) { - Collection webEndpoints = webEndpointsSupplier.getEndpoints(); - ExposableWebEndpoint healthEndpoint = webEndpoints.stream() - .filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)) - .findFirst() - .orElse(null); - return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), healthEndpoint, - groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT)); - } - @Bean @ConditionalOnMissingBean @SuppressWarnings("removal") @@ -161,6 +147,27 @@ public class WebFluxEndpointManagementContextConfiguration { SingletonSupplier.of(endpointJsonMapper::getObject)); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(HealthEndpoint.class) + static class HealthConfiguration { + + @Bean + @ConditionalOnManagementPort(ManagementPortType.DIFFERENT) + @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) + @ConditionalOnBean(HealthEndpoint.class) + AdditionalHealthEndpointPathsWebFluxHandlerMapping managementHealthEndpointWebFluxHandlerMapping( + WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) { + Collection webEndpoints = webEndpointsSupplier.getEndpoints(); + ExposableWebEndpoint healthEndpoint = webEndpoints.stream() + .filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)) + .findFirst() + .orElse(null); + return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), healthEndpoint, + groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT)); + } + + } + /** * {@link BeanPostProcessor} to apply {@link EndpointJsonMapper} for * {@link OperationResponseBody} to {@link JacksonJsonEncoder} instances. diff --git a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/actuate/web/WebFluxManagementChildContextConfigurationIntegrationTests.java b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/actuate/web/WebFluxManagementChildContextConfigurationIntegrationTests.java index ade1f3cb0f5..9e69217c444 100644 --- a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/actuate/web/WebFluxManagementChildContextConfigurationIntegrationTests.java +++ b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/actuate/web/WebFluxManagementChildContextConfigurationIntegrationTests.java @@ -41,6 +41,8 @@ import org.springframework.boot.env.ConfigTreePropertySource; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; import org.springframework.boot.tomcat.TomcatWebServer; import org.springframework.boot.tomcat.autoconfigure.actuate.web.server.TomcatReactiveManagementContextAutoConfiguration; import org.springframework.boot.tomcat.autoconfigure.reactive.TomcatReactiveWebServerAutoConfiguration; @@ -64,6 +66,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Andy Wilkinson */ +@DirtiesUrlFactories class WebFluxManagementChildContextConfigurationIntegrationTests { private final List webServers = new ArrayList<>(); @@ -124,6 +127,12 @@ class WebFluxManagementChildContextConfigurationIntegrationTests { }); } + @Test + @ClassPathExclusions(packages = "org.springframework.boot.health.actuate.endpoint") + void refreshSucceedsWithoutHealth() { + this.runner.run((context) -> assertThat(context).hasNotFailed()); + } + private @Nullable AccessLogValve findAccessLogValve() { assertThat(this.webServers).hasSize(2); Tomcat tomcat = ((TomcatWebServer) this.webServers.get(1)).getTomcat(); diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfiguration.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfiguration.java index a60b7bb1adc..a0044f304bf 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfiguration.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfiguration.java @@ -44,6 +44,7 @@ import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; @@ -54,6 +55,7 @@ import org.springframework.boot.webmvc.actuate.endpoint.web.AdditionalHealthEndp import org.springframework.boot.webmvc.actuate.endpoint.web.WebMvcEndpointHandlerMapping; import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; import org.springframework.core.env.Environment; import org.springframework.http.MediaType; @@ -108,25 +110,6 @@ public class WebMvcEndpointManagementContextConfiguration { || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); } - @Bean - @ConditionalOnManagementPort(ManagementPortType.DIFFERENT) - @ConditionalOnBean(HealthEndpoint.class) - @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) - AdditionalHealthEndpointPathsWebMvcHandlerMapping managementHealthEndpointWebMvcHandlerMapping( - WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) { - Collection webEndpoints = webEndpointsSupplier.getEndpoints(); - ExposableWebEndpoint healthEndpoint = webEndpoints.stream() - .filter(this::isHealthEndpoint) - .findFirst() - .orElse(null); - return new AdditionalHealthEndpointPathsWebMvcHandlerMapping(healthEndpoint, - groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT)); - } - - private boolean isHealthEndpoint(ExposableWebEndpoint endpoint) { - return endpoint.getEndpointId().equals(HealthEndpoint.ID); - } - @Bean @ConditionalOnMissingBean @SuppressWarnings("removal") @@ -169,6 +152,31 @@ public class WebMvcEndpointManagementContextConfiguration { return new EndpointJackson2ObjectMapperWebMvcConfigurer(endpointJsonMapper); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(HealthEndpoint.class) + static class HealthConfiguration { + + @Bean + @ConditionalOnManagementPort(ManagementPortType.DIFFERENT) + @ConditionalOnBean(HealthEndpoint.class) + @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) + AdditionalHealthEndpointPathsWebMvcHandlerMapping managementHealthEndpointWebMvcHandlerMapping( + WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) { + Collection webEndpoints = webEndpointsSupplier.getEndpoints(); + ExposableWebEndpoint healthEndpoint = webEndpoints.stream() + .filter(this::isHealthEndpoint) + .findFirst() + .orElse(null); + return new AdditionalHealthEndpointPathsWebMvcHandlerMapping(healthEndpoint, + groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT)); + } + + private boolean isHealthEndpoint(ExposableWebEndpoint endpoint) { + return endpoint.getEndpointId().equals(HealthEndpoint.ID); + } + + } + /** * {@link WebMvcConfigurer} to apply {@link EndpointJsonMapper} for * {@link OperationResponseBody} to {@link JacksonJsonHttpMessageConverter} instances. diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfigurationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfigurationTests.java index 6d909d91086..e85bfcee701 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfigurationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfigurationTests.java @@ -30,6 +30,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration; import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath; import org.springframework.context.annotation.Bean; @@ -66,6 +67,12 @@ class WebMvcEndpointManagementContextConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(ServletEndpointRegistrar.class)); } + @Test + @ClassPathExclusions(packages = "org.springframework.boot.health.actuate.endpoint") + void refreshSucceedsWithoutHealth() { + this.contextRunner.run((context) -> assertThat(context).hasNotFailed()); + } + @Configuration(proxyBeanMethods = false) @ImportAutoConfiguration(WebMvcEndpointManagementContextConfiguration.class) static class TestConfig {