Add links to child paths for /actuator/gateway endpoint

This commit is contained in:
Marta Medio
2023-11-23 13:11:28 +01:00
parent cba61f7b94
commit 63fc79c5b6
3 changed files with 139 additions and 0 deletions

View File

@@ -16,12 +16,16 @@
package org.springframework.cloud.gateway.actuate;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -34,6 +38,7 @@ import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
@@ -42,6 +47,9 @@ import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.Ordered;
import org.springframework.core.type.MethodMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
@@ -51,6 +59,8 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ResponseStatusException;
@@ -61,6 +71,8 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis
private static final Log log = LogFactory.getLog(GatewayControllerEndpoint.class);
private static final String ENDPOINT_PREFIX = "/actuator/gateway";
protected RouteDefinitionLocator routeDefinitionLocator;
protected List<GlobalFilter> globalFilters;
@@ -88,6 +100,53 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis
this.routeLocator = routeLocator;
}
private List<GatewayEndpointInfo> getAvailableEndpointsForClass(String className) {
try {
MetadataReader metadataReader = new SimpleMetadataReaderFactory().getMetadataReader(className);
Set<MethodMetadata> annotatedMethods = metadataReader.getAnnotationMetadata()
.getAnnotatedMethods(RequestMapping.class.getName());
return annotatedMethods.stream().map(method -> new GatewayEndpointInfo(ENDPOINT_PREFIX
+ ((String[]) method.getAnnotationAttributes(RequestMapping.class.getName()).get("path"))[0],
((RequestMethod[]) method.getAnnotationAttributes(RequestMapping.class.getName()).get("method"))[0]
.name()))
.collect(Collectors.toList());
}
catch (IOException exception) {
log.warn(exception.getMessage());
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, exception.getMessage());
}
}
public static List<GatewayEndpointInfo> mergeEndpoints(List<GatewayEndpointInfo> listA,
List<GatewayEndpointInfo> listB) {
Map<String, List<String>> mergedMap = new HashMap<>();
Stream.concat(listA.stream(), listB.stream()).forEach(e -> mergedMap
.computeIfAbsent(e.getHref(), k -> new ArrayList<>()).addAll(Arrays.asList(e.getMethods())));
return mergedMap.entrySet().stream().map(entry -> new GatewayEndpointInfo(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
GatewayEndpointInfo generateHref(Route r, GatewayEndpointInfo path) {
return new GatewayEndpointInfo(path.getHref().replace("{id}", r.getId()), Arrays.asList(path.getMethods()));
}
@GetMapping("/")
public Mono<List<GatewayEndpointInfo>> getEndpoints() {
List<GatewayEndpointInfo> endpoints = mergeEndpoints(
getAvailableEndpointsForClass(AbstractGatewayControllerEndpoint.class.getName()),
getAvailableEndpointsForClass(GatewayControllerEndpoint.class.getName()));
return Flux.fromIterable(endpoints).map(p -> p)
.flatMap(path -> this.routeLocator.getRoutes().map(r -> generateHref(r, path)).distinct().collectList()
.flatMapMany(Flux::fromIterable))
.distinct() // Ensure overall uniqueness
.collectList();
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2013-2020 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
*
* https://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.cloud.gateway.actuate;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* @author Marta Medio
*/
class GatewayEndpointInfo {
private String href;
private List<String> methods;
public String getHref() {
return href;
}
public void setHref(String href) {
this.href = href;
}
public String[] getMethods() {
return methods.stream().toArray(String[]::new);
}
GatewayEndpointInfo(String href, String method) {
this.href = href;
this.methods = Collections.singletonList(method);
}
GatewayEndpointInfo(String href, List<String> methods) {
this.href = href;
this.methods = methods;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
GatewayEndpointInfo that = (GatewayEndpointInfo) o;
return Objects.equals(href, that.href) && Objects.equals(methods, that.methods);
}
@Override
public int hashCode() {
return Objects.hash(href, methods);
}
}

View File

@@ -68,6 +68,15 @@ public class GatewayControllerEndpointTests {
@LocalServerPort
int port;
@Test
public void testEndpoints() {
testClient.get().uri("http://localhost:" + port + "/actuator/gateway").exchange().expectStatus().isOk()
.expectBody(List.class).consumeWith(result -> {
List<String> responseBody = result.getResponseBody();
assertThat(responseBody).isNotEmpty();
});
}
@Test
public void testRefresh() {
testClient.post().uri("http://localhost:" + port + "/actuator/gateway/refresh").exchange().expectStatus()