diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java index fd31bf68..ab521961 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java @@ -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 globalFilters; @@ -88,6 +100,53 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis this.routeLocator = routeLocator; } + private List getAvailableEndpointsForClass(String className) { + try { + MetadataReader metadataReader = new SimpleMetadataReaderFactory().getMetadataReader(className); + Set 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 mergeEndpoints(List listA, + List listB) { + Map> 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> getEndpoints() { + List 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; diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayEndpointInfo.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayEndpointInfo.java new file mode 100644 index 00000000..ffb1cf21 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayEndpointInfo.java @@ -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 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 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); + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java index 193761a5..3909761a 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java @@ -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 responseBody = result.getResponseBody(); + assertThat(responseBody).isNotEmpty(); + }); + } + @Test public void testRefresh() { testClient.post().uri("http://localhost:" + port + "/actuator/gateway/refresh").exchange().expectStatus()