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
This commit is contained in:
Andy Wilkinson 2025-02-10 10:01:19 +00:00
parent 1c0253b380
commit c3c7ed4c2e
2 changed files with 66 additions and 3 deletions

View File

@ -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<String> getAdditionalPaths(EndpointId endpointId, WebServerNamespace webServerNamespace) {
if (!HealthEndpoint.ID.equals(endpointId)) {
return null;
}
List<String> 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;
}
}

View File

@ -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);