diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 392397ed..e54fd92b 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -1476,6 +1476,34 @@ spring: For some usages of the gateway, properties will be adequate, but some production use cases will benefit from loading configuration from an external source, such as a database. Future milestone versions will have `RouteDefinitionLocator` implementations based off of Spring Data Repositories such as: Redis, MongoDB and Cassandra. +== Route metadata configuration +Additional parameters can be configured for each route using metadata: + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: route_with_metadata + uri: https://example.org + metadata: + optionName: "OptionValue" + compositeObject: + name: "value" + iAmNumber: 1 +---- + +All metadata properties could be acquired from exchange: +``` +Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); +// get all metadata properties +route.getMetadata(); +// get a single metadata property +route.getMetadata(someKey); +``` + === Fluent Java Routes API To allow for simple configuration in Java, there is a fluent API defined in the `RouteLocatorBuilder` bean. @@ -1495,6 +1523,7 @@ public RouteLocator customRouteLocator(RouteLocatorBuilder builder, ThrottleGate .filters(f -> f.addResponseHeader("X-AnotherHeader", "baz")) .uri("http://httpbin.org:80") + .metadata("key", "value") ) .route(r -> r.order(-1) .host("**.throttle.org").and().path("/get") @@ -1503,6 +1532,7 @@ public RouteLocator customRouteLocator(RouteLocatorBuilder builder, ThrottleGate 10, TimeUnit.SECONDS))) .uri("http://httpbin.org:80") + .metadata("key", "value") ) .build(); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java index 244d976a..59fb9603 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java @@ -32,6 +32,7 @@ import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.route.RouteDefinitionWriter; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.http.ResponseEntity; +import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -59,6 +60,9 @@ public class GatewayControllerEndpoint extends AbstractGatewayControllerEndpoint r.put("uri", route.getUri().toString()); r.put("order", route.getOrder()); r.put("predicate", route.getPredicate().toString()); + if (!CollectionUtils.isEmpty(route.getMetadata())) { + r.put("metadata", route.getMetadata()); + } ArrayList filters = new ArrayList<>(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayLegacyControllerEndpoint.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayLegacyControllerEndpoint.java index 69c65152..2c954260 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayLegacyControllerEndpoint.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayLegacyControllerEndpoint.java @@ -33,6 +33,7 @@ import org.springframework.cloud.gateway.route.RouteDefinitionLocator; import org.springframework.cloud.gateway.route.RouteDefinitionWriter; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.http.ResponseEntity; +import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -81,6 +82,10 @@ public class GatewayLegacyControllerEndpoint extends AbstractGatewayControllerEn obj.put("filters", filters); } + if (!CollectionUtils.isEmpty(route.getMetadata())) { + obj.put("metadata", route.getMetadata()); + } + if (!obj.isEmpty()) { r.put("route_object", obj); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java index 1c5ed592..ce360026 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java @@ -21,7 +21,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.Predicate; @@ -50,14 +52,24 @@ public class Route implements Ordered { private final List gatewayFilters; + private final Map metadata; + + @Deprecated private Route(String id, URI uri, int order, AsyncPredicate predicate, List gatewayFilters) { + this(id, uri, order, predicate, gatewayFilters, new HashMap<>()); + } + + private Route(String id, URI uri, int order, + AsyncPredicate predicate, + List gatewayFilters, Map metadata) { this.id = id; this.uri = uri; this.order = order; this.predicate = predicate; this.gatewayFilters = gatewayFilters; + this.metadata = metadata; } public static Builder builder() { @@ -65,8 +77,12 @@ public class Route implements Ordered { } public static Builder builder(RouteDefinition routeDefinition) { - return new Builder().id(routeDefinition.getId()).uri(routeDefinition.getUri()) - .order(routeDefinition.getOrder()); + // @formatter:off + return new Builder().id(routeDefinition.getId()) + .uri(routeDefinition.getUri()) + .order(routeDefinition.getOrder()) + .metadata(routeDefinition.getMetadata()); + // @formatter:on } public static AsyncBuilder async() { @@ -74,8 +90,12 @@ public class Route implements Ordered { } public static AsyncBuilder async(RouteDefinition routeDefinition) { + // @formatter:off return new AsyncBuilder().id(routeDefinition.getId()) - .uri(routeDefinition.getUri()).order(routeDefinition.getOrder()); + .uri(routeDefinition.getUri()) + .order(routeDefinition.getOrder()) + .metadata(routeDefinition.getMetadata()); + // @formatter:on } public String getId() { @@ -98,6 +118,10 @@ public class Route implements Ordered { return Collections.unmodifiableList(this.gatewayFilters); } + public Map getMetadata() { + return Collections.unmodifiableMap(metadata); + } + @Override public boolean equals(Object o) { if (this == o) { @@ -107,15 +131,17 @@ public class Route implements Ordered { return false; } Route route = (Route) o; - return Objects.equals(id, route.id) && Objects.equals(uri, route.uri) - && Objects.equals(order, route.order) - && Objects.equals(predicate, route.predicate) - && Objects.equals(gatewayFilters, route.gatewayFilters); + return this.order == route.order && Objects.equals(this.id, route.id) + && Objects.equals(this.uri, route.uri) + && Objects.equals(this.predicate, route.predicate) + && Objects.equals(this.gatewayFilters, route.gatewayFilters) + && Objects.equals(this.metadata, route.metadata); } @Override public int hashCode() { - return Objects.hash(id, uri, predicate, gatewayFilters); + return Objects.hash(this.id, this.uri, this.order, this.predicate, + this.gatewayFilters, this.metadata); } @Override @@ -126,6 +152,7 @@ public class Route implements Ordered { sb.append(", order=").append(order); sb.append(", predicate=").append(predicate); sb.append(", gatewayFilters=").append(gatewayFilters); + sb.append(", metadata=").append(metadata); sb.append('}'); return sb.toString(); } @@ -140,6 +167,8 @@ public class Route implements Ordered { protected List gatewayFilters = new ArrayList<>(); + protected Map metadata = new HashMap<>(); + protected AbstractBuilder() { } @@ -177,6 +206,21 @@ public class Route implements Ordered { return getThis(); } + public B replaceMetadata(Map metadata) { + this.metadata = metadata; + return getThis(); + } + + public B metadata(Map metadata) { + this.metadata.putAll(metadata); + return getThis(); + } + + public B metadata(String key, Object value) { + this.metadata.put(key, value); + return getThis(); + } + public abstract AsyncPredicate getPredicate(); public B replaceFilters(List gatewayFilters) { @@ -205,7 +249,7 @@ public class Route implements Ordered { Assert.notNull(predicate, "predicate can not be null"); return new Route(this.id, this.uri, this.order, predicate, - this.gatewayFilters); + this.gatewayFilters, this.metadata); } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinition.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinition.java index 8c4e2aff..1e24375f 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinition.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinition.java @@ -18,7 +18,9 @@ package org.springframework.cloud.gateway.route; import java.net.URI; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -52,6 +54,8 @@ public class RouteDefinition { @NotNull private URI uri; + private Map metadata = new HashMap<>(); + private int order = 0; public RouteDefinition() { @@ -115,6 +119,14 @@ public class RouteDefinition { this.order = order; } + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -123,22 +135,25 @@ public class RouteDefinition { if (o == null || getClass() != o.getClass()) { return false; } - RouteDefinition routeDefinition = (RouteDefinition) o; - return Objects.equals(id, routeDefinition.id) - && Objects.equals(predicates, routeDefinition.predicates) - && Objects.equals(order, routeDefinition.order) - && Objects.equals(uri, routeDefinition.uri); + RouteDefinition that = (RouteDefinition) o; + return this.order == that.order && Objects.equals(this.id, that.id) + && Objects.equals(this.predicates, that.predicates) + && Objects.equals(this.filters, that.filters) + && Objects.equals(this.uri, that.uri) + && Objects.equals(this.metadata, that.metadata); } @Override public int hashCode() { - return Objects.hash(id, predicates, uri); + return Objects.hash(this.id, this.predicates, this.filters, this.uri, + this.metadata, this.order); } @Override public String toString() { return "RouteDefinition{" + "id='" + id + '\'' + ", predicates=" + predicates - + ", filters=" + filters + ", uri=" + uri + ", order=" + order + '}'; + + ", filters=" + filters + ", uri=" + uri + ", order=" + order + + ", metadata=" + metadata + '}'; } } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java index c52ddd2a..4a3291c1 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java @@ -19,6 +19,7 @@ package org.springframework.cloud.gateway.actuate; import java.util.List; import java.util.Map; +import org.assertj.core.util.Maps; import org.junit.Test; import org.junit.runner.RunWith; @@ -80,6 +81,18 @@ public class GatewayControllerEndpointTests { }); } + @Test + public void testRouteReturnsMetadata() { + testClient.get() + .uri("http://localhost:" + port + + "/actuator/gateway/routes/route_with_metadata") + .exchange().expectStatus().isOk().expectBody().jsonPath("$.metadata") + .value(map -> assertThat((Map) map).hasSize(3) + .containsEntry("optionName", "OptionValue") + .containsEntry("iAmNumber", 1).containsEntry("compositeObject", + Maps.newHashMap("name", "value"))); + } + @SpringBootConfiguration @EnableAutoConfiguration @Import(PermitAllSecurityConfiguration.class) diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteDefinitionTest.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteDefinitionTest.java new file mode 100644 index 00000000..7c6a250a --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteDefinitionTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2019 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.route; + +import java.util.Map; + +import org.assertj.core.util.Maps; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stefan Stus + */ +public class RouteDefinitionTest { + + @Test + public void addRouteDefinitionKeepsExistingMetadata() { + Map originalMetadata = Maps.newHashMap("key", "value"); + Map newMetadata = Maps.newHashMap("key2", "value2"); + + RouteDefinition routeDefinition = new RouteDefinition(); + routeDefinition.setMetadata(originalMetadata); + routeDefinition.getMetadata().putAll(newMetadata); + + assertThat(routeDefinition.getMetadata()).hasSize(2) + .containsAllEntriesOf(originalMetadata).containsAllEntriesOf(newMetadata); + } + + @Test + public void setRouteDefinitionReplacesExistingMetadata() { + Map originalMetadata = Maps.newHashMap("key", "value"); + Map newMetadata = Maps.newHashMap("key2", "value2"); + + RouteDefinition routeDefinition = new RouteDefinition(); + routeDefinition.setMetadata(originalMetadata); + routeDefinition.setMetadata(newMetadata); + + assertThat(routeDefinition.getMetadata()).isEqualTo(newMetadata); + } + + @Test + public void addSingleMetadataEntryKeepsOriginalMetadata() { + Map originalMetadata = Maps.newHashMap("key", "value"); + + RouteDefinition routeDefinition = new RouteDefinition(); + routeDefinition.setMetadata(originalMetadata); + routeDefinition.getMetadata().put("key2", "value2"); + + assertThat(routeDefinition.getMetadata()).hasSize(2) + .containsAllEntriesOf(originalMetadata).containsEntry("key2", "value2"); + } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteTests.java index 603553e9..e1df9109 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteTests.java @@ -16,6 +16,10 @@ package org.springframework.cloud.gateway.route; +import java.net.URI; +import java.util.HashMap; + +import org.assertj.core.util.Maps; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -57,4 +61,37 @@ public class RouteTests { Route.async().id("1").predicate(exchange -> true).uri("/pathonly"); } + @Test + public void defaultMetadataToEmpty() { + Route route = Route.async().id("1").predicate(exchange -> true) + .uri("http://acme.com:8080").build(); + + assertThat(route.getMetadata()).isEmpty(); + } + + @Test + public void isAbleToAddMetadata() { + Route route = Route.async().id("1").predicate(exchange -> true) + .uri("http://acme.com:8080").metadata(Maps.newHashMap("key", "value")) + .metadata("key2", "value2").build(); + + assertThat(route.getMetadata()).hasSize(2).containsEntry("key", "value") + .containsEntry("key2", "value2"); + } + + @Test + public void metadataIsAddedFromDefinition() { + RouteDefinition definition = new RouteDefinition(); + definition.setId("1"); + definition.setUri(URI.create("http://acme.com:8080")); + HashMap metadata = new HashMap<>(); + metadata.put("key", "value"); + metadata.put("key2", "value2"); + definition.setMetadata(metadata); + Route route = Route.async(definition).predicate(exchange -> true).build(); + + assertThat(route.getMetadata()).hasSize(2).containsEntry("key", "value") + .containsEntry("key2", "value2"); + } + } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/builder/RouteBuilderTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/builder/RouteBuilderTests.java index 4dd5c17d..09550ded 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/builder/RouteBuilderTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/builder/RouteBuilderTests.java @@ -17,9 +17,12 @@ package org.springframework.cloud.gateway.route.builder; import java.net.URI; +import java.util.Map; +import org.assertj.core.util.Maps; import org.junit.Test; import org.junit.runner.RunWith; + import reactor.test.StepVerifier; import org.springframework.beans.factory.annotation.Autowired; @@ -63,6 +66,31 @@ public class RouteBuilderTests { .expectComplete().verify(); } + @Test + public void testRouteOptionsPropagatedToRoute() { + Map routeMetadata = Maps.newHashMap("key", "value"); + RouteLocator routeLocator = this.routeLocatorBuilder.routes() + .route("test1", r -> r.host("*.somehost.org").and().path("/somepath") + .filters(f -> f.addRequestHeader("header1", "header-value-1")) + .uri("http://someuri").metadata("key", "value")) + .route("test2", r -> r.host("*.somehost2.org") + .filters(f -> f.addResponseHeader("header-response-1", + "header-response-1")) + .uri("https://httpbin.org:9090")) + .build(); + + StepVerifier.create(routeLocator.getRoutes()) + .expectNextMatches( + r -> r.getId().equals("test1") && r.getFilters().size() == 1 + && r.getUri().equals(URI.create("http://someuri:80")) + && r.getMetadata().equals(routeMetadata)) + .expectNextMatches(r -> r.getId().equals("test2") + && r.getFilters().size() == 1 + && r.getUri().equals(URI.create("https://httpbin.org:9090")) + && r.getMetadata().isEmpty()) + .expectComplete().verify(); + } + @EnableAutoConfiguration @Configuration public static class SpringConfig { diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index c7a25b73..e2393e10 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -387,6 +387,19 @@ spring: - name: Path args: pattern: /** + # ===================================== + - id: route_with_metadata + uri: ${test.uri} + order: 10000 + predicates: + - name: Path + args: + pattern: /route_with_metadata + metadata: + optionName: "OptionValue" + compositeObject: + name: "value" + iAmNumber: 1 hystrix.command.successcmd.execution.isolation.thread.timeoutInMilliseconds: 5000