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())); } }