Move from WebFilter to GatewayFilter.

Add GatewayHostHandlerMapping
This commit is contained in:
Spencer Gibb
2016-11-23 15:20:06 -07:00
parent 21614831b7
commit 5b3bf045bf
11 changed files with 499 additions and 128 deletions

View File

@@ -20,10 +20,9 @@ public class GatewayApplication {
private static final Log log = LogFactory.getLog(GatewayApplication.class);
// TODO: only apply filters to zuul?
@Bean
@Order(501)
public WebFilter modifyResponseFilter() {
public GatewayFilter modifyResponseFilter() {
return (exchange, chain) -> {
log.info("modifyResponseFilter start");
exchange.getResponse().getHeaders().add("X-My-Custom", "MyCustomValue");
@@ -33,7 +32,7 @@ public class GatewayApplication {
@Bean
@Order(502)
public WebFilter postFilter() {
public GatewayFilter postFilter() {
return (exchange, chain) -> {
log.info("postFilter start");
return chain.filter(exchange).then(postFilterWork(exchange));
@@ -49,7 +48,6 @@ public class GatewayApplication {
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(GatewayApplication.class)
//TODO: howto do programatically
.run(args);
}
}

View File

@@ -2,7 +2,7 @@ package org.springframework.cloud.gateway;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filters.FindRouteFilter;
import org.springframework.cloud.gateway.filters.RouteToUrlFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
@@ -22,8 +22,8 @@ public class GatewayConfiguration {
}
@Bean
public FindRouteFilter findRouteFilter(GatewayProperties properties) {
return new FindRouteFilter(properties);
public RouteToUrlFilter findRouteFilter(GatewayProperties properties) {
return new RouteToUrlFilter(properties);
}
@Bean
@@ -37,18 +37,18 @@ public class GatewayConfiguration {
}
@Bean
public GatewayHandlerMapping gatewayHandlerMapping(GatewayProperties properties, GatewayWebHandler gatewayWebHandler) {
return new GatewayHandlerMapping(properties, gatewayWebHandler);
public GatewayFilteringWebHandler gatewayFilteringWebHandler(GatewayWebHandler gatewayWebHandler, GatewayFilter[] filters) {
return new GatewayFilteringWebHandler(gatewayWebHandler, filters);
}
/*@Bean
public GatewayWebReactiveConfigurer gatewayWebReactiveConfiguration() {
return new GatewayWebReactiveConfigurer();
@Bean
public GatewayUrlHandlerMapping gatewayUrlHandlerMapping(GatewayProperties properties, GatewayFilteringWebHandler webHandler) {
return new GatewayUrlHandlerMapping(properties, webHandler);
}
@Bean
public GatewayHostHandlerMapping gatewayHostHandlerMapping(GatewayProperties properties, GatewayFilteringWebHandler webHandler) {
return new GatewayHostHandlerMapping(properties, webHandler);
}
public static class GatewayWebReactiveConfigurer implements WebReactiveConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
}
}*/
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2002-2015 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
*
* http://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;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
/**
* Contract for interception-style, chained processing of Web requests that may
* be used to implement cross-cutting, application-agnostic requirements such
* as security, timeouts, and others.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public interface GatewayFilter {
default boolean shouldFilter(ServerWebExchange exchange) {
return true;
}
/**
* Process the Web request and (optionally) delegate to the next
* {@code WebFilter} through the given {@link WebFilterChain}.
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return {@code Mono<Void>} to indicate when request processing is complete
*/
Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain);
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2002-2016 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
*
* http://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;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.handler.WebHandlerDecorator;
import reactor.core.publisher.Mono;
/**
* WebHandler that delegates to a chain of {@link GatewayFilter} instances and then
* to the target {@link WebHandler}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class GatewayFilteringWebHandler extends WebHandlerDecorator {
private final List<GatewayFilter> filters;
public GatewayFilteringWebHandler(WebHandler targetHandler, GatewayFilter... filters) {
super(targetHandler);
this.filters = initList(filters);
}
private static List<GatewayFilter> initList(GatewayFilter[] list) {
return (list != null ? Collections.unmodifiableList(Arrays.asList(list)) : Collections.emptyList());
}
/**
* Return a read-only list of the configured filters.
*/
public List<GatewayFilter> getFilters() {
return this.filters;
}
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
return new DefaultWebFilterChain().filter(exchange);
}
private class DefaultWebFilterChain implements WebFilterChain {
private int index;
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
if (this.index < filters.size()) {
GatewayFilter filter = filters.get(this.index++);
if (filter.shouldFilter(exchange)) {
return filter.filter(exchange, this);
} else {
return this.filter(exchange);
}
}
else {
return getDelegate().handle(exchange);
}
}
}
}

View File

@@ -1,38 +0,0 @@
package org.springframework.cloud.gateway;
import org.springframework.beans.BeansException;
import org.springframework.cloud.gateway.GatewayProperties.Route;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.handler.AbstractUrlHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import java.util.Map;
/**
* @author Spencer Gibb
*/
public class GatewayHandlerMapping extends AbstractUrlHandlerMapping {
private GatewayProperties properties;
private GatewayWebHandler gatewayWebHandler;
public GatewayHandlerMapping(GatewayProperties properties, GatewayWebHandler gatewayWebHandler) {
this.properties = properties;
this.gatewayWebHandler = gatewayWebHandler;
}
@Override
protected void initApplicationContext() throws BeansException {
super.initApplicationContext();
registerHandlers(this.properties.getRoutes());
}
protected void registerHandlers(Map<String, Route> routes) {
for (Route route : routes.values()) {
if (StringUtils.hasText(route.getRequestPath())) {
registerHandler(route.getRequestPath(), this.gatewayWebHandler);
}
}
}
}

View File

@@ -0,0 +1,218 @@
package org.springframework.cloud.gateway;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.cloud.gateway.GatewayProperties.Route;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.handler.AbstractHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import reactor.core.publisher.Mono;
/**
* @author Spencer Gibb
*/
public class GatewayHostHandlerMapping extends AbstractHandlerMapping {
private GatewayProperties properties;
private WebHandler webHandler;
private final Map<String, Object> handlerMap = new LinkedHashMap<>();
public GatewayHostHandlerMapping(GatewayProperties properties, WebHandler webHandler) {
this.properties = properties;
this.webHandler = webHandler;
}
@Override
protected void initApplicationContext() throws BeansException {
super.initApplicationContext();
registerHandlers(this.properties.getRoutes());
setPathMatcher(new AntPathMatcher("."));
}
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
String host = exchange.getRequest().getHeaders().getFirst("Host");
Object handler;
try {
handler = lookupHandler(host, exchange);
}
catch (Exception ex) {
return Mono.error(ex);
}
if (handler != null && logger.isDebugEnabled()) {
logger.debug("Mapping [" + host + "] to " + handler);
}
else if (handler == null && logger.isTraceEnabled()) {
logger.trace("No handler mapping found for [" + host + "]");
}
return Mono.justOrEmpty(handler);
}
/**
* Look up a handler instance for the given URL path.
*
* <p>Supports direct matches, e.g. a registered "/test" matches "/test",
* and various Ant-style pattern matches, e.g. a registered "/t*" matches
* both "/test" and "/team". For details, see the AntPathMatcher class.
*
* <p>Looks for the most exact pattern, where most exact is defined as
* the longest path pattern.
*
* @param host URL the bean is mapped to
* @param exchange the current exchange
* @return the associated handler instance, or {@code null} if not found
* @see org.springframework.util.AntPathMatcher
*/
protected Object lookupHandler(String host, ServerWebExchange exchange) throws Exception {
// Direct match?
Object handler = this.handlerMap.get(host);
if (handler != null) {
return handleMatch(handler, host, exchange);
}
// Pattern match?
List<String> matches = new ArrayList<>();
for (String pattern : this.handlerMap.keySet()) {
if (getPathMatcher().match(pattern, host)) {
matches.add(pattern);
}
}
String bestMatch = null;
Comparator<String> comparator = getPathMatcher().getPatternComparator(host);
if (!matches.isEmpty()) {
Collections.sort(matches, comparator);
if (logger.isDebugEnabled()) {
logger.debug("Matching patterns for request [" + host + "] are " + matches);
}
bestMatch = matches.get(0);
}
if (bestMatch != null) {
handler = this.handlerMap.get(bestMatch);
if (handler == null) {
Assert.isTrue(bestMatch.endsWith("/"));
handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1));
}
return handleMatch(handler, bestMatch, exchange);
}
// No handler found...
return null;
}
private Object handleMatch(Object handler, String bestMatch,
ServerWebExchange exchange) throws Exception {
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = getApplicationContext().getBean(handlerName);
}
if (handler instanceof RouteHolder) {
RouteHolder holder = (RouteHolder) handler;
exchange.getAttributes().put("gatewayRoute", holder.route);
return holder.webHandler;
}
validateHandler(handler, exchange);
exchange.getAttributes().put(BEST_MATCHING_PATTERN_ATTRIBUTE, bestMatch);
return handler;
}
/**
* Validate the given handler against the current request.
* <p>The default implementation is empty. Can be overridden in subclasses,
* for example to enforce specific preconditions expressed in URL mappings.
* @param handler the handler object to validate
* @param exchange current exchange
* @throws Exception if validation failed
*/
@SuppressWarnings("UnusedParameters")
protected void validateHandler(Object handler, ServerWebExchange exchange) throws Exception {
}
protected void registerHandlers(Map<String, Route> routes) {
for (Route route : routes.values()) {
if (StringUtils.hasText(route.getRequestHost())) {
registerHandler(route.getRequestHost(), new RouteHolder(route, this.webHandler));
}
}
}
/**
* Register the specified handler for the given host path.
* @param hostPath the host the bean should be mapped to
* @param handler the handler instance or handler bean name String
* (a bean name will automatically be resolved into the corresponding handler bean)
* @throws BeansException if the handler couldn't be registered
* @throws IllegalStateException if there is a conflicting handler registered
*/
protected void registerHandler(String hostPath, Object handler) throws BeansException, IllegalStateException {
Assert.notNull(hostPath, "host path must not be null");
Assert.notNull(handler, "Handler object must not be null");
Object resolvedHandler = handler;
// Eagerly resolve handler if referencing singleton via name.
/*if (!this.lazyInitHandlers && handler instanceof String) {
String handlerName = (String) handler;
if (getApplicationContext().isSingleton(handlerName)) {
resolvedHandler = getApplicationContext().getBean(handlerName);
}
}*/
Object mappedHandler = this.handlerMap.get(hostPath);
if (mappedHandler != null) {
if (mappedHandler != resolvedHandler) {
throw new IllegalStateException(
"Cannot map " + getHandlerDescription(handler) + " to host path [" + hostPath +
"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
}
}
else {
this.handlerMap.put(hostPath, resolvedHandler);
if (logger.isInfoEnabled()) {
logger.info("Mapped host path [" + hostPath + "] onto " + getHandlerDescription(handler));
}
}
}
private String getHandlerDescription(Object handler) {
String desc;
if (handler instanceof String) {
desc = "'" + handler + "'";
} else if (handler instanceof RouteHolder) {
desc = "of type [" + ((RouteHolder) handler).webHandler.getClass() + "]";
} else {
desc = "of type [" + handler.getClass() + "]";
}
return "handler " + desc;
}
private class RouteHolder {
private final Route route;
private final WebHandler webHandler;
public RouteHolder(Route route, WebHandler webHandler) {
this.route = route;
this.webHandler = webHandler;
}
}
}

View File

@@ -0,0 +1,62 @@
package org.springframework.cloud.gateway;
import org.springframework.beans.BeansException;
import org.springframework.cloud.gateway.GatewayProperties.Route;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.handler.AbstractUrlHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import reactor.core.publisher.Mono;
import java.util.Map;
/**
* @author Spencer Gibb
*/
public class GatewayUrlHandlerMapping extends AbstractUrlHandlerMapping {
private GatewayProperties properties;
private WebHandler webHandler;
public GatewayUrlHandlerMapping(GatewayProperties properties, WebHandler webHandler) {
this.properties = properties;
this.webHandler = webHandler;
}
@Override
protected void initApplicationContext() throws BeansException {
super.initApplicationContext();
registerHandlers(this.properties.getRoutes());
}
@Override
public Mono<Object> getHandler(ServerWebExchange exchange) {
return super.getHandler(exchange).map(o -> {
if (o instanceof RouteHolder) {
RouteHolder holder = (RouteHolder) o;
exchange.getAttributes().put("gatewayRoute", holder.route);
return holder.webHandler;
}
return o;
});
}
protected void registerHandlers(Map<String, Route> routes) {
for (Route route : routes.values()) {
if (StringUtils.hasText(route.getRequestPath())) {
registerHandler(route.getRequestPath(), new RouteHolder(route, this.webHandler));
}
}
}
private class RouteHolder {
private final Route route;
private final WebHandler webHandler;
public RouteHolder(Route route, WebHandler webHandler) {
this.route = route;
this.webHandler = webHandler;
}
}
}

View File

@@ -28,6 +28,7 @@ public class GatewayWebHandler implements WebHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
//TODO: move to constant
Optional<URI> requestUrl = exchange.getAttribute("requestUrl");
ServerHttpRequest request = exchange.getRequest();
ClientRequest<Void> clientRequest = ClientRequest

View File

@@ -1,70 +0,0 @@
package org.springframework.cloud.gateway.filters;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.GatewayProperties;
import org.springframework.cloud.gateway.GatewayProperties.Route;
import org.springframework.cloud.gateway.GatewayApplication;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import static org.springframework.util.StringUtils.hasText;
/**
* @author Spencer Gibb
*/
public class FindRouteFilter implements WebFilter, Ordered {
private static final Log log = LogFactory.getLog(GatewayApplication.class);
private final GatewayProperties properties;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final AntPathMatcher hostMatcher = new AntPathMatcher(".");
public FindRouteFilter(GatewayProperties properties) {
this.properties = properties;
}
@Override
public int getOrder() {
return 500;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
log.info("FindRouteFilter start");
//TODO:
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();
String path = uri.getPath();
String host = uri.getHost();
for (Route route : this.properties.getRoutes().values()) {
if (hasText(route.getRequestPath())
&& this.pathMatcher.match(route.getRequestPath(), path)) {
populateRequestUrl(exchange, request, route);
// TODO: this stuff needs to move into GatewayHandlerMapping
// otherwise, only path based routing works
} else if (hasText(route.getRequestHost())
&& this.hostMatcher.match(route.getRequestHost(), host)) {
populateRequestUrl(exchange, request, route);
}
}
return chain.filter(exchange);
}
private void populateRequestUrl(ServerWebExchange exchange, ServerHttpRequest request, Route route) {
URI requestUrl = UriComponentsBuilder.fromHttpRequest(request)
.uri(route.getDownstreamUrl())
.build(true)
.toUri();
exchange.getAttributes().put("requestUrl", requestUrl);
}
}

View File

@@ -0,0 +1,62 @@
package org.springframework.cloud.gateway.filters;
import java.net.URI;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.GatewayFilter;
import org.springframework.cloud.gateway.GatewayProperties;
import org.springframework.cloud.gateway.GatewayProperties.Route;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
/**
* @author Spencer Gibb
*/
public class RouteToUrlFilter implements GatewayFilter, Ordered {
private static final Log log = LogFactory.getLog(RouteToUrlFilter.class);
private final GatewayProperties properties;
public RouteToUrlFilter(GatewayProperties properties) {
this.properties = properties;
}
@Override
public int getOrder() {
// TODO: move to constant
return 500;
}
// TODO: do we really need shouldFilter or just move the if into filter?
@Override
public boolean shouldFilter(ServerWebExchange exchange) {
//TODO: move to constant
return exchange.getAttributes().containsKey("gatewayRoute");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
log.info("RouteToUrlFilter start");
Object gatewayRoute = exchange.getAttributes().get("gatewayRoute");
if (!(gatewayRoute instanceof Route)) {
return Mono.error(new IllegalStateException("gatewayRoute" +
" not an instance of " + Route.class.getSimpleName() +
", is " + gatewayRoute.getClass()));
}
Route route = (Route) gatewayRoute;
URI requestUrl = UriComponentsBuilder.fromHttpRequest(exchange.getRequest())
.uri(route.getDownstreamUrl())
.build(true)
.toUri();
//TODO: move to constant
exchange.getAttributes().put("requestUrl", requestUrl);
return chain.filter(exchange);
}
}

View File

@@ -1,3 +1,4 @@
spring:
resources:
# TODO: how to add this programmatically
@@ -5,7 +6,12 @@ spring:
cloud:
gateway:
routes:
test1:
requestPath: /**
# requestHost: '**.example.org'
# test1:
# requestPath: /**
# downstreamUrl: http://httpbin.org:80
test2:
requestHost: '**.example.org'
downstreamUrl: http://httpbin.org:80
logging:
level:
org.springframework.cloud.gateway: TRACE