Adds metadata field on Route and RouteDefinition

Fixes gh-1021
This commit is contained in:
Stefan_Stus
2019-06-21 19:07:38 +03:00
committed by Spencer Gibb
parent 73eee9c559
commit 5c83f45ed9
10 changed files with 273 additions and 16 deletions

View File

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

View File

@@ -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<String> filters = new ArrayList<>();

View File

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

View File

@@ -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<GatewayFilter> gatewayFilters;
private final Map<String, Object> metadata;
@Deprecated
private Route(String id, URI uri, int order,
AsyncPredicate<ServerWebExchange> predicate,
List<GatewayFilter> gatewayFilters) {
this(id, uri, order, predicate, gatewayFilters, new HashMap<>());
}
private Route(String id, URI uri, int order,
AsyncPredicate<ServerWebExchange> predicate,
List<GatewayFilter> gatewayFilters, Map<String, Object> 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<String, Object> 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<GatewayFilter> gatewayFilters = new ArrayList<>();
protected Map<String, Object> metadata = new HashMap<>();
protected AbstractBuilder() {
}
@@ -177,6 +206,21 @@ public class Route implements Ordered {
return getThis();
}
public B replaceMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
return getThis();
}
public B metadata(Map<String, Object> metadata) {
this.metadata.putAll(metadata);
return getThis();
}
public B metadata(String key, Object value) {
this.metadata.put(key, value);
return getThis();
}
public abstract AsyncPredicate<ServerWebExchange> getPredicate();
public B replaceFilters(List<GatewayFilter> 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);
}
}

View File

@@ -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<String, Object> metadata = new HashMap<>();
private int order = 0;
public RouteDefinition() {
@@ -115,6 +119,14 @@ public class RouteDefinition {
this.order = order;
}
public Map<String, Object> getMetadata() {
return metadata;
}
public void setMetadata(Map<String, Object> 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 + '}';
}
}

View File

@@ -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<String, Object>) map).hasSize(3)
.containsEntry("optionName", "OptionValue")
.containsEntry("iAmNumber", 1).containsEntry("compositeObject",
Maps.newHashMap("name", "value")));
}
@SpringBootConfiguration
@EnableAutoConfiguration
@Import(PermitAllSecurityConfiguration.class)

View File

@@ -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<String, Object> originalMetadata = Maps.newHashMap("key", "value");
Map<String, Object> 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<String, Object> originalMetadata = Maps.newHashMap("key", "value");
Map<String, Object> 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<String, Object> 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");
}
}

View File

@@ -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<String, Object> 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");
}
}

View File

@@ -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<String, Object> 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 {

View File

@@ -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