initial per Route filter mechanism

This commit is contained in:
Spencer Gibb
2017-01-13 19:04:49 -07:00
parent d37bdba31d
commit b25fca4891
9 changed files with 288 additions and 11 deletions

View File

@@ -0,0 +1,90 @@
package org.springframework.cloud.gateway.config;
import java.util.Arrays;
import java.util.Objects;
import javax.validation.ValidationException;
import javax.validation.constraints.NotNull;
import static org.springframework.util.StringUtils.tokenizeToStringArray;
/**
* @author Spencer Gibb
*/
public class FilterDefinition {
@NotNull
private String name;
@NotNull
private String value;
private String[] args;
public FilterDefinition() {
}
public FilterDefinition(String text) {
int eqIdx = text.indexOf("=");
if (eqIdx <= 0) {
throw new ValidationException("Unable to parse FilterDefinition text '" + text + "'" +
", must be of the form name=value");
}
setName(text.substring(0, eqIdx));
String[] args = tokenizeToStringArray(text.substring(eqIdx+1), ",");
setValue(args[0]);
if (args.length > 1) {
setArgs(Arrays.copyOfRange(args, 1, args.length));
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String[] getArgs() {
return args;
}
public void setArgs(String[] args) {
this.args = args;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FilterDefinition that = (FilterDefinition) o;
return Objects.equals(name, that.name) &&
Objects.equals(value, that.value) &&
Arrays.equals(args, that.args);
}
@Override
public int hashCode() {
return Objects.hash(name, value, args);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("FilterDefinition{");
sb.append("name='").append(name).append('\'');
sb.append(", value='").append(value).append('\'');
sb.append(", args=").append(Arrays.toString(args));
sb.append('}');
return sb.toString();
}
}

View File

@@ -9,6 +9,9 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cloud.gateway.actuate.GatewayEndpoint;
import org.springframework.cloud.gateway.api.RouteReader;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.definition.AppendRequestHeaderFilter;
import org.springframework.cloud.gateway.filter.definition.AppendResponseHeaderFilter;
import org.springframework.cloud.gateway.filter.definition.GatewayFilterDefinition;
import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter;
import org.springframework.cloud.gateway.handler.GatewayFilteringWebHandler;
import org.springframework.cloud.gateway.handler.GatewayPredicateHandlerMapping;
@@ -60,10 +63,14 @@ public class GatewayAutoConfiguration {
}
@Bean
public GatewayFilteringWebHandler gatewayFilteringWebHandler(GatewayWebHandler gatewayWebHandler, GatewayFilter[] filters) {
return new GatewayFilteringWebHandler(gatewayWebHandler, filters);
public GatewayFilteringWebHandler gatewayFilteringWebHandler(GatewayWebHandler gatewayWebHandler,
List<GatewayFilter> filters,
List<GatewayFilterDefinition> filterDefinitions) {
return new GatewayFilteringWebHandler(gatewayWebHandler, filters, filterDefinitions);
}
// Predicate beans
@Bean
public GatewayPredicateHandlerMapping gatewayPredicateHandlerMapping(GatewayFilteringWebHandler webHandler,
List<GatewayPredicate> predicates,
@@ -101,6 +108,18 @@ public class GatewayAutoConfiguration {
return new UrlPredicate();
}
// Filter beans
@Bean
public AppendRequestHeaderFilter appendRequestHeaderFilter() {
return new AppendRequestHeaderFilter();
}
@Bean
public AppendResponseHeaderFilter appendResponseHeaderFilter() {
return new AppendResponseHeaderFilter();
}
@Configuration
@ConditionalOnClass(Endpoint.class)
protected static class GatewayActuatorConfiguration {

View File

@@ -24,6 +24,9 @@ public class Route {
@Valid
private List<PredicateDefinition> predicates = new ArrayList<>();
@Valid
private List<FilterDefinition> filters = new ArrayList<>();
@NotNull
private URI uri;
@@ -63,6 +66,14 @@ public class Route {
this.predicates = predicates;
}
public List<FilterDefinition> getFilters() {
return filters;
}
public void setFilters(List<FilterDefinition> filters) {
this.filters = filters;
}
public URI getUri() {
return uri;
}
@@ -91,6 +102,7 @@ public class Route {
return "Route{" +
"id='" + id + '\'' +
", predicates=" + predicates +
", filters=" + filters +
", uri=" + uri +
'}';
}

View File

@@ -0,0 +1,31 @@
package org.springframework.cloud.gateway.filter.definition;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.Assert;
/**
* @author Spencer Gibb
*/
public class AppendRequestHeaderFilter implements GatewayFilterDefinition {
@Override
public String getName() {
return "AppendRequestHeader";
}
@Override
public GatewayFilter apply(String header, String[] args) {
Assert.isTrue(args != null && args.length == 1,
"args must have one entry");
//TODO: caching can happen here
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate()
.header(header, args[0])
.build();
return chain.filter(exchange.mutate().request(request).build());
};
}
}

View File

@@ -0,0 +1,29 @@
package org.springframework.cloud.gateway.filter.definition;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.Assert;
/**
* @author Spencer Gibb
*/
public class AppendResponseHeaderFilter implements GatewayFilterDefinition {
@Override
public String getName() {
return "AppendResponseHeader";
}
@Override
public GatewayFilter apply(String header, String[] args) {
Assert.isTrue(args != null && args.length == 1,
"args must have one entry");
//TODO: caching can happen here
return (exchange, chain) -> {
exchange.getResponse().getHeaders().add(header, args[0]);
return chain.filter(exchange);
};
}
}

View File

@@ -0,0 +1,13 @@
package org.springframework.cloud.gateway.filter.definition;
import org.springframework.cloud.gateway.filter.GatewayFilter;
/**
* @author Spencer Gibb
*/
public interface GatewayFilterDefinition {
String getName();
GatewayFilter apply(String value, String[] args);
}

View File

@@ -16,11 +16,21 @@
package org.springframework.cloud.gateway.handler;
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.Optional;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.config.Route;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.definition.GatewayFilterDefinition;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebHandler;
@@ -28,6 +38,9 @@ import org.springframework.web.server.handler.WebHandlerDecorator;
import reactor.core.publisher.Mono;
import static java.util.Collections.emptyList;
import static org.springframework.cloud.gateway.filter.GatewayFilter.GATEWAY_ROUTE_ATTR;
/**
* WebHandler that delegates to a chain of {@link GatewayFilter} instances and then
* to the target {@link WebHandler}.
@@ -36,20 +49,22 @@ import reactor.core.publisher.Mono;
* @since 5.0
*/
public class GatewayFilteringWebHandler extends WebHandlerDecorator {
protected final Log logger = LogFactory.getLog(getClass());
private final List<GatewayFilter> filters;
private final Map<String, GatewayFilterDefinition> filterDefinitions = new HashMap<>();
public GatewayFilteringWebHandler(WebHandler targetHandler, GatewayFilter... filters) {
public GatewayFilteringWebHandler(WebHandler targetHandler, List<GatewayFilter> filters,
List<GatewayFilterDefinition> filterDefinitions) {
super(targetHandler);
this.filters = initList(filters);
initList(filterDefinitions).forEach(def -> this.filterDefinitions.put(def.getName(), def));
}
private static List<GatewayFilter> initList(GatewayFilter[] list) {
return (list != null ? Collections.unmodifiableList(Arrays.asList(list)) : Collections.emptyList());
private static <T> List<T> initList(List<T> list) {
return (list != null ? list : emptyList());
}
/**
* Return a read-only list of the configured filters.
*/
@@ -59,13 +74,48 @@ public class GatewayFilteringWebHandler extends WebHandlerDecorator {
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
return new DefaultWebFilterChain().filter(exchange);
//TODO: probably a java 8 stream way of doing this
ArrayList<GatewayFilter> routeFilters = new ArrayList<>(this.filters);
Optional<Route> route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
if (route.isPresent() && !route.get().getFilters().isEmpty()) {
routeFilters.addAll(loadFilters(route.get()));
}
return new DefaultWebFilterChain(routeFilters, getDelegate()).filter(exchange);
}
private Collection<GatewayFilter> loadFilters(Route route) {
return route.getFilters().stream()
.map(definition -> {
GatewayFilterDefinition filter = this.filterDefinitions.get(definition.getName());
if (filter == null) {
throw new IllegalArgumentException("Unable to find GatewayFilterDefinition with name " + definition.getName());
}
if (logger.isDebugEnabled()) {
List<String> args;
if (definition.getArgs() != null) {
args = Arrays.asList(definition.getArgs());
} else {
args = Collections.emptyList();
}
logger.debug("Route " + route.getId() + " applying filter "+ definition.getValue()
+ ", " + args + " to " + definition.getName());
}
return filter.apply(definition.getValue(), definition.getArgs());
})
.collect(Collectors.toList());
}
private class DefaultWebFilterChain implements WebFilterChain {
private static class DefaultWebFilterChain implements WebFilterChain {
private int index;
private final List<GatewayFilter> filters;
private final WebHandler delegate;
public DefaultWebFilterChain(List<GatewayFilter> filters, WebHandler delegate) {
this.filters = filters;
this.delegate = delegate;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
@@ -74,7 +124,7 @@ public class GatewayFilteringWebHandler extends WebHandlerDecorator {
return filter.filter(exchange, this);
}
else {
return getDelegate().handle(exchange);
return this.delegate.handle(exchange);
}
}
}

View File

@@ -116,6 +116,25 @@ public class GatewayIntegrationTests {
.verify(Duration.ofSeconds(3));
}
@Test
public void appendRequestHeaderFilterWorks() {
Mono<Map> result = webClient.exchange(
GET("http://localhost:" + port + "/headers")
.header("Host", "www.bar.org")
.build()
).then(response -> response.body(toMono(Map.class)));
StepVerifier.create(result)
.consumeNextWith(
response -> {
assertThat(response).containsKey("headers").isInstanceOf(Map.class);
Map<String, Object> headers = (Map<String, Object>) response.get("headers");
assertThat(headers).containsEntry("X-Request-Foo", "Bar");
})
.expectComplete()
.verify(Duration.ofSeconds(3));
}
@Test
public void postWorks() {
ClientRequest<Mono<String>> request = POST("http://localhost:" + port + "/post")
@@ -162,6 +181,8 @@ public class GatewayIntegrationTests {
.isEqualTo(GatewayPredicateHandlerMapping.class.getSimpleName());
assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER))
.isEqualTo("host_foo_path_headers_to_httpbin");
assertThat(httpHeaders.getFirst("X-Response-Foo"))
.isEqualTo("Bar");
assertThat(statusCode).isEqualTo(HttpStatus.OK);
})
.expectComplete()

View File

@@ -17,6 +17,18 @@ spring:
- Query=foo, ba.
- Query=baz
- Cookie=chocolate, ch.p
filters:
- AppendRequestHeader=X-Request-Foo, Bar
- AppendResponseHeader=X-Response-Foo, Bar
# =====================================
- id: host_bar_path_headers_to_httpbin
uri: http://httpbin.org:80
predicates:
- Host=**.bar.org
- Url=/headers
filters:
- AppendRequestHeader=X-Request-Foo, Bar
# =====================================
- id: default_path_to_httpbin
@@ -30,4 +42,4 @@ logging:
management:
context-path: /admin
# port: 8081
port: 8081