From 52f732920ba1a9606cfc7e0bad83ccbeceb8374d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 6 Jul 2023 13:34:43 +0100 Subject: [PATCH] Support multiple health groups with an additional path with Jersey This commit knowingly makes breaking API changes to JerseyHealthEndpointAdditionalPathResourceFactory. We considered other options but they all had the potential to be backwards incompatible in one way or another. Faced with that situation we concluded that the likelihood of anyone using the modified API directly is small enough to warrant making the breaking changes. If it becomes apparent that we have misjudged things we can revisit the changes in the future. Closes gh-36250 --- ...ndpointManagementContextConfiguration.java | 2 +- ...althEndpointWebExtensionConfiguration.java | 2 +- ...ndpointAdditionalPathIntegrationTests.java | 43 ++++++++++++++----- ...EndpointAdditionalPathResourceFactory.java | 32 +++++++++++--- 4 files changed, 59 insertions(+), 20 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index e0c1f42f547e..75dab516eb18 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -184,7 +184,7 @@ private void register(ResourceConfig config) { JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory( WebServerNamespace.MANAGEMENT, this.groups); Collection endpointResources = resourceFactory - .createEndpointResources(mapping, Collections.singletonList(this.endpoint), null, null, false) + .createEndpointResources(mapping, Collections.singletonList(this.endpoint)) .stream() .filter(Objects::nonNull) .collect(Collectors.toList()); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java index 33e91ed0ff03..2ef22a102319 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java @@ -163,7 +163,7 @@ private void register(ResourceConfig config) { JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory( WebServerNamespace.SERVER, this.groups); Collection endpointResources = resourceFactory - .createEndpointResources(mapping, Collections.singletonList(this.endpoint), null, null, false) + .createEndpointResources(mapping, Collections.singletonList(this.endpoint)) .stream() .filter(Objects::nonNull) .collect(Collectors.toList()); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java index 5e3101832524..b0d41c2a9bba 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java @@ -27,6 +27,8 @@ import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; +import static org.assertj.core.api.Assertions.assertThatNoException; + /** * Abstract base class for health groups with an additional path. * @@ -52,6 +54,18 @@ void groupIsAvailableAtAdditionalPath() { .run(withWebTestClient(this::testResponse, "local.server.port")); } + @Test + void multipleGroupsAreAvailableAtAdditionalPaths() { + this.runner + .withPropertyValues("management.endpoint.health.group.one.include=diskSpace", + "management.endpoint.health.group.two.include=diskSpace", + "management.endpoint.health.group.one.additional-path=server:/alpha", + "management.endpoint.health.group.two.additional-path=server:/bravo", + "management.endpoint.health.group.one.show-components=always", + "management.endpoint.health.group.two.show-components=always") + .run(withWebTestClient((client) -> testResponses(client, "/alpha", "/bravo"), "local.server.port")); + } + @Test void groupIsAvailableAtAdditionalPathWithoutSlash() { this.runner @@ -125,17 +139,24 @@ void groupsAreNotConfiguredWhenHealthEndpointIsNotExposedWithDifferentManagement } private void testResponse(WebTestClient client) { - client.get() - .uri("/healthz") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus() - .isOk() - .expectBody() - .jsonPath("status") - .isEqualTo("UP") - .jsonPath("components.diskSpace") - .exists(); + testResponses(client, "/healthz"); + } + + private void testResponses(WebTestClient client, String... paths) { + for (String path : paths) { + assertThatNoException().as(path) + .isThrownBy(() -> client.get() + .uri(path) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("status") + .isEqualTo("UP") + .jsonPath("components.diskSpace") + .exists()); + } } private ContextConsumer withWebTestClient(Consumer consumer, String property) { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyHealthEndpointAdditionalPathResourceFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyHealthEndpointAdditionalPathResourceFactory.java index 3b7033753b86..9e744585c2fc 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyHealthEndpointAdditionalPathResourceFactory.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyHealthEndpointAdditionalPathResourceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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,11 +16,17 @@ package org.springframework.boot.actuate.endpoint.web.jersey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.glassfish.jersey.server.model.Resource; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebOperation; import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; @@ -35,7 +41,9 @@ * @author Madhura Bhave * @since 2.6.0 */ -public class JerseyHealthEndpointAdditionalPathResourceFactory extends JerseyEndpointResourceFactory { +public final class JerseyHealthEndpointAdditionalPathResourceFactory { + + private final JerseyEndpointResourceFactory delegate = new JerseyEndpointResourceFactory(); private final Set groups; @@ -47,20 +55,30 @@ public JerseyHealthEndpointAdditionalPathResourceFactory(WebServerNamespace serv this.groups = groups.getAllWithAdditionalPath(serverNamespace); } - @Override - protected Resource createResource(EndpointMapping endpointMapping, WebOperation operation) { + public Collection createEndpointResources(EndpointMapping endpointMapping, + Collection endpoints) { + return endpoints.stream() + .flatMap((endpoint) -> endpoint.getOperations().stream()) + .flatMap((operation) -> createResources(endpointMapping, operation)) + .collect(Collectors.toList()); + } + + private Stream createResources(EndpointMapping endpointMapping, WebOperation operation) { WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate(); String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable(); if (matchAllRemainingPathSegmentsVariable != null) { + List resources = new ArrayList<>(); for (HealthEndpointGroup group : this.groups) { AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath(); if (additionalPath != null) { - return getResource(endpointMapping, operation, requestPredicate, additionalPath.getValue(), - this.serverNamespace, (data, pathSegmentsVariable) -> data.getUriInfo().getPath()); + resources.add(this.delegate.getResource(endpointMapping, operation, requestPredicate, + additionalPath.getValue(), this.serverNamespace, + (data, pathSegmentsVariable) -> data.getUriInfo().getPath())); } } + return resources.stream(); } - return null; + return Stream.empty(); } }