mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Support multi-segment additional health paths
Signed-off-by: Wan bin yu <3431359639@qq.com> See gh-51470
This commit is contained in:
committed by
Andy Wilkinson
parent
1f3209130d
commit
3aa96f2229
+1
-1
@@ -926,7 +926,7 @@ management.endpoint.health.group.live.additional-path="server:/healthz"
|
||||
|
||||
This would make the `live` health group available on the main server port at `/healthz`.
|
||||
The prefix is mandatory and must be either `server:` (represents the main server port) or `management:` (represents the management port, if configured.)
|
||||
The path must be a single path segment.
|
||||
The path can contain one or more path segments.
|
||||
|
||||
|
||||
|
||||
|
||||
+31
@@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
* @param <C> the application context type
|
||||
* @param <A> the assertions
|
||||
* @author Madhura Bhave
|
||||
* @author Wan bin yu
|
||||
*/
|
||||
public abstract class AbstractHealthEndpointAdditionalPathIntegrationTests<T extends AbstractApplicationContextRunner<T, C, A>, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider<A, C>> {
|
||||
|
||||
@@ -54,6 +55,15 @@ public abstract class AbstractHealthEndpointAdditionalPathIntegrationTests<T ext
|
||||
.run(withWebTestClient(this::testResponse, "local.server.port"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void groupIsAvailableAtAdditionalPathWithMultipleSegments() {
|
||||
this.runner
|
||||
.withPropertyValues("management.endpoint.health.group.live.include=diskSpace",
|
||||
"management.endpoint.health.group.live.additional-path=server:/myBasePath/health",
|
||||
"management.endpoint.health.group.live.show-components=always")
|
||||
.run(withWebTestClient((client) -> testResponses(client, "/myBasePath/health"), "local.server.port"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleGroupsAreAvailableAtAdditionalPaths() {
|
||||
this.runner
|
||||
@@ -66,6 +76,27 @@ public abstract class AbstractHealthEndpointAdditionalPathIntegrationTests<T ext
|
||||
.run(withWebTestClient((client) -> testResponses(client, "/alpha", "/bravo"), "local.server.port"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void healthComponentIsAvailableWhenMultiSegmentAdditionalPathIsConfigured() {
|
||||
this.runner
|
||||
.withPropertyValues("management.endpoint.health.show-components=always",
|
||||
"management.endpoint.health.group.live.include=diskSpace",
|
||||
"management.endpoint.health.group.live.additional-path=server:/myBasePath/health",
|
||||
"management.endpoint.health.group.live.show-components=always")
|
||||
.run(withWebTestClient((client) -> {
|
||||
testResponses(client, "/myBasePath/health");
|
||||
client.get()
|
||||
.uri("/actuator/health/diskSpace")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody()
|
||||
.jsonPath("status")
|
||||
.isEqualTo("UP");
|
||||
}, "local.server.port"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void groupIsAvailableAtAdditionalPathWithoutSlash() {
|
||||
this.runner
|
||||
|
||||
+3
-9
@@ -27,6 +27,7 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Madhura Bhave
|
||||
* @author Wan bin yu
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public final class AdditionalHealthEndpointPath {
|
||||
@@ -99,8 +100,8 @@ public final class AdditionalHealthEndpointPath {
|
||||
|
||||
/**
|
||||
* Creates an {@link AdditionalHealthEndpointPath} from the given input. The input
|
||||
* must contain a prefix and value separated by a `:`. The value must be limited to
|
||||
* one path segment. For example, `server:/healthz`.
|
||||
* must contain a prefix and value separated by a `:`. The value can contain one or
|
||||
* more path segments. For example, `server:/healthz`.
|
||||
* @param value the value to parse
|
||||
* @return the new instance
|
||||
*/
|
||||
@@ -110,7 +111,6 @@ public final class AdditionalHealthEndpointPath {
|
||||
Assert.isTrue(values.length == 2, "'value' must contain a valid namespace and value separated by ':'.");
|
||||
Assert.isTrue(StringUtils.hasText(values[0]), "'value' must contain a valid namespace.");
|
||||
WebServerNamespace namespace = WebServerNamespace.from(values[0]);
|
||||
validateValue(values[1]);
|
||||
return new AdditionalHealthEndpointPath(namespace, values[1]);
|
||||
}
|
||||
|
||||
@@ -124,13 +124,7 @@ public final class AdditionalHealthEndpointPath {
|
||||
public static AdditionalHealthEndpointPath of(WebServerNamespace webServerNamespace, String value) {
|
||||
Assert.notNull(webServerNamespace, "'webServerNamespace' must not be null.");
|
||||
Assert.notNull(value, "'value' must not be null.");
|
||||
validateValue(value);
|
||||
return new AdditionalHealthEndpointPath(webServerNamespace, value);
|
||||
}
|
||||
|
||||
private static void validateValue(String value) {
|
||||
Assert.isTrue(StringUtils.countOccurrencesOf(value, "/") <= 1 && value.indexOf("/") <= 0,
|
||||
"'value' must contain only one segment.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-7
@@ -44,6 +44,7 @@ import org.springframework.util.StringUtils;
|
||||
* @param <D> the descriptor type
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Wan bin yu
|
||||
*/
|
||||
abstract class HealthEndpointSupport<H, D> {
|
||||
|
||||
@@ -75,19 +76,29 @@ abstract class HealthEndpointSupport<H, D> {
|
||||
|
||||
@Nullable Result<D> getResult(ApiVersion apiVersion, @Nullable WebServerNamespace serverNamespace,
|
||||
SecurityContext securityContext, boolean showAll, String... path) {
|
||||
HealthEndpointGroup group = (path.length > 0) ? getGroup(serverNamespace, path) : null;
|
||||
if (group != null) {
|
||||
return getResult(apiVersion, group, securityContext, showAll, path, 1);
|
||||
GroupMatch groupMatch = (path.length > 0) ? getGroup(serverNamespace, path) : null;
|
||||
if (groupMatch != null) {
|
||||
return getResult(apiVersion, groupMatch.group(), securityContext, showAll, path, groupMatch.pathOffset());
|
||||
}
|
||||
return getResult(apiVersion, this.groups.getPrimary(), securityContext, showAll, path, 0);
|
||||
}
|
||||
|
||||
private @Nullable HealthEndpointGroup getGroup(@Nullable WebServerNamespace serverNamespace, String... path) {
|
||||
if (this.groups.get(path[0]) != null) {
|
||||
return this.groups.get(path[0]);
|
||||
private @Nullable GroupMatch getGroup(@Nullable WebServerNamespace serverNamespace, String... path) {
|
||||
HealthEndpointGroup group = this.groups.get(path[0]);
|
||||
if (group != null) {
|
||||
return new GroupMatch(group, 1);
|
||||
}
|
||||
if (serverNamespace != null) {
|
||||
return this.groups.get(AdditionalHealthEndpointPath.of(serverNamespace, path[0]));
|
||||
StringBuilder additionalPath = new StringBuilder();
|
||||
GroupMatch groupMatch = null;
|
||||
for (int i = 0; i < path.length; i++) {
|
||||
additionalPath.append((i != 0) ? "/" : "").append(path[i]);
|
||||
group = this.groups.get(AdditionalHealthEndpointPath.of(serverNamespace, additionalPath.toString()));
|
||||
if (group != null) {
|
||||
groupMatch = new GroupMatch(group, i + 1);
|
||||
}
|
||||
}
|
||||
return groupMatch;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -210,4 +221,8 @@ abstract class HealthEndpointSupport<H, D> {
|
||||
|
||||
}
|
||||
|
||||
private record GroupMatch(HealthEndpointGroup group, int pathOffset) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
-9
@@ -66,15 +66,17 @@ class AdditionalHealthEndpointPathTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromPathWithMultipleSegmentsShouldThrowException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> AdditionalHealthEndpointPath.from("server:/my-path/my-sub-path"));
|
||||
void fromPathWithMultipleSegmentsShouldCreatePath() {
|
||||
AdditionalHealthEndpointPath path = AdditionalHealthEndpointPath.from("server:/my-path/my-sub-path");
|
||||
assertThat(path.getValue()).isEqualTo("/my-path/my-sub-path");
|
||||
assertThat(path.getNamespace()).isEqualTo(WebServerNamespace.SERVER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromPathWithMultipleSegmentsNotStartingWithSlashShouldThrowException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> AdditionalHealthEndpointPath.from("server:my-path/my-sub-path"));
|
||||
void fromPathWithMultipleSegmentsNotStartingWithSlashShouldCreatePath() {
|
||||
AdditionalHealthEndpointPath path = AdditionalHealthEndpointPath.from("server:my-path/my-sub-path");
|
||||
assertThat(path.getValue()).isEqualTo("my-path/my-sub-path");
|
||||
assertThat(path.getNamespace()).isEqualTo(WebServerNamespace.SERVER);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,9 +111,11 @@ class AdditionalHealthEndpointPathTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithMultipleSegmentValueShouldThrowException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> AdditionalHealthEndpointPath.of(WebServerNamespace.SERVER, "/my-path/my-subpath"));
|
||||
void ofWithMultipleSegmentValueShouldCreatePath() {
|
||||
AdditionalHealthEndpointPath additionalPath = AdditionalHealthEndpointPath.of(WebServerNamespace.SERVER,
|
||||
"/my-path/my-subpath");
|
||||
assertThat(additionalPath.getValue()).isEqualTo("/my-path/my-subpath");
|
||||
assertThat(additionalPath.getNamespace()).isEqualTo(WebServerNamespace.SERVER);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+14
@@ -353,6 +353,20 @@ abstract class HealthEndpointSupportTests<E extends HealthEndpointSupport<H, D>,
|
||||
assertThat(descriptor.getComponents()).containsKey("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getResultWhenGroupHasMultiSegmentAdditionalPath() {
|
||||
R registry = createRegistry("test", createContributor(this.up));
|
||||
TestHealthEndpointGroup testGroup = new TestHealthEndpointGroup((name) -> name.startsWith("test"));
|
||||
testGroup.setAdditionalPath(AdditionalHealthEndpointPath.from("server:/myBasePath/health"));
|
||||
HealthEndpointGroups groups = HealthEndpointGroups.of(this.primaryGroup, Map.of("testGroup", testGroup));
|
||||
E endpoint = create(registry, groups);
|
||||
Result<D> result = endpoint.getResult(ApiVersion.V3, WebServerNamespace.SERVER, SecurityContext.NONE, false,
|
||||
"myBasePath", "health");
|
||||
assertThat(result).isNotNull();
|
||||
CompositeHealthDescriptor descriptor = (CompositeHealthDescriptor) getDescriptor(result);
|
||||
assertThat(descriptor.getComponents()).containsKey("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getResultWhenGroupHasAdditionalPathAndShowComponentsFalse() {
|
||||
R registry = createRegistry("test", createContributor(this.up));
|
||||
|
||||
+25
-17
@@ -76,6 +76,9 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* A custom {@link HandlerMapping} that makes {@link ExposableWebEndpoint web endpoints}
|
||||
@@ -85,6 +88,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi
|
||||
* @author Madhura Bhave
|
||||
* @author Phillip Webb
|
||||
* @author Brian Clozel
|
||||
* @author Wan bin yu
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ImportRuntimeHints(AbstractWebMvcEndpointHandlerMappingRuntimeHints.class)
|
||||
@@ -192,7 +196,7 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||
protected void registerMapping(ExposableWebEndpoint endpoint, WebOperationRequestPredicate predicate,
|
||||
WebOperation operation, String path) {
|
||||
ServletWebOperation servletWebOperation = wrapServletWebOperation(endpoint, operation,
|
||||
new ServletWebOperationAdapter(operation));
|
||||
new ServletWebOperationAdapter(operation, getPatternParser()));
|
||||
registerMapping(createRequestMappingInfo(predicate, path), new OperationHandler(servletWebOperation),
|
||||
this.handleMethod);
|
||||
}
|
||||
@@ -333,8 +337,11 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||
|
||||
private final WebOperation operation;
|
||||
|
||||
ServletWebOperationAdapter(WebOperation operation) {
|
||||
private final @Nullable PathPatternParser patternParser;
|
||||
|
||||
ServletWebOperationAdapter(WebOperation operation, @Nullable PathPatternParser patternParser) {
|
||||
this.operation = operation;
|
||||
this.patternParser = patternParser;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -390,24 +397,25 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||
}
|
||||
|
||||
private Object getRemainingPathSegments(HttpServletRequest request) {
|
||||
String[] pathTokens = tokenize(request, HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, true);
|
||||
String[] patternTokens = tokenize(request, HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, false);
|
||||
int numberOfRemainingPathSegments = pathTokens.length - patternTokens.length + 1;
|
||||
Assert.state(numberOfRemainingPathSegments >= 0, "Unable to extract remaining path segments");
|
||||
String[] remainingPathSegments = new String[numberOfRemainingPathSegments];
|
||||
System.arraycopy(pathTokens, patternTokens.length - 1, remainingPathSegments, 0,
|
||||
numberOfRemainingPathSegments);
|
||||
return remainingPathSegments;
|
||||
Assert.state(this.patternParser != null, "'patternParser' must not be null");
|
||||
String pattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
|
||||
Assert.state(pattern != null, "'pattern' must not be null");
|
||||
PathPattern pathPattern = this.patternParser.parse(pattern);
|
||||
if (pathPattern.hasPatternSyntax()) {
|
||||
String remainingSegments = pathPattern
|
||||
.extractPathWithinPattern(
|
||||
ServletRequestPathUtils.getParsedRequestPath(request).pathWithinApplication())
|
||||
.value();
|
||||
return tokenizePathSegments(remainingSegments);
|
||||
}
|
||||
return tokenizePathSegments(pathPattern.toString());
|
||||
}
|
||||
|
||||
private String[] tokenize(HttpServletRequest request, String attributeName, boolean decode) {
|
||||
String value = (String) request.getAttribute(attributeName);
|
||||
private String[] tokenizePathSegments(String value) {
|
||||
String[] segments = StringUtils.tokenizeToStringArray(value, PATH_SEPARATOR, false, true);
|
||||
if (decode) {
|
||||
for (int i = 0; i < segments.length; i++) {
|
||||
if (segments[i].contains("%")) {
|
||||
segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8);
|
||||
}
|
||||
for (int i = 0; i < segments.length; i++) {
|
||||
if (segments[i].contains("%")) {
|
||||
segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
return segments;
|
||||
|
||||
Reference in New Issue
Block a user