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
This commit is contained in:
Andy Wilkinson 2023-07-06 13:34:43 +01:00
parent 76cd102aa6
commit 52f732920b
4 changed files with 59 additions and 20 deletions

View File

@ -184,7 +184,7 @@ class JerseyWebEndpointManagementContextConfiguration {
JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory(
WebServerNamespace.MANAGEMENT, this.groups);
Collection<Resource> endpointResources = resourceFactory
.createEndpointResources(mapping, Collections.singletonList(this.endpoint), null, null, false)
.createEndpointResources(mapping, Collections.singletonList(this.endpoint))
.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());

View File

@ -163,7 +163,7 @@ class HealthEndpointWebExtensionConfiguration {
JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory(
WebServerNamespace.SERVER, this.groups);
Collection<Resource> endpointResources = resourceFactory
.createEndpointResources(mapping, Collections.singletonList(this.endpoint), null, null, false)
.createEndpointResources(mapping, Collections.singletonList(this.endpoint))
.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());

View File

@ -27,6 +27,8 @@ import org.springframework.context.ConfigurableApplicationContext;
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 @@ abstract class AbstractHealthEndpointAdditionalPathIntegrationTests<T extends Ab
.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 @@ abstract class AbstractHealthEndpointAdditionalPathIntegrationTests<T extends Ab
}
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<A> withWebTestClient(Consumer<WebTestClient> consumer, String property) {

View File

@ -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 @@ import org.springframework.boot.actuate.health.HealthEndpointGroups;
* @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<HealthEndpointGroup> groups;
@ -47,20 +55,30 @@ public class JerseyHealthEndpointAdditionalPathResourceFactory extends JerseyEnd
this.groups = groups.getAllWithAdditionalPath(serverNamespace);
}
@Override
protected Resource createResource(EndpointMapping endpointMapping, WebOperation operation) {
public Collection<Resource> createEndpointResources(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints) {
return endpoints.stream()
.flatMap((endpoint) -> endpoint.getOperations().stream())
.flatMap((operation) -> createResources(endpointMapping, operation))
.collect(Collectors.toList());
}
private Stream<Resource> createResources(EndpointMapping endpointMapping, WebOperation operation) {
WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate();
String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
List<Resource> 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();
}
}