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:
@@ -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<? 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
|
||||
* 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<String, Link> resolveLinks(Collection<ExposableWebEndpoint> endpoints,
|
||||
String requestUrl) {
|
||||
public Map<String, Link> resolveLinks(String requestUrl) {
|
||||
String normalizedUrl = normalizeRequestUrl(requestUrl);
|
||||
Map<String, Link> 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<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) {
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Resource> createEndpointResources(EndpointMapping endpointMapping,
|
||||
Collection<ExposableWebEndpoint> endpoints,
|
||||
EndpointMediaTypes endpointMediaTypes) {
|
||||
EndpointMediaTypes endpointMediaTypes, EndpointLinksResolver linksResolver) {
|
||||
List<Resource> 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<ExposableWebEndpoint> 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<ContainerRequestContext, Response> {
|
||||
|
||||
private final Collection<ExposableWebEndpoint> endpoints;
|
||||
|
||||
private final EndpointLinksResolver linksResolver;
|
||||
|
||||
private EndpointLinksInflector(Collection<ExposableWebEndpoint> endpoints,
|
||||
EndpointLinksResolver linksResolver) {
|
||||
this.endpoints = endpoints;
|
||||
private EndpointLinksInflector(EndpointLinksResolver linksResolver) {
|
||||
this.linksResolver = linksResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response apply(ContainerRequestContext request) {
|
||||
Map<String, Link> links = this.linksResolver.resolveLinks(this.endpoints,
|
||||
request.getUriInfo().getAbsolutePath().toString());
|
||||
Map<String, Link> links = this.linksResolver
|
||||
.resolveLinks(request.getUriInfo().getAbsolutePath().toString());
|
||||
return Response.ok(Collections.singletonMap("_links", links)).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ExposableWebEndpoint> 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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<ExposableWebEndpoint> 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<String, Map<String, Link>> 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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String, Link> links = this.linksResolver.resolveLinks(Collections.emptyList(),
|
||||
"https://api.example.com/actuator/");
|
||||
Map<String, Link> 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<String, Link> links = this.linksResolver.resolveLinks(Collections.emptyList(),
|
||||
"https://api.example.com/actuator");
|
||||
Map<String, Link> 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<WebOperation> 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<String, Link> links = this.linksResolver
|
||||
.resolveLinks(Collections.singletonList(endpoint), requestUrl);
|
||||
Map<String, Link> 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<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) {
|
||||
WebOperationRequestPredicate predicate = new WebOperationRequestPredicate(path,
|
||||
WebEndpointHttpMethod.GET, Collections.emptyList(),
|
||||
|
||||
@@ -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<Resource> 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()),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Resource> resources = new JerseyEndpointResourceFactory()
|
||||
.createEndpointResources(new EndpointMapping("/actuator"),
|
||||
discoverer.getEndpoints(), endpointMediaTypes);
|
||||
discoverer.getEndpoints(), endpointMediaTypes,
|
||||
new EndpointLinksResolver(discoverer.getEndpoints()));
|
||||
config.registerResources(new HashSet<>(resources));
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user