From c3c7ed4c2e5c5fadfeb2bc309552b1a474d96af9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 10 Feb 2025 10:01:19 +0000 Subject: [PATCH] Fix class cast during additional path matching with health probes Previously, when health probes were enabled, the post-processor of AutoConfiguredHealthEndpointGroups resulted in the bean no longer implementing AdditionalPathMapper. This then caused a ClassCastException when working with AdditionalPathMapper beans in EndpointRequest's additional path mapping support. This commit updates the type returned by the post-processor to implement both HealthEndpointGroups and AdditionalPathMapper, as AutoConfiguredHealthEndpointGroups does. Its implementation of getAdditionalPaths produces a result that combines both the additional paths of the original HealthEndpointGroups bean and its own additional paths for the probes. Fixes gh-44052 --- ...vailabilityProbesHealthEndpointGroups.java | 29 +++++++++++++- ...ealthEndpointGroupsPostProcessorTests.java | 40 ++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroups.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroups.java index f93303feccd..aa40148cc0e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroups.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroups.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -16,14 +16,20 @@ package org.springframework.boot.actuate.autoconfigure.availability; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import org.springframework.boot.actuate.endpoint.EndpointId; +import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper; import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath; +import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpointGroup; import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.util.Assert; @@ -35,7 +41,7 @@ import org.springframework.util.Assert; * @author Brian Clozel * @author Madhura Bhave */ -class AvailabilityProbesHealthEndpointGroups implements HealthEndpointGroups { +class AvailabilityProbesHealthEndpointGroups implements HealthEndpointGroups, AdditionalPathsMapper { private final HealthEndpointGroups groups; @@ -107,4 +113,23 @@ class AvailabilityProbesHealthEndpointGroups implements HealthEndpointGroups { return name.equals(LIVENESS) || name.equals(READINESS); } + @Override + public List getAdditionalPaths(EndpointId endpointId, WebServerNamespace webServerNamespace) { + if (!HealthEndpoint.ID.equals(endpointId)) { + return null; + } + List additionalPaths = new ArrayList<>(); + if (this.groups instanceof AdditionalPathsMapper additionalPathsMapper) { + additionalPaths.addAll(additionalPathsMapper.getAdditionalPaths(endpointId, webServerNamespace)); + } + additionalPaths.addAll(this.probeGroups.values() + .stream() + .map(HealthEndpointGroup::getAdditionalPath) + .filter(Objects::nonNull) + .filter((additionalPath) -> additionalPath.hasNamespace(webServerNamespace)) + .map(AdditionalHealthEndpointPath::getValue) + .toList()); + return additionalPaths; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessorTests.java index b522bec9722..162927c8b0d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -17,10 +17,15 @@ package org.springframework.boot.actuate.autoconfigure.availability; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.boot.actuate.endpoint.EndpointId; +import org.springframework.boot.actuate.endpoint.web.AdditionalPathsMapper; +import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.actuate.health.HealthEndpointGroup; import org.springframework.boot.actuate.health.HealthEndpointGroups; import org.springframework.mock.env.MockEnvironment; @@ -103,6 +108,39 @@ class AvailabilityProbesHealthEndpointGroupsPostProcessorTests { assertThat(readiness.getAdditionalPath()).hasToString("server:/readyz"); } + @Test + void delegatesAdditionalPathMappingToOriginalBean() { + HealthEndpointGroups groups = mock(HealthEndpointGroups.class, + Mockito.withSettings().extraInterfaces(AdditionalPathsMapper.class)); + given(((AdditionalPathsMapper) groups).getAdditionalPaths(EndpointId.of("health"), WebServerNamespace.SERVER)) + .willReturn(List.of("/one", "/two", "/three")); + MockEnvironment environment = new MockEnvironment(); + AvailabilityProbesHealthEndpointGroupsPostProcessor postProcessor = new AvailabilityProbesHealthEndpointGroupsPostProcessor( + environment); + HealthEndpointGroups postProcessed = postProcessor.postProcessHealthEndpointGroups(groups); + assertThat(postProcessed).isInstanceOf(AdditionalPathsMapper.class); + AdditionalPathsMapper additionalPathsMapper = (AdditionalPathsMapper) postProcessed; + assertThat(additionalPathsMapper.getAdditionalPaths(EndpointId.of("health"), WebServerNamespace.SERVER)) + .containsExactly("/one", "/two", "/three"); + } + + @Test + void whenAddAdditionalPathsIsTrueThenIncludesOwnAdditionalPathsInGetAdditionalPathsResult() { + HealthEndpointGroups groups = mock(HealthEndpointGroups.class, + Mockito.withSettings().extraInterfaces(AdditionalPathsMapper.class)); + given(((AdditionalPathsMapper) groups).getAdditionalPaths(EndpointId.of("health"), WebServerNamespace.SERVER)) + .willReturn(List.of("/one", "/two", "/three")); + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("management.endpoint.health.probes.add-additional-paths", "true"); + AvailabilityProbesHealthEndpointGroupsPostProcessor postProcessor = new AvailabilityProbesHealthEndpointGroupsPostProcessor( + environment); + HealthEndpointGroups postProcessed = postProcessor.postProcessHealthEndpointGroups(groups); + assertThat(postProcessed).isInstanceOf(AdditionalPathsMapper.class); + AdditionalPathsMapper additionalPathsMapper = (AdditionalPathsMapper) postProcessed; + assertThat(additionalPathsMapper.getAdditionalPaths(EndpointId.of("health"), WebServerNamespace.SERVER)) + .containsExactly("/one", "/two", "/three", "/livez", "/readyz"); + } + private HealthEndpointGroups getPostProcessed(String value) { MockEnvironment environment = new MockEnvironment(); environment.setProperty("management.endpoint.health.probes.add-additional-paths", value);