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
This commit is contained in:
Andy Wilkinson 2018-02-08 20:43:24 +00:00
parent 772d4cc739
commit 2993dccd1e
24 changed files with 480 additions and 72 deletions

View File

@ -410,6 +410,11 @@
<artifactId>hsqldb</artifactId> <artifactId>hsqldb</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-spring4</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.glassfish.jersey.media</groupId> <groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId> <artifactId>jersey-media-json-jackson</artifactId>

View File

@ -54,13 +54,15 @@ class CloudFoundryWebFluxEndpointHandlerMapping
private final CloudFoundrySecurityInterceptor securityInterceptor; private final CloudFoundrySecurityInterceptor securityInterceptor;
private final EndpointLinksResolver linksResolver = new EndpointLinksResolver(); private final EndpointLinksResolver linksResolver;
CloudFoundryWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping, CloudFoundryWebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints, Collection<ExposableWebEndpoint> endpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration, EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
CloudFoundrySecurityInterceptor securityInterceptor) { CloudFoundrySecurityInterceptor securityInterceptor,
EndpointLinksResolver linksResolver) {
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration); super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration);
this.linksResolver = linksResolver;
this.securityInterceptor = securityInterceptor; this.securityInterceptor = securityInterceptor;
} }
@ -83,7 +85,7 @@ class CloudFoundryWebFluxEndpointHandlerMapping
AccessLevel accessLevel = exchange AccessLevel accessLevel = exchange
.getAttribute(AccessLevel.REQUEST_ATTRIBUTE); .getAttribute(AccessLevel.REQUEST_ATTRIBUTE);
Map<String, Link> links = this.linksResolver Map<String, Link> links = this.linksResolver
.resolveLinks(getEndpoints(), request.getURI().toString()); .resolveLinks(request.getURI().toString());
return new ResponseEntity<>( return new ResponseEntity<>(
Collections.singletonMap("_links", Collections.singletonMap("_links",
getAccessibleLinks(accessLevel, links)), getAccessibleLinks(accessLevel, links)),

View File

@ -16,18 +16,25 @@
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; 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.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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; 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.PathMapper;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension; import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
@ -85,17 +92,22 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
@Bean @Bean
public CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebFluxEndpointHandlerMapping( public CloudFoundryWebFluxEndpointHandlerMapping cloudFoundryWebFluxEndpointHandlerMapping(
ParameterValueMapper parameterMapper, EndpointMediaTypes endpointMediaTypes, ParameterValueMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
WebClient.Builder webClientBuilder) { WebClient.Builder webClientBuilder,
ControllerEndpointsSupplier controllerEndpointsSupplier) {
CloudFoundryWebEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebEndpointDiscoverer( CloudFoundryWebEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebEndpointDiscoverer(
this.applicationContext, parameterMapper, endpointMediaTypes, this.applicationContext, parameterMapper, endpointMediaTypes,
PathMapper.useEndpointId(), Collections.emptyList(), PathMapper.useEndpointId(), Collections.emptyList(),
Collections.emptyList()); Collections.emptyList());
CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor( CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor(
webClientBuilder, this.applicationContext.getEnvironment()); webClientBuilder, this.applicationContext.getEnvironment());
Collection<ExposableWebEndpoint> webEndpoints = endpointDiscoverer.getEndpoints();
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
return new CloudFoundryWebFluxEndpointHandlerMapping( return new CloudFoundryWebFluxEndpointHandlerMapping(
new EndpointMapping("/cloudfoundryapplication"), new EndpointMapping("/cloudfoundryapplication"), webEndpoints,
endpointDiscoverer.getEndpoints(), endpointMediaTypes, endpointMediaTypes, getCorsConfiguration(), securityInterceptor,
getCorsConfiguration(), securityInterceptor); new EndpointLinksResolver(allEndpoints));
} }
private CloudFoundrySecurityInterceptor getSecurityInterceptor( private CloudFoundrySecurityInterceptor getSecurityInterceptor(

View File

@ -16,17 +16,25 @@
package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet; package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; 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.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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; 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.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.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointWebExtension; import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
@ -87,17 +95,24 @@ public class CloudFoundryActuatorAutoConfiguration {
@Bean @Bean
public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServletHandlerMapping( public CloudFoundryWebEndpointServletHandlerMapping cloudFoundryWebEndpointServletHandlerMapping(
ParameterValueMapper parameterMapper, EndpointMediaTypes endpointMediaTypes, ParameterValueMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
RestTemplateBuilder restTemplateBuilder) { RestTemplateBuilder restTemplateBuilder,
ServletEndpointsSupplier servletEndpointsSupplier,
ControllerEndpointsSupplier controllerEndpointsSupplier) {
CloudFoundryWebEndpointDiscoverer discoverer = new CloudFoundryWebEndpointDiscoverer( CloudFoundryWebEndpointDiscoverer discoverer = new CloudFoundryWebEndpointDiscoverer(
this.applicationContext, parameterMapper, endpointMediaTypes, this.applicationContext, parameterMapper, endpointMediaTypes,
PathMapper.useEndpointId(), Collections.emptyList(), PathMapper.useEndpointId(), Collections.emptyList(),
Collections.emptyList()); Collections.emptyList());
CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor( CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor(
restTemplateBuilder, this.applicationContext.getEnvironment()); restTemplateBuilder, this.applicationContext.getEnvironment());
Collection<ExposableWebEndpoint> webEndpoints = discoverer.getEndpoints();
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
return new CloudFoundryWebEndpointServletHandlerMapping( return new CloudFoundryWebEndpointServletHandlerMapping(
new EndpointMapping("/cloudfoundryapplication"), new EndpointMapping("/cloudfoundryapplication"), webEndpoints,
discoverer.getEndpoints(), endpointMediaTypes, getCorsConfiguration(), endpointMediaTypes, getCorsConfiguration(), securityInterceptor,
securityInterceptor); new EndpointLinksResolver(allEndpoints));
} }
private CloudFoundrySecurityInterceptor getSecurityInterceptor( private CloudFoundrySecurityInterceptor getSecurityInterceptor(

View File

@ -52,14 +52,16 @@ class CloudFoundryWebEndpointServletHandlerMapping
private final CloudFoundrySecurityInterceptor securityInterceptor; private final CloudFoundrySecurityInterceptor securityInterceptor;
private final EndpointLinksResolver linksResolver = new EndpointLinksResolver(); private final EndpointLinksResolver linksResolver;
CloudFoundryWebEndpointServletHandlerMapping(EndpointMapping endpointMapping, CloudFoundryWebEndpointServletHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints, Collection<ExposableWebEndpoint> endpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration, EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
CloudFoundrySecurityInterceptor securityInterceptor) { CloudFoundrySecurityInterceptor securityInterceptor,
EndpointLinksResolver linksResolver) {
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration); super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration);
this.securityInterceptor = securityInterceptor; this.securityInterceptor = securityInterceptor;
this.linksResolver = linksResolver;
} }
@Override @Override
@ -80,8 +82,8 @@ class CloudFoundryWebEndpointServletHandlerMapping
} }
AccessLevel accessLevel = (AccessLevel) request AccessLevel accessLevel = (AccessLevel) request
.getAttribute(AccessLevel.REQUEST_ATTRIBUTE); .getAttribute(AccessLevel.REQUEST_ATTRIBUTE);
Map<String, Link> links = this.linksResolver.resolveLinks(getEndpoints(), Map<String, Link> links = this.linksResolver
request.getRequestURL().toString()); .resolveLinks(request.getRequestURL().toString());
Map<String, Link> filteredLinks = new LinkedHashMap<>(); Map<String, Link> filteredLinks = new LinkedHashMap<>();
if (accessLevel == null) { if (accessLevel == null) {
return Collections.singletonMap("_links", filteredLinks); return Collections.singletonMap("_links", filteredLinks);

View File

@ -16,19 +16,25 @@
package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey; package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; 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.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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; 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.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@ -55,17 +61,24 @@ class JerseyWebEndpointManagementContextConfiguration {
@Bean @Bean
public ResourceConfigCustomizer webEndpointRegistrar( public ResourceConfigCustomizer webEndpointRegistrar(
WebEndpointsSupplier webEndpointsSupplier, WebEndpointsSupplier webEndpointsSupplier,
ServletEndpointsSupplier servletEndpointsSupplier,
ControllerEndpointsSupplier controllerEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes, EndpointMediaTypes endpointMediaTypes,
WebEndpointProperties webEndpointProperties) { WebEndpointProperties webEndpointProperties) {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
allEndpoints.addAll(webEndpointsSupplier.getEndpoints());
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
return (resourceConfig) -> { return (resourceConfig) -> {
JerseyEndpointResourceFactory resourceFactory = new JerseyEndpointResourceFactory(); JerseyEndpointResourceFactory resourceFactory = new JerseyEndpointResourceFactory();
String basePath = webEndpointProperties.getBasePath(); String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath); EndpointMapping endpointMapping = new EndpointMapping(basePath);
Collection<ExposableWebEndpoint> endpoints = Collections Collection<ExposableWebEndpoint> webEndpoints = Collections
.unmodifiableCollection(webEndpointsSupplier.getEndpoints()); .unmodifiableCollection(webEndpointsSupplier.getEndpoints());
resourceConfig.registerResources( resourceConfig.registerResources(
new HashSet<>(resourceFactory.createEndpointResources(endpointMapping, new HashSet<>(resourceFactory.createEndpointResources(endpointMapping,
endpoints, endpointMediaTypes))); webEndpoints, endpointMediaTypes,
new EndpointLinksResolver(allEndpoints))));
}; };
} }

View File

@ -16,12 +16,19 @@
package org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive; 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.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; 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.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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; 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.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.reactive.ControllerEndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.web.reactive.ControllerEndpointHandlerMapping;
@ -54,13 +61,18 @@ public class WebFluxEndpointManagementContextConfiguration {
@ConditionalOnMissingBean @ConditionalOnMissingBean
public WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping( public WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping(
WebEndpointsSupplier webEndpointsSupplier, WebEndpointsSupplier webEndpointsSupplier,
ControllerEndpointsSupplier controllerEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties) { WebEndpointProperties webEndpointProperties) {
EndpointMapping endpointMapping = new EndpointMapping( EndpointMapping endpointMapping = new EndpointMapping(
webEndpointProperties.getBasePath()); webEndpointProperties.getBasePath());
return new WebFluxEndpointHandlerMapping(endpointMapping, Collection<ExposableWebEndpoint> endpoints = webEndpointsSupplier.getEndpoints();
webEndpointsSupplier.getEndpoints(), endpointMediaTypes, List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
corsProperties.toCorsConfiguration()); allEndpoints.addAll(endpoints);
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
return new WebFluxEndpointHandlerMapping(endpointMapping, endpoints,
endpointMediaTypes, corsProperties.toCorsConfiguration(),
new EndpointLinksResolver(allEndpoints));
} }
@Bean @Bean

View File

@ -16,14 +16,22 @@
package org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet; 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.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; 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.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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; 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.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; 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.ControllerEndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@ -53,13 +61,21 @@ public class WebMvcEndpointManagementContextConfiguration {
@ConditionalOnMissingBean @ConditionalOnMissingBean
public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping( public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(
WebEndpointsSupplier webEndpointsSupplier, WebEndpointsSupplier webEndpointsSupplier,
ServletEndpointsSupplier servletEndpointsSupplier,
ControllerEndpointsSupplier controllerEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties) { WebEndpointProperties webEndpointProperties) {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier
.getEndpoints();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
EndpointMapping endpointMapping = new EndpointMapping( EndpointMapping endpointMapping = new EndpointMapping(
webEndpointProperties.getBasePath()); webEndpointProperties.getBasePath());
return new WebMvcEndpointHandlerMapping(endpointMapping, return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints,
webEndpointsSupplier.getEndpoints(), endpointMediaTypes, endpointMediaTypes, corsProperties.toCorsConfiguration(),
corsProperties.toCorsConfiguration()); new EndpointLinksResolver(allEndpoints));
} }
@Bean @Bean

View File

@ -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.annotation.WriteOperation;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; 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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.PathMapper; import org.springframework.boot.actuate.endpoint.web.PathMapper;
@ -221,7 +222,8 @@ public class CloudFoundryWebFluxEndpointIntegrationTests {
return new CloudFoundryWebFluxEndpointHandlerMapping( return new CloudFoundryWebFluxEndpointHandlerMapping(
new EndpointMapping("/cfApplication"), new EndpointMapping("/cfApplication"),
webEndpointDiscoverer.getEndpoints(), endpointMediaTypes, webEndpointDiscoverer.getEndpoints(), endpointMediaTypes,
corsConfiguration, interceptor); corsConfiguration, interceptor,
new EndpointLinksResolver(webEndpointDiscoverer.getEndpoints()));
} }
@Bean @Bean

View File

@ -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.annotation.WriteOperation;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; 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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.PathMapper; import org.springframework.boot.actuate.endpoint.web.PathMapper;
@ -207,7 +208,8 @@ public class CloudFoundryMvcWebEndpointIntegrationTests {
return new CloudFoundryWebEndpointServletHandlerMapping( return new CloudFoundryWebEndpointServletHandlerMapping(
new EndpointMapping("/cfApplication"), new EndpointMapping("/cfApplication"),
webEndpointDiscoverer.getEndpoints(), endpointMediaTypes, webEndpointDiscoverer.getEndpoints(), endpointMediaTypes,
corsConfiguration, interceptor); corsConfiguration, interceptor,
new EndpointLinksResolver(webEndpointDiscoverer.getEndpoints()));
} }
@Bean @Bean

View File

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

View File

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

View File

@ -16,6 +16,10 @@
package org.springframework.boot.actuate.autoconfigure.integrationtest; package org.springframework.boot.actuate.autoconfigure.integrationtest;
import java.util.function.Supplier;
import javax.servlet.http.HttpServlet;
import org.junit.After; import org.junit.After;
import org.junit.Test; 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.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; 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.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration; 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.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.util.TestPropertyValues; 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.context.annotation.Import;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.mock.web.MockServletContext; 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.test.web.servlet.setup.MockMvcConfigurer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; 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.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 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; 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()); 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() { private MockMvc createSecureMockMvc() {
return doCreateMockMvc(springSecurity()); return doCreateMockMvc(springSecurity());
} }
@ -142,4 +166,45 @@ public class WebMvcEndpointIntegrationTests {
} }
@ServletEndpoint(id = "servlet")
static class TestServletEndpoint implements Supplier<EndpointServlet> {
@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();
}
}
} }

View File

@ -20,6 +20,8 @@ import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
/** /**
* A resolver for {@link Link links} to web endpoints. * A resolver for {@link Link links} to web endpoints.
* *
@ -28,22 +30,29 @@ import java.util.Map;
*/ */
public class EndpointLinksResolver { public class EndpointLinksResolver {
private final Collection<? extends ExposableEndpoint<?>> endpoints;
public EndpointLinksResolver(Collection<? extends ExposableEndpoint<?>> endpoints) {
this.endpoints = endpoints;
}
/** /**
* Resolves links to the operations of the given {code webEndpoints} based on a * Resolves links to the known endpoints based on a request with the given
* request with the given {@code requestUrl}. * {@code requestUrl}.
* @param endpoints the source endpoints
* @param requestUrl the url of the request for the endpoint links * @param requestUrl the url of the request for the endpoint links
* @return the links * @return the links
*/ */
public Map<String, Link> resolveLinks(Collection<ExposableWebEndpoint> endpoints, public Map<String, Link> resolveLinks(String requestUrl) {
String requestUrl) {
String normalizedUrl = normalizeRequestUrl(requestUrl); String normalizedUrl = normalizeRequestUrl(requestUrl);
Map<String, Link> links = new LinkedHashMap<>(); Map<String, Link> links = new LinkedHashMap<>();
links.put("self", new Link(normalizedUrl)); links.put("self", new Link(normalizedUrl));
for (ExposableWebEndpoint endpoint : endpoints) { for (ExposableEndpoint<?> endpoint : this.endpoints) {
for (WebOperation operation : endpoint.getOperations()) { if (endpoint instanceof ExposableWebEndpoint) {
endpoints.stream().map(ExposableWebEndpoint::getId).forEach((id) -> links collectLinks(links, (ExposableWebEndpoint) endpoint, normalizedUrl);
.put(operation.getId(), createLink(normalizedUrl, operation))); }
else if (endpoint instanceof PathMappedEndpoint) {
links.put(endpoint.getId(), createLink(normalizedUrl,
((PathMappedEndpoint) endpoint).getRootPath()));
} }
} }
return links; return links;
@ -56,8 +65,18 @@ public class EndpointLinksResolver {
return requestUrl; return requestUrl;
} }
private void collectLinks(Map<String, Link> links, ExposableWebEndpoint endpoint,
String normalizedUrl) {
for (WebOperation operation : endpoint.getOperations()) {
links.put(operation.getId(), createLink(normalizedUrl, operation));
}
}
private Link createLink(String requestUrl, WebOperation 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)); return new Link(requestUrl + (path.startsWith("/") ? path : "/" + path));
} }

View File

@ -62,26 +62,25 @@ import org.springframework.util.StringUtils;
*/ */
public class JerseyEndpointResourceFactory { public class JerseyEndpointResourceFactory {
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
/** /**
* Creates {@link Resource Resources} for the operations of the given * Creates {@link Resource Resources} for the operations of the given
* {@code webEndpoints}. * {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints * @param endpointMapping the base mapping for all endpoints
* @param endpoints the web endpoints * @param endpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the 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 * @return the resources for the operations
*/ */
public Collection<Resource> createEndpointResources(EndpointMapping endpointMapping, public Collection<Resource> createEndpointResources(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints, Collection<ExposableWebEndpoint> endpoints,
EndpointMediaTypes endpointMediaTypes) { EndpointMediaTypes endpointMediaTypes, EndpointLinksResolver linksResolver) {
List<Resource> resources = new ArrayList<>(); List<Resource> resources = new ArrayList<>();
endpoints.stream().flatMap((endpoint) -> endpoint.getOperations().stream()) endpoints.stream().flatMap((endpoint) -> endpoint.getOperations().stream())
.map((operation) -> createResource(endpointMapping, operation)) .map((operation) -> createResource(endpointMapping, operation))
.forEach(resources::add); .forEach(resources::add);
if (StringUtils.hasText(endpointMapping.getPath())) { if (StringUtils.hasText(endpointMapping.getPath())) {
Resource resource = createEndpointLinksResource(endpointMapping.getPath(), Resource resource = createEndpointLinksResource(endpointMapping.getPath(),
endpoints, endpointMediaTypes); endpointMediaTypes, linksResolver);
resources.add(resource); resources.add(resource);
} }
return resources; return resources;
@ -105,14 +104,12 @@ public class JerseyEndpointResourceFactory {
} }
private Resource createEndpointLinksResource(String endpointPath, private Resource createEndpointLinksResource(String endpointPath,
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes, EndpointLinksResolver linksResolver) {
EndpointMediaTypes endpointMediaTypes) {
Builder resourceBuilder = Resource.builder().path(endpointPath); Builder resourceBuilder = Resource.builder().path(endpointPath);
resourceBuilder.addMethod("GET") resourceBuilder.addMethod("GET")
.produces(endpointMediaTypes.getProduced() .produces(endpointMediaTypes.getProduced()
.toArray(new String[endpointMediaTypes.getProduced().size()])) .toArray(new String[endpointMediaTypes.getProduced().size()]))
.handledBy(new EndpointLinksInflector(endpoints, .handledBy(new EndpointLinksInflector(linksResolver));
this.endpointLinksResolver));
return resourceBuilder.build(); return resourceBuilder.build();
} }
@ -266,20 +263,16 @@ public class JerseyEndpointResourceFactory {
private static final class EndpointLinksInflector private static final class EndpointLinksInflector
implements Inflector<ContainerRequestContext, Response> { implements Inflector<ContainerRequestContext, Response> {
private final Collection<ExposableWebEndpoint> endpoints;
private final EndpointLinksResolver linksResolver; private final EndpointLinksResolver linksResolver;
private EndpointLinksInflector(Collection<ExposableWebEndpoint> endpoints, private EndpointLinksInflector(EndpointLinksResolver linksResolver) {
EndpointLinksResolver linksResolver) {
this.endpoints = endpoints;
this.linksResolver = linksResolver; this.linksResolver = linksResolver;
} }
@Override @Override
public Response apply(ContainerRequestContext request) { public Response apply(ContainerRequestContext request) {
Map<String, Link> links = this.linksResolver.resolveLinks(this.endpoints, Map<String, Link> links = this.linksResolver
request.getUriInfo().getAbsolutePath().toString()); .resolveLinks(request.getUriInfo().getAbsolutePath().toString());
return Response.ok(Collections.singletonMap("_links", links)).build(); return Response.ok(Collections.singletonMap("_links", links)).build();
} }

View File

@ -43,7 +43,7 @@ import org.springframework.web.util.UriComponentsBuilder;
public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping
implements InitializingBean { implements InitializingBean {
private final EndpointLinksResolver linksResolver = new EndpointLinksResolver(); private final EndpointLinksResolver linksResolver;
/** /**
* Creates a new {@code WebFluxEndpointHandlerMapping} instance that provides mappings * Creates a new {@code WebFluxEndpointHandlerMapping} instance that provides mappings
@ -52,11 +52,14 @@ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandle
* @param endpoints the web endpoints * @param endpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints * @param endpointMediaTypes media types consumed and produced by the endpoints
* @param corsConfiguration the CORS configuration for the endpoints or {@code null} * @param corsConfiguration the CORS configuration for the endpoints or {@code null}
* @param linksResolver resolver for determining links to available endpoints
*/ */
public WebFluxEndpointHandlerMapping(EndpointMapping endpointMapping, public WebFluxEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints, Collection<ExposableWebEndpoint> endpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
EndpointLinksResolver linksResolver) {
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration); super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration);
this.linksResolver = linksResolver;
setOrder(-100); setOrder(-100);
} }
@ -66,7 +69,7 @@ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandle
String requestUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()) String requestUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
.replaceQuery(null).toUriString(); .replaceQuery(null).toUriString();
return Collections.singletonMap("_links", return Collections.singletonMap("_links",
this.linksResolver.resolveLinks(getEndpoints(), requestUri)); this.linksResolver.resolveLinks(requestUri));
} }
} }

View File

@ -42,7 +42,7 @@ import org.springframework.web.servlet.HandlerMapping;
*/ */
public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerMapping { public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerMapping {
private final EndpointLinksResolver linksResolver = new EndpointLinksResolver(); private final EndpointLinksResolver linksResolver;
/** /**
* Creates a new {@code WebMvcEndpointHandlerMapping} instance that provides mappings * Creates a new {@code WebMvcEndpointHandlerMapping} instance that provides mappings
@ -51,11 +51,14 @@ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerM
* @param endpoints the web endpoints * @param endpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints * @param endpointMediaTypes media types consumed and produced by the endpoints
* @param corsConfiguration the CORS configuration for the endpoints or {@code null} * @param corsConfiguration the CORS configuration for the endpoints or {@code null}
* @param linksResolver resolver for determining links to available endpoints
*/ */
public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints, Collection<ExposableWebEndpoint> endpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
EndpointLinksResolver linksResolver) {
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration); super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration);
this.linksResolver = linksResolver;
setOrder(-100); setOrder(-100);
} }
@ -63,9 +66,8 @@ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerM
@ResponseBody @ResponseBody
protected Map<String, Map<String, Link>> links(HttpServletRequest request, protected Map<String, Map<String, Link>> links(HttpServletRequest request,
HttpServletResponse response) { HttpServletResponse response) {
String requestUri = request.getRequestURL().toString();
return Collections.singletonMap("_links", return Collections.singletonMap("_links",
this.linksResolver.resolveLinks(getEndpoints(), requestUri)); this.linksResolver.resolveLinks(request.getRequestURL().toString()));
} }
} }

View File

@ -25,6 +25,7 @@ import org.assertj.core.api.Condition;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.actuate.endpoint.OperationType; 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.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
@ -37,12 +38,10 @@ import static org.mockito.Mockito.mock;
*/ */
public class EndpointLinksResolverTests { public class EndpointLinksResolverTests {
private final EndpointLinksResolver linksResolver = new EndpointLinksResolver();
@Test @Test
public void linkResolutionWithTrailingSlashStripsSlashOnSelfLink() { public void linkResolutionWithTrailingSlashStripsSlashOnSelfLink() {
Map<String, Link> links = this.linksResolver.resolveLinks(Collections.emptyList(), Map<String, Link> links = new EndpointLinksResolver(Collections.emptyList())
"https://api.example.com/actuator/"); .resolveLinks("https://api.example.com/actuator/");
assertThat(links).hasSize(1); assertThat(links).hasSize(1);
assertThat(links).hasEntrySatisfying("self", assertThat(links).hasEntrySatisfying("self",
linkWithHref("https://api.example.com/actuator")); linkWithHref("https://api.example.com/actuator"));
@ -50,15 +49,15 @@ public class EndpointLinksResolverTests {
@Test @Test
public void linkResolutionWithoutTrailingSlash() { public void linkResolutionWithoutTrailingSlash() {
Map<String, Link> links = this.linksResolver.resolveLinks(Collections.emptyList(), Map<String, Link> links = new EndpointLinksResolver(Collections.emptyList())
"https://api.example.com/actuator"); .resolveLinks("https://api.example.com/actuator");
assertThat(links).hasSize(1); assertThat(links).hasSize(1);
assertThat(links).hasEntrySatisfying("self", assertThat(links).hasEntrySatisfying("self",
linkWithHref("https://api.example.com/actuator")); linkWithHref("https://api.example.com/actuator"));
} }
@Test @Test
public void resolvedLinksContainsALinkForEachEndpointOperation() { public void resolvedLinksContainsALinkForEachWebEndpointOperation() {
List<WebOperation> operations = new ArrayList<>(); List<WebOperation> operations = new ArrayList<>();
operations.add(operationWithPath("/alpha", "alpha")); operations.add(operationWithPath("/alpha", "alpha"));
operations.add(operationWithPath("/alpha/{name}", "alpha-name")); operations.add(operationWithPath("/alpha/{name}", "alpha-name"));
@ -67,8 +66,8 @@ public class EndpointLinksResolverTests {
given(endpoint.isEnableByDefault()).willReturn(true); given(endpoint.isEnableByDefault()).willReturn(true);
given(endpoint.getOperations()).willReturn(operations); given(endpoint.getOperations()).willReturn(operations);
String requestUrl = "https://api.example.com/actuator"; String requestUrl = "https://api.example.com/actuator";
Map<String, Link> links = this.linksResolver Map<String, Link> links = new EndpointLinksResolver(
.resolveLinks(Collections.singletonList(endpoint), requestUrl); Collections.singletonList(endpoint)).resolveLinks(requestUrl);
assertThat(links).hasSize(3); assertThat(links).hasSize(3);
assertThat(links).hasEntrySatisfying("self", assertThat(links).hasEntrySatisfying("self",
linkWithHref("https://api.example.com/actuator")); linkWithHref("https://api.example.com/actuator"));
@ -78,6 +77,39 @@ public class EndpointLinksResolverTests {
linkWithHref("https://api.example.com/actuator/alpha/{name}")); 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<String, Link> 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<String, Link> 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) { private WebOperation operationWithPath(String path, String id) {
WebOperationRequestPredicate predicate = new WebOperationRequestPredicate(path, WebOperationRequestPredicate predicate = new WebOperationRequestPredicate(path,
WebEndpointHttpMethod.GET, Collections.emptyList(), WebEndpointHttpMethod.GET, Collections.emptyList(),

View File

@ -35,6 +35,7 @@ import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource; import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.servlet.ServletContainer; 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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests; import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests;
@ -110,7 +111,8 @@ public class JerseyWebEndpointIntegrationTests extends
Collection<Resource> resources = new JerseyEndpointResourceFactory() Collection<Resource> resources = new JerseyEndpointResourceFactory()
.createEndpointResources( .createEndpointResources(
new EndpointMapping(environment.getProperty("endpointPath")), new EndpointMapping(environment.getProperty("endpointPath")),
endpointDiscoverer.getEndpoints(), endpointMediaTypes); endpointDiscoverer.getEndpoints(), endpointMediaTypes,
new EndpointLinksResolver(endpointDiscoverer.getEndpoints()));
resourceConfig.registerResources(new HashSet<>(resources)); resourceConfig.registerResources(new HashSet<>(resources));
resourceConfig.register(JacksonFeature.class); resourceConfig.register(JacksonFeature.class);
resourceConfig.register(new ObjectMapperContextResolver(new ObjectMapper()), resourceConfig.register(new ObjectMapperContextResolver(new ObjectMapper()),

View File

@ -22,6 +22,7 @@ import java.util.Arrays;
import org.junit.Test; import org.junit.Test;
import reactor.core.publisher.Mono; 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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests; import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests;
@ -132,7 +133,8 @@ public class WebFluxEndpointIntegrationTests
return new WebFluxEndpointHandlerMapping( return new WebFluxEndpointHandlerMapping(
new EndpointMapping(environment.getProperty("endpointPath")), new EndpointMapping(environment.getProperty("endpointPath")),
endpointDiscoverer.getEndpoints(), endpointMediaTypes, endpointDiscoverer.getEndpoints(), endpointMediaTypes,
corsConfiguration); corsConfiguration,
new EndpointLinksResolver(endpointDiscoverer.getEndpoints()));
} }
@Bean @Bean

View File

@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletResponse;
import org.junit.Test; 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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests; import org.springframework.boot.actuate.endpoint.web.annotation.AbstractWebEndpointIntegrationTests;
@ -129,7 +130,8 @@ public class MvcWebEndpointIntegrationTests extends
return new WebMvcEndpointHandlerMapping( return new WebMvcEndpointHandlerMapping(
new EndpointMapping(environment.getProperty("endpointPath")), new EndpointMapping(environment.getProperty("endpointPath")),
endpointDiscoverer.getEndpoints(), endpointMediaTypes, endpointDiscoverer.getEndpoints(), endpointMediaTypes,
corsConfiguration); corsConfiguration,
new EndpointLinksResolver(endpointDiscoverer.getEndpoints()));
} }
} }

View File

@ -31,6 +31,7 @@ import org.junit.runners.model.InitializationError;
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; 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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.PathMapper; import org.springframework.boot.actuate.endpoint.web.PathMapper;
@ -104,7 +105,8 @@ class JerseyEndpointsRunner extends AbstractWebEndpointRunner {
Collections.emptyList(), Collections.emptyList()); Collections.emptyList(), Collections.emptyList());
Collection<Resource> resources = new JerseyEndpointResourceFactory() Collection<Resource> resources = new JerseyEndpointResourceFactory()
.createEndpointResources(new EndpointMapping("/actuator"), .createEndpointResources(new EndpointMapping("/actuator"),
discoverer.getEndpoints(), endpointMediaTypes); discoverer.getEndpoints(), endpointMediaTypes,
new EndpointLinksResolver(discoverer.getEndpoints()));
config.registerResources(new HashSet<>(resources)); config.registerResources(new HashSet<>(resources));
} }

View File

@ -25,6 +25,7 @@ import org.junit.runners.model.InitializationError;
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; 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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.PathMapper; import org.springframework.boot.actuate.endpoint.web.PathMapper;
@ -110,7 +111,8 @@ class WebFluxEndpointsRunner extends AbstractWebEndpointRunner {
Collections.emptyList(), Collections.emptyList()); Collections.emptyList(), Collections.emptyList());
return new WebFluxEndpointHandlerMapping(new EndpointMapping("/actuator"), return new WebFluxEndpointHandlerMapping(new EndpointMapping("/actuator"),
discoverer.getEndpoints(), endpointMediaTypes, discoverer.getEndpoints(), endpointMediaTypes,
new CorsConfiguration()); new CorsConfiguration(),
new EndpointLinksResolver(discoverer.getEndpoints()));
} }
} }

View File

@ -25,6 +25,7 @@ import org.junit.runners.model.InitializationError;
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; 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.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.PathMapper; import org.springframework.boot.actuate.endpoint.web.PathMapper;
@ -93,7 +94,8 @@ class WebMvcEndpointRunner extends AbstractWebEndpointRunner {
Collections.emptyList(), Collections.emptyList()); Collections.emptyList(), Collections.emptyList());
return new WebMvcEndpointHandlerMapping(new EndpointMapping("/actuator"), return new WebMvcEndpointHandlerMapping(new EndpointMapping("/actuator"),
discoverer.getEndpoints(), endpointMediaTypes, discoverer.getEndpoints(), endpointMediaTypes,
new CorsConfiguration()); new CorsConfiguration(),
new EndpointLinksResolver(discoverer.getEndpoints()));
} }
} }