From 2993dccd1e521c2c2945cecd3ad0b432ed45bc3a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Feb 2018 20:43:24 +0000 Subject: [PATCH] Provide links to all types of endpoints Previously, links were only provide to web endpoints. This commit expands link resolution to also provide links for servlet endpoints, controller endpoints, and rest controller endpoints. Closes gh-11902 --- .../pom.xml | 5 + ...dFoundryWebFluxEndpointHandlerMapping.java | 8 +- ...CloudFoundryActuatorAutoConfiguration.java | 20 +++- ...CloudFoundryActuatorAutoConfiguration.java | 23 +++- ...undryWebEndpointServletHandlerMapping.java | 10 +- ...ndpointManagementContextConfiguration.java | 17 ++- ...ndpointManagementContextConfiguration.java | 18 ++- ...ndpointManagementContextConfiguration.java | 22 +++- ...oundryWebFluxEndpointIntegrationTests.java | 4 +- ...FoundryMvcWebEndpointIntegrationTests.java | 4 +- .../JerseyEndpointIntegrationTests.java | 103 ++++++++++++++++++ .../WebFluxEndpointIntegrationTests.java | 98 +++++++++++++++++ .../WebMvcEndpointIntegrationTests.java | 65 +++++++++++ .../endpoint/web/EndpointLinksResolver.java | 39 +++++-- .../jersey/JerseyEndpointResourceFactory.java | 23 ++-- .../WebFluxEndpointHandlerMapping.java | 9 +- .../servlet/WebMvcEndpointHandlerMapping.java | 10 +- .../web/EndpointLinksResolverTests.java | 50 +++++++-- .../JerseyWebEndpointIntegrationTests.java | 4 +- .../WebFluxEndpointIntegrationTests.java | 4 +- .../MvcWebEndpointIntegrationTests.java | 4 +- .../web/test/JerseyEndpointsRunner.java | 4 +- .../web/test/WebFluxEndpointsRunner.java | 4 +- .../web/test/WebMvcEndpointRunner.java | 4 +- 24 files changed, 480 insertions(+), 72 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointIntegrationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml b/spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml index d0151ebc913..2de8b044da4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml @@ -410,6 +410,11 @@ hsqldb test + + org.glassfish.jersey.ext + jersey-spring4 + test + org.glassfish.jersey.media jersey-media-json-jackson diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java index d0aed7f3a51..1cee0e313f7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java @@ -54,13 +54,15 @@ class CloudFoundryWebFluxEndpointHandlerMapping private final CloudFoundrySecurityInterceptor securityInterceptor; - private final EndpointLinksResolver linksResolver = new EndpointLinksResolver(); + private final EndpointLinksResolver linksResolver; CloudFoundryWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping, Collection endpoints, EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration, - CloudFoundrySecurityInterceptor securityInterceptor) { + CloudFoundrySecurityInterceptor securityInterceptor, + EndpointLinksResolver linksResolver) { super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration); + this.linksResolver = linksResolver; this.securityInterceptor = securityInterceptor; } @@ -83,7 +85,7 @@ class CloudFoundryWebFluxEndpointHandlerMapping AccessLevel accessLevel = exchange .getAttribute(AccessLevel.REQUEST_ATTRIBUTE); Map links = this.linksResolver - .resolveLinks(getEndpoints(), request.getURI().toString()); + .resolveLinks(request.getURI().toString()); return new ResponseEntity<>( Collections.singletonMap("_links", getAccessibleLinks(accessLevel, links)), diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java index 8eae3c7ceb6..0e4edfa56c9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java @@ -16,18 +16,25 @@ package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.List; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.PathMapper; +import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -85,17 +92,22 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration { @Bean public CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebFluxEndpointHandlerMapping( ParameterValueMapper parameterMapper, EndpointMediaTypes endpointMediaTypes, - WebClient.Builder webClientBuilder) { + WebClient.Builder webClientBuilder, + ControllerEndpointsSupplier controllerEndpointsSupplier) { CloudFoundryWebEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebEndpointDiscoverer( this.applicationContext, parameterMapper, endpointMediaTypes, PathMapper.useEndpointId(), Collections.emptyList(), Collections.emptyList()); CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor( webClientBuilder, this.applicationContext.getEnvironment()); + Collection webEndpoints = endpointDiscoverer.getEndpoints(); + List> allEndpoints = new ArrayList<>(); + allEndpoints.addAll(webEndpoints); + allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); return new CloudFoundryWebFluxEndpointHandlerMapping( - new EndpointMapping("/cloudfoundryapplication"), - endpointDiscoverer.getEndpoints(), endpointMediaTypes, - getCorsConfiguration(), securityInterceptor); + new EndpointMapping("/cloudfoundryapplication"), webEndpoints, + endpointMediaTypes, getCorsConfiguration(), securityInterceptor, + new EndpointLinksResolver(allEndpoints)); } private CloudFoundrySecurityInterceptor getSecurityInterceptor( diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java index 4185f30c677..3d12e473989 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java @@ -16,17 +16,25 @@ package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.List; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.PathMapper; +import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpointWebExtension; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -87,17 +95,24 @@ public class CloudFoundryActuatorAutoConfiguration { @Bean public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServletHandlerMapping( ParameterValueMapper parameterMapper, EndpointMediaTypes endpointMediaTypes, - RestTemplateBuilder restTemplateBuilder) { + RestTemplateBuilder restTemplateBuilder, + ServletEndpointsSupplier servletEndpointsSupplier, + ControllerEndpointsSupplier controllerEndpointsSupplier) { CloudFoundryWebEndpointDiscoverer discoverer = new CloudFoundryWebEndpointDiscoverer( this.applicationContext, parameterMapper, endpointMediaTypes, PathMapper.useEndpointId(), Collections.emptyList(), Collections.emptyList()); CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor( restTemplateBuilder, this.applicationContext.getEnvironment()); + Collection webEndpoints = discoverer.getEndpoints(); + List> allEndpoints = new ArrayList<>(); + allEndpoints.addAll(webEndpoints); + allEndpoints.addAll(servletEndpointsSupplier.getEndpoints()); + allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); return new CloudFoundryWebEndpointServletHandlerMapping( - new EndpointMapping("/cloudfoundryapplication"), - discoverer.getEndpoints(), endpointMediaTypes, getCorsConfiguration(), - securityInterceptor); + new EndpointMapping("/cloudfoundryapplication"), webEndpoints, + endpointMediaTypes, getCorsConfiguration(), securityInterceptor, + new EndpointLinksResolver(allEndpoints)); } private CloudFoundrySecurityInterceptor getSecurityInterceptor( diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java index 2b45b1edd1c..3ffcdc0491a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java @@ -52,14 +52,16 @@ class CloudFoundryWebEndpointServletHandlerMapping private final CloudFoundrySecurityInterceptor securityInterceptor; - private final EndpointLinksResolver linksResolver = new EndpointLinksResolver(); + private final EndpointLinksResolver linksResolver; CloudFoundryWebEndpointServletHandlerMapping(EndpointMapping endpointMapping, Collection endpoints, EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration, - CloudFoundrySecurityInterceptor securityInterceptor) { + CloudFoundrySecurityInterceptor securityInterceptor, + EndpointLinksResolver linksResolver) { super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration); this.securityInterceptor = securityInterceptor; + this.linksResolver = linksResolver; } @Override @@ -80,8 +82,8 @@ class CloudFoundryWebEndpointServletHandlerMapping } AccessLevel accessLevel = (AccessLevel) request .getAttribute(AccessLevel.REQUEST_ATTRIBUTE); - Map links = this.linksResolver.resolveLinks(getEndpoints(), - request.getRequestURL().toString()); + Map links = this.linksResolver + .resolveLinks(request.getRequestURL().toString()); Map filteredLinks = new LinkedHashMap<>(); if (accessLevel == null) { return Collections.singletonMap("_links", filteredLinks); 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 cdf50e32323..55a3c51ecdd 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 @@ -16,19 +16,25 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.List; import org.glassfish.jersey.server.ResourceConfig; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -55,17 +61,24 @@ class JerseyWebEndpointManagementContextConfiguration { @Bean public ResourceConfigCustomizer webEndpointRegistrar( WebEndpointsSupplier webEndpointsSupplier, + ServletEndpointsSupplier servletEndpointsSupplier, + ControllerEndpointsSupplier controllerEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, WebEndpointProperties webEndpointProperties) { + List> allEndpoints = new ArrayList<>(); + allEndpoints.addAll(webEndpointsSupplier.getEndpoints()); + allEndpoints.addAll(servletEndpointsSupplier.getEndpoints()); + allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); return (resourceConfig) -> { JerseyEndpointResourceFactory resourceFactory = new JerseyEndpointResourceFactory(); String basePath = webEndpointProperties.getBasePath(); EndpointMapping endpointMapping = new EndpointMapping(basePath); - Collection endpoints = Collections + Collection webEndpoints = Collections .unmodifiableCollection(webEndpointsSupplier.getEndpoints()); resourceConfig.registerResources( new HashSet<>(resourceFactory.createEndpointResources(endpointMapping, - endpoints, endpointMediaTypes))); + webEndpoints, endpointMediaTypes, + new EndpointLinksResolver(allEndpoints)))); }; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index 6b99bef0e7d..8124b490ff9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -16,12 +16,19 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.reactive.ControllerEndpointHandlerMapping; @@ -54,13 +61,18 @@ public class WebFluxEndpointManagementContextConfiguration { @ConditionalOnMissingBean public WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping( WebEndpointsSupplier webEndpointsSupplier, + ControllerEndpointsSupplier controllerEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties) { EndpointMapping endpointMapping = new EndpointMapping( webEndpointProperties.getBasePath()); - return new WebFluxEndpointHandlerMapping(endpointMapping, - webEndpointsSupplier.getEndpoints(), endpointMediaTypes, - corsProperties.toCorsConfiguration()); + Collection endpoints = webEndpointsSupplier.getEndpoints(); + List> allEndpoints = new ArrayList<>(); + allEndpoints.addAll(endpoints); + allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); + return new WebFluxEndpointHandlerMapping(endpointMapping, endpoints, + endpointMediaTypes, corsProperties.toCorsConfiguration(), + new EndpointLinksResolver(allEndpoints)); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java index 2072a43e888..679760eb679 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java @@ -16,14 +16,22 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.servlet.ControllerEndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -53,13 +61,21 @@ public class WebMvcEndpointManagementContextConfiguration { @ConditionalOnMissingBean public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping( WebEndpointsSupplier webEndpointsSupplier, + ServletEndpointsSupplier servletEndpointsSupplier, + ControllerEndpointsSupplier controllerEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties) { + List> allEndpoints = new ArrayList<>(); + Collection webEndpoints = webEndpointsSupplier + .getEndpoints(); + allEndpoints.addAll(webEndpoints); + allEndpoints.addAll(servletEndpointsSupplier.getEndpoints()); + allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); EndpointMapping endpointMapping = new EndpointMapping( webEndpointProperties.getBasePath()); - return new WebMvcEndpointHandlerMapping(endpointMapping, - webEndpointsSupplier.getEndpoints(), endpointMediaTypes, - corsProperties.toCorsConfiguration()); + return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, + endpointMediaTypes, corsProperties.toCorsConfiguration(), + new EndpointLinksResolver(allEndpoints)); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java index b94ff8e93d0..56152f3b076 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java @@ -34,6 +34,7 @@ import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.PathMapper; @@ -221,7 +222,8 @@ public class CloudFoundryWebFluxEndpointIntegrationTests { return new CloudFoundryWebFluxEndpointHandlerMapping( new EndpointMapping("/cfApplication"), webEndpointDiscoverer.getEndpoints(), endpointMediaTypes, - corsConfiguration, interceptor); + corsConfiguration, interceptor, + new EndpointLinksResolver(webEndpointDiscoverer.getEndpoints())); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java index 7c5da5dedee..6dd6a226f24 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java @@ -33,6 +33,7 @@ import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.PathMapper; @@ -207,7 +208,8 @@ public class CloudFoundryMvcWebEndpointIntegrationTests { return new CloudFoundryWebEndpointServletHandlerMapping( new EndpointMapping("/cfApplication"), webEndpointDiscoverer.getEndpoints(), endpointMediaTypes, - corsConfiguration, interceptor); + corsConfiguration, interceptor, + new EndpointLinksResolver(webEndpointDiscoverer.getEndpoints())); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java new file mode 100644 index 00000000000..5bfd7c70609 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-2018 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.integrationtest; + +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.Test; + +import org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint; +import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * Integration tests for the Jersey actuator endpoints. + * + * @author Andy Wilkinson + */ +public class JerseyEndpointIntegrationTests { + + @Test + public void linksAreProvidedToAllEndpointTypes() throws Exception { + new WebApplicationContextRunner( + AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration( + AutoConfigurations.of(JacksonAutoConfiguration.class, + JerseyAutoConfiguration.class, + EndpointAutoConfiguration.class, + ServletWebServerFactoryAutoConfiguration.class, + WebEndpointAutoConfiguration.class, + ManagementContextAutoConfiguration.class, + BeansEndpointAutoConfiguration.class)) + .withUserConfiguration(EndpointsConfiguration.class) + .withPropertyValues("management.endpoints.web.expose:*", + "server.port:0") + .run((context) -> { + int port = context.getSourceApplicationContext( + AnnotationConfigServletWebServerApplicationContext.class) + .getWebServer().getPort(); + WebTestClient client = WebTestClient.bindToServer() + .baseUrl("http://localhost:" + port).build(); + client.get().uri("/actuator").exchange().expectStatus().isOk() + .expectBody().jsonPath("_links.beans").isNotEmpty() + .jsonPath("_links.restcontroller").isNotEmpty() + .jsonPath("_links.controller").isNotEmpty(); + }); + } + + @ControllerEndpoint(id = "controller") + static class TestControllerEndpoint { + + } + + @RestControllerEndpoint(id = "restcontroller") + static class TestRestControllerEndpoint { + + } + + @Configuration + static class EndpointsConfiguration { + + @Bean + ResourceConfig testResourceConfig() { + return new ResourceConfig(); + } + + @Bean + TestControllerEndpoint testControllerEndpoint() { + return new TestControllerEndpoint(); + } + + @Bean + TestRestControllerEndpoint testRestControllerEndpoint() { + return new TestRestControllerEndpoint(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointIntegrationTests.java new file mode 100644 index 00000000000..254032951bb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointIntegrationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2018 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.integrationtest; + +import org.junit.Test; + +import org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementContextAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint; +import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * Integration tests for the WebFlux actuator endpoints. + * + * @author Andy Wilkinson + */ +public class WebFluxEndpointIntegrationTests { + + @Test + public void linksAreProvidedToAllEndpointTypes() throws Exception { + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, + CodecsAutoConfiguration.class, WebFluxAutoConfiguration.class, + HttpHandlerAutoConfiguration.class, + EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, + ManagementContextAutoConfiguration.class, + ReactiveManagementContextAutoConfiguration.class, + BeansEndpointAutoConfiguration.class)) + .withUserConfiguration(EndpointsConfiguration.class) + .withPropertyValues("management.endpoints.web.expose:*") + .run((context) -> { + WebTestClient client = createWebTestClient(context); + client.get().uri("/actuator").exchange().expectStatus().isOk() + .expectBody().jsonPath("_links.beans").isNotEmpty() + .jsonPath("_links.restcontroller").isNotEmpty() + .jsonPath("_links.controller").isNotEmpty(); + }); + } + + private WebTestClient createWebTestClient(ApplicationContext context) { + return WebTestClient.bindToApplicationContext(context).configureClient() + .baseUrl("https://spring.example.org").build(); + } + + @ControllerEndpoint(id = "controller") + static class TestControllerEndpoint { + + } + + @RestControllerEndpoint(id = "restcontroller") + static class TestRestControllerEndpoint { + + } + + @Configuration + static class EndpointsConfiguration { + + @Bean + TestControllerEndpoint testControllerEndpoint() { + return new TestControllerEndpoint(); + } + + @Bean + TestRestControllerEndpoint testRestControllerEndpoint() { + return new TestRestControllerEndpoint(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java index 1d4fb3f0490..b8bdf6547b8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java @@ -16,6 +16,10 @@ package org.springframework.boot.actuate.autoconfigure.integrationtest; +import java.util.function.Supplier; + +import javax.servlet.http.HttpServlet; + import org.junit.After; import org.junit.Test; @@ -25,6 +29,10 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfi import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; +import org.springframework.boot.actuate.endpoint.web.EndpointServlet; +import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint; +import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; +import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration; @@ -35,6 +43,8 @@ import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfi import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.mock.web.MockServletContext; @@ -46,8 +56,11 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import static org.hamcrest.Matchers.both; +import static org.hamcrest.Matchers.hasKey; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -97,6 +110,17 @@ public class WebMvcEndpointIntegrationTests { mockMvc.perform(get("/management/beans")).andExpect(status().isOk()); } + @Test + public void linksAreProvidedToAllEndpointTypes() throws Exception { + this.context = new AnnotationConfigWebApplicationContext(); + this.context.register(DefaultConfiguration.class, EndpointsConfiguration.class); + TestPropertyValues.of("management.endpoints.web.expose=*").applyTo(this.context); + MockMvc mockMvc = doCreateMockMvc(); + mockMvc.perform(get("/actuator").accept("*/*")).andExpect(status().isOk()) + .andExpect(jsonPath("_links", both(hasKey("beans")).and(hasKey("servlet")) + .and(hasKey("restcontroller")).and(hasKey("controller")))); + } + private MockMvc createSecureMockMvc() { return doCreateMockMvc(springSecurity()); } @@ -142,4 +166,45 @@ public class WebMvcEndpointIntegrationTests { } + @ServletEndpoint(id = "servlet") + static class TestServletEndpoint implements Supplier { + + @Override + public EndpointServlet get() { + return new EndpointServlet(new HttpServlet() { + }); + } + + } + + @ControllerEndpoint(id = "controller") + static class TestControllerEndpoint { + + } + + @RestControllerEndpoint(id = "restcontroller") + static class TestRestControllerEndpoint { + + } + + @Configuration + static class EndpointsConfiguration { + + @Bean + TestServletEndpoint testServletEndpoint() { + return new TestServletEndpoint(); + } + + @Bean + TestControllerEndpoint testControllerEndpoint() { + return new TestControllerEndpoint(); + } + + @Bean + TestRestControllerEndpoint testRestControllerEndpoint() { + return new TestRestControllerEndpoint(); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolver.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolver.java index e95ff06cc26..dbcee560c70 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolver.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolver.java @@ -20,6 +20,8 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; + /** * A resolver for {@link Link links} to web endpoints. * @@ -28,22 +30,29 @@ import java.util.Map; */ public class EndpointLinksResolver { + private final Collection> endpoints; + + public EndpointLinksResolver(Collection> endpoints) { + this.endpoints = endpoints; + } + /** - * Resolves links to the operations of the given {code webEndpoints} based on a - * request with the given {@code requestUrl}. - * @param endpoints the source endpoints + * Resolves links to the known endpoints based on a request with the given + * {@code requestUrl}. * @param requestUrl the url of the request for the endpoint links * @return the links */ - public Map resolveLinks(Collection endpoints, - String requestUrl) { + public Map resolveLinks(String requestUrl) { String normalizedUrl = normalizeRequestUrl(requestUrl); Map links = new LinkedHashMap<>(); links.put("self", new Link(normalizedUrl)); - for (ExposableWebEndpoint endpoint : endpoints) { - for (WebOperation operation : endpoint.getOperations()) { - endpoints.stream().map(ExposableWebEndpoint::getId).forEach((id) -> links - .put(operation.getId(), createLink(normalizedUrl, operation))); + for (ExposableEndpoint endpoint : this.endpoints) { + if (endpoint instanceof ExposableWebEndpoint) { + collectLinks(links, (ExposableWebEndpoint) endpoint, normalizedUrl); + } + else if (endpoint instanceof PathMappedEndpoint) { + links.put(endpoint.getId(), createLink(normalizedUrl, + ((PathMappedEndpoint) endpoint).getRootPath())); } } return links; @@ -56,8 +65,18 @@ public class EndpointLinksResolver { return requestUrl; } + private void collectLinks(Map links, ExposableWebEndpoint endpoint, + String normalizedUrl) { + for (WebOperation operation : endpoint.getOperations()) { + links.put(operation.getId(), createLink(normalizedUrl, operation)); + } + } + private Link createLink(String requestUrl, WebOperation operation) { - String path = operation.getRequestPredicate().getPath(); + return createLink(requestUrl, operation.getRequestPredicate().getPath()); + } + + private Link createLink(String requestUrl, String path) { return new Link(requestUrl + (path.startsWith("/") ? path : "/" + path)); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java index 72d9aecb062..19fa276241b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java @@ -62,26 +62,25 @@ import org.springframework.util.StringUtils; */ public class JerseyEndpointResourceFactory { - private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver(); - /** * Creates {@link Resource Resources} for the operations of the given * {@code webEndpoints}. * @param endpointMapping the base mapping for all endpoints * @param endpoints the web endpoints * @param endpointMediaTypes media types consumed and produced by the endpoints + * @param linksResolver resolver for determining links to available endpoints * @return the resources for the operations */ public Collection createEndpointResources(EndpointMapping endpointMapping, Collection endpoints, - EndpointMediaTypes endpointMediaTypes) { + EndpointMediaTypes endpointMediaTypes, EndpointLinksResolver linksResolver) { List resources = new ArrayList<>(); endpoints.stream().flatMap((endpoint) -> endpoint.getOperations().stream()) .map((operation) -> createResource(endpointMapping, operation)) .forEach(resources::add); if (StringUtils.hasText(endpointMapping.getPath())) { Resource resource = createEndpointLinksResource(endpointMapping.getPath(), - endpoints, endpointMediaTypes); + endpointMediaTypes, linksResolver); resources.add(resource); } return resources; @@ -105,14 +104,12 @@ public class JerseyEndpointResourceFactory { } private Resource createEndpointLinksResource(String endpointPath, - Collection endpoints, - EndpointMediaTypes endpointMediaTypes) { + EndpointMediaTypes endpointMediaTypes, EndpointLinksResolver linksResolver) { Builder resourceBuilder = Resource.builder().path(endpointPath); resourceBuilder.addMethod("GET") .produces(endpointMediaTypes.getProduced() .toArray(new String[endpointMediaTypes.getProduced().size()])) - .handledBy(new EndpointLinksInflector(endpoints, - this.endpointLinksResolver)); + .handledBy(new EndpointLinksInflector(linksResolver)); return resourceBuilder.build(); } @@ -266,20 +263,16 @@ public class JerseyEndpointResourceFactory { private static final class EndpointLinksInflector implements Inflector { - private final Collection endpoints; - private final EndpointLinksResolver linksResolver; - private EndpointLinksInflector(Collection endpoints, - EndpointLinksResolver linksResolver) { - this.endpoints = endpoints; + private EndpointLinksInflector(EndpointLinksResolver linksResolver) { this.linksResolver = linksResolver; } @Override public Response apply(ContainerRequestContext request) { - Map links = this.linksResolver.resolveLinks(this.endpoints, - request.getUriInfo().getAbsolutePath().toString()); + Map links = this.linksResolver + .resolveLinks(request.getUriInfo().getAbsolutePath().toString()); return Response.ok(Collections.singletonMap("_links", links)).build(); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java index 7e19a5450ca..42bd357b2b2 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java @@ -43,7 +43,7 @@ import org.springframework.web.util.UriComponentsBuilder; public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping implements InitializingBean { - private final EndpointLinksResolver linksResolver = new EndpointLinksResolver(); + private final EndpointLinksResolver linksResolver; /** * Creates a new {@code WebFluxEndpointHandlerMapping} instance that provides mappings @@ -52,11 +52,14 @@ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandle * @param endpoints the web endpoints * @param endpointMediaTypes media types consumed and produced by the endpoints * @param corsConfiguration the CORS configuration for the endpoints or {@code null} + * @param linksResolver resolver for determining links to available endpoints */ public WebFluxEndpointHandlerMapping(EndpointMapping endpointMapping, Collection endpoints, - EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { + EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration, + EndpointLinksResolver linksResolver) { super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration); + this.linksResolver = linksResolver; setOrder(-100); } @@ -66,7 +69,7 @@ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandle String requestUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()) .replaceQuery(null).toUriString(); return Collections.singletonMap("_links", - this.linksResolver.resolveLinks(getEndpoints(), requestUri)); + this.linksResolver.resolveLinks(requestUri)); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java index 2b91e40ec92..ca1aae006d6 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcEndpointHandlerMapping.java @@ -42,7 +42,7 @@ import org.springframework.web.servlet.HandlerMapping; */ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerMapping { - private final EndpointLinksResolver linksResolver = new EndpointLinksResolver(); + private final EndpointLinksResolver linksResolver; /** * Creates a new {@code WebMvcEndpointHandlerMapping} instance that provides mappings @@ -51,11 +51,14 @@ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerM * @param endpoints the web endpoints * @param endpointMediaTypes media types consumed and produced by the endpoints * @param corsConfiguration the CORS configuration for the endpoints or {@code null} + * @param linksResolver resolver for determining links to available endpoints */ public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, Collection endpoints, - EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { + EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration, + EndpointLinksResolver linksResolver) { super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration); + this.linksResolver = linksResolver; setOrder(-100); } @@ -63,9 +66,8 @@ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerM @ResponseBody protected Map> links(HttpServletRequest request, HttpServletResponse response) { - String requestUri = request.getRequestURL().toString(); return Collections.singletonMap("_links", - this.linksResolver.resolveLinks(getEndpoints(), requestUri)); + this.linksResolver.resolveLinks(request.getRequestURL().toString())); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolverTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolverTests.java index 48826753b35..32ae0fe60ef 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolverTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolverTests.java @@ -25,6 +25,7 @@ import org.assertj.core.api.Condition; import org.junit.Test; import org.springframework.boot.actuate.endpoint.OperationType; +import org.springframework.boot.actuate.endpoint.web.annotation.ExposableControllerEndpoint; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -37,12 +38,10 @@ import static org.mockito.Mockito.mock; */ public class EndpointLinksResolverTests { - private final EndpointLinksResolver linksResolver = new EndpointLinksResolver(); - @Test public void linkResolutionWithTrailingSlashStripsSlashOnSelfLink() { - Map links = this.linksResolver.resolveLinks(Collections.emptyList(), - "https://api.example.com/actuator/"); + Map links = new EndpointLinksResolver(Collections.emptyList()) + .resolveLinks("https://api.example.com/actuator/"); assertThat(links).hasSize(1); assertThat(links).hasEntrySatisfying("self", linkWithHref("https://api.example.com/actuator")); @@ -50,15 +49,15 @@ public class EndpointLinksResolverTests { @Test public void linkResolutionWithoutTrailingSlash() { - Map links = this.linksResolver.resolveLinks(Collections.emptyList(), - "https://api.example.com/actuator"); + Map links = new EndpointLinksResolver(Collections.emptyList()) + .resolveLinks("https://api.example.com/actuator"); assertThat(links).hasSize(1); assertThat(links).hasEntrySatisfying("self", linkWithHref("https://api.example.com/actuator")); } @Test - public void resolvedLinksContainsALinkForEachEndpointOperation() { + public void resolvedLinksContainsALinkForEachWebEndpointOperation() { List operations = new ArrayList<>(); operations.add(operationWithPath("/alpha", "alpha")); operations.add(operationWithPath("/alpha/{name}", "alpha-name")); @@ -67,8 +66,8 @@ public class EndpointLinksResolverTests { given(endpoint.isEnableByDefault()).willReturn(true); given(endpoint.getOperations()).willReturn(operations); String requestUrl = "https://api.example.com/actuator"; - Map links = this.linksResolver - .resolveLinks(Collections.singletonList(endpoint), requestUrl); + Map links = new EndpointLinksResolver( + Collections.singletonList(endpoint)).resolveLinks(requestUrl); assertThat(links).hasSize(3); assertThat(links).hasEntrySatisfying("self", linkWithHref("https://api.example.com/actuator")); @@ -78,6 +77,39 @@ public class EndpointLinksResolverTests { linkWithHref("https://api.example.com/actuator/alpha/{name}")); } + @Test + public void resolvedLinksContainsALinkForServletEndpoint() { + ExposableServletEndpoint servletEndpoint = mock(ExposableServletEndpoint.class); + given(servletEndpoint.getId()).willReturn("alpha"); + given(servletEndpoint.isEnableByDefault()).willReturn(true); + given(servletEndpoint.getRootPath()).willReturn("alpha"); + String requestUrl = "https://api.example.com/actuator"; + Map links = new EndpointLinksResolver( + Collections.singletonList(servletEndpoint)).resolveLinks(requestUrl); + assertThat(links).hasSize(2); + assertThat(links).hasEntrySatisfying("self", + linkWithHref("https://api.example.com/actuator")); + assertThat(links).hasEntrySatisfying("alpha", + linkWithHref("https://api.example.com/actuator/alpha")); + } + + @Test + public void resolvedLinksContainsALinkForControllerEndpoint() { + ExposableControllerEndpoint controllerEndpoint = mock( + ExposableControllerEndpoint.class); + given(controllerEndpoint.getId()).willReturn("alpha"); + given(controllerEndpoint.isEnableByDefault()).willReturn(true); + given(controllerEndpoint.getRootPath()).willReturn("alpha"); + String requestUrl = "https://api.example.com/actuator"; + Map links = new EndpointLinksResolver( + Collections.singletonList(controllerEndpoint)).resolveLinks(requestUrl); + assertThat(links).hasSize(2); + assertThat(links).hasEntrySatisfying("self", + linkWithHref("https://api.example.com/actuator")); + assertThat(links).hasEntrySatisfying("alpha", + linkWithHref("https://api.example.com/actuator/alpha")); + } + private WebOperation operationWithPath(String path, String id) { WebOperationRequestPredicate predicate = new WebOperationRequestPredicate(path, WebEndpointHttpMethod.GET, Collections.emptyList(), diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java index b2b3fbbbfb4..ce75518d70f 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java @@ -35,6 +35,7 @@ import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.model.Resource; import org.glassfish.jersey.servlet.ServletContainer; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests; @@ -110,7 +111,8 @@ public class JerseyWebEndpointIntegrationTests extends Collection resources = new JerseyEndpointResourceFactory() .createEndpointResources( new EndpointMapping(environment.getProperty("endpointPath")), - endpointDiscoverer.getEndpoints(), endpointMediaTypes); + endpointDiscoverer.getEndpoints(), endpointMediaTypes, + new EndpointLinksResolver(endpointDiscoverer.getEndpoints())); resourceConfig.registerResources(new HashSet<>(resources)); resourceConfig.register(JacksonFeature.class); resourceConfig.register(new ObjectMapperContextResolver(new ObjectMapper()), diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java index a1db891b195..ff33eb81050 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java @@ -22,6 +22,7 @@ import java.util.Arrays; import org.junit.Test; import reactor.core.publisher.Mono; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests; @@ -132,7 +133,8 @@ public class WebFluxEndpointIntegrationTests return new WebFluxEndpointHandlerMapping( new EndpointMapping(environment.getProperty("endpointPath")), endpointDiscoverer.getEndpoints(), endpointMediaTypes, - corsConfiguration); + corsConfiguration, + new EndpointLinksResolver(endpointDiscoverer.getEndpoints())); } @Bean diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java index baa71015583..43995e74dbe 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java @@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletResponse; import org.junit.Test; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests; @@ -129,7 +130,8 @@ public class MvcWebEndpointIntegrationTests extends return new WebMvcEndpointHandlerMapping( new EndpointMapping(environment.getProperty("endpointPath")), endpointDiscoverer.getEndpoints(), endpointMediaTypes, - corsConfiguration); + corsConfiguration, + new EndpointLinksResolver(endpointDiscoverer.getEndpoints())); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/JerseyEndpointsRunner.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/JerseyEndpointsRunner.java index 2eadacbc1bb..b0076658f94 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/JerseyEndpointsRunner.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/JerseyEndpointsRunner.java @@ -31,6 +31,7 @@ import org.junit.runners.model.InitializationError; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.PathMapper; @@ -104,7 +105,8 @@ class JerseyEndpointsRunner extends AbstractWebEndpointRunner { Collections.emptyList(), Collections.emptyList()); Collection resources = new JerseyEndpointResourceFactory() .createEndpointResources(new EndpointMapping("/actuator"), - discoverer.getEndpoints(), endpointMediaTypes); + discoverer.getEndpoints(), endpointMediaTypes, + new EndpointLinksResolver(discoverer.getEndpoints())); config.registerResources(new HashSet<>(resources)); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebFluxEndpointsRunner.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebFluxEndpointsRunner.java index 83e1d5dce71..8735d051418 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebFluxEndpointsRunner.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebFluxEndpointsRunner.java @@ -25,6 +25,7 @@ import org.junit.runners.model.InitializationError; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.PathMapper; @@ -110,7 +111,8 @@ class WebFluxEndpointsRunner extends AbstractWebEndpointRunner { Collections.emptyList(), Collections.emptyList()); return new WebFluxEndpointHandlerMapping(new EndpointMapping("/actuator"), discoverer.getEndpoints(), endpointMediaTypes, - new CorsConfiguration()); + new CorsConfiguration(), + new EndpointLinksResolver(discoverer.getEndpoints())); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebMvcEndpointRunner.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebMvcEndpointRunner.java index a3b71d4ed6c..da424a19952 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebMvcEndpointRunner.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebMvcEndpointRunner.java @@ -25,6 +25,7 @@ import org.junit.runners.model.InitializationError; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.PathMapper; @@ -93,7 +94,8 @@ class WebMvcEndpointRunner extends AbstractWebEndpointRunner { Collections.emptyList(), Collections.emptyList()); return new WebMvcEndpointHandlerMapping(new EndpointMapping("/actuator"), discoverer.getEndpoints(), endpointMediaTypes, - new CorsConfiguration()); + new CorsConfiguration(), + new EndpointLinksResolver(discoverer.getEndpoints())); } }