diff --git a/mvnw b/mvnw index a1ba1bf5..3cc6f681 100755 --- a/mvnw +++ b/mvnw @@ -136,7 +136,6 @@ if $mingw ; then M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? fi if [ -z "$JAVA_HOME" ]; then diff --git a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 38d9e869..2c79f65e 100644 --- a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -8,9 +8,11 @@ import org.springframework.cloud.gateway.actuate.GatewayEndpoint; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter; import org.springframework.cloud.gateway.handler.GatewayFilteringWebHandler; -import org.springframework.cloud.gateway.handler.GatewayHostHandlerMapping; -import org.springframework.cloud.gateway.handler.GatewayUrlHandlerMapping; +import org.springframework.cloud.gateway.handler.HostPredicateFactory; +import org.springframework.cloud.gateway.handler.GatewayPredicateFactory; import org.springframework.cloud.gateway.handler.GatewayWebHandler; +import org.springframework.cloud.gateway.handler.ServerWebExchangePredicateHandlerMapping; +import org.springframework.cloud.gateway.handler.UrlPredicateFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.reactive.ReactorClientHttpConnector; @@ -52,13 +54,20 @@ public class GatewayAutoConfiguration { } @Bean - public GatewayUrlHandlerMapping gatewayUrlHandlerMapping(GatewayProperties properties, GatewayFilteringWebHandler webHandler) { - return new GatewayUrlHandlerMapping(properties, webHandler); + public ServerWebExchangePredicateHandlerMapping serverWebExchangePredicateHandlerMapping(GatewayProperties properties, + GatewayFilteringWebHandler webHandler, + List predicateFactories) { + return new ServerWebExchangePredicateHandlerMapping(webHandler, predicateFactories, properties); } @Bean - public GatewayHostHandlerMapping gatewayHostHandlerMapping(GatewayProperties properties, GatewayFilteringWebHandler webHandler) { - return new GatewayHostHandlerMapping(properties, webHandler); + public HostPredicateFactory hostPredicateFactory() { + return new HostPredicateFactory(); + } + + @Bean + public UrlPredicateFactory urlPredicateFactory() { + return new UrlPredicateFactory(); } @Configuration diff --git a/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java b/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java index d64dd33f..07bfcbf2 100644 --- a/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java +++ b/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java @@ -1,10 +1,15 @@ package org.springframework.cloud.gateway.config; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.hibernate.validator.constraints.NotEmpty; import org.springframework.boot.context.properties.ConfigurationProperties; -import java.net.URI; -import java.util.LinkedHashMap; -import java.util.Map; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; /** * @author Spencer Gibb @@ -12,40 +17,46 @@ import java.util.Map; @ConfigurationProperties("spring.cloud.gateway") public class GatewayProperties { - /** * Map of route names to properties. */ - private Map routes = new LinkedHashMap<>(); + @NotNull + @Valid + private List routes = new ArrayList<>(); - public Map getRoutes() { + public List getRoutes() { return routes; } - public void setRoutes(Map routes) { + public void setRoutes(List routes) { this.routes = routes; } public static class Route { + @NotEmpty private String id; - private String requestPath; - private String requestHost; + + @NotEmpty + @Valid + private List predicates = new ArrayList<>(); + + @NotNull private URI downstreamUrl; - public String getRequestPath() { - return this.requestPath; + public String getId() { + return id; } - public void setRequestPath(String requestPath) { - this.requestPath = requestPath; + public void setId(String id) { + this.id = id; } - public String getRequestHost() { - return requestHost; + public List getPredicates() { + return predicates; } - public void setRequestHost(String requestHost) { - this.requestHost = requestHost; + public void setPredicates(List predicates) { + this.predicates = predicates; } public URI getDownstreamUrl() { @@ -56,13 +67,72 @@ public class GatewayProperties { this.downstreamUrl = downstreamUrl; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Route route = (Route) o; + return Objects.equals(id, route.id) && + Objects.equals(predicates, route.predicates) && + Objects.equals(downstreamUrl, route.downstreamUrl); + } + + @Override + public int hashCode() { + return Objects.hash(id, predicates, downstreamUrl); + } + @Override public String toString() { return "Route{" + "id='" + id + '\'' + - ", requestPath='" + requestPath + '\'' + - ", requestHost='" + requestHost + '\'' + - ", downstreamUrl='" + downstreamUrl + '\'' + + ", predicates=" + predicates + + ", downstreamUrl=" + downstreamUrl + + '}'; + } + } + + public static class Predicate { + @NotNull + private String name; + @NotNull + private String value; + + 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; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Predicate predicate = (Predicate) o; + return Objects.equals(name, predicate.name) && + Objects.equals(value, predicate.value); + } + + @Override + public int hashCode() { + return Objects.hash(name, value); + } + + @Override + public String toString() { + return "Predicate{" + + "name='" + name + '\'' + + ", value='" + value + '\'' + '}'; } } diff --git a/src/main/java/org/springframework/cloud/gateway/handler/GatewayHostHandlerMapping.java b/src/main/java/org/springframework/cloud/gateway/handler/GatewayHostHandlerMapping.java deleted file mode 100644 index 13989ee3..00000000 --- a/src/main/java/org/springframework/cloud/gateway/handler/GatewayHostHandlerMapping.java +++ /dev/null @@ -1,224 +0,0 @@ -package org.springframework.cloud.gateway.handler; - -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.filter.GatewayFilter; -import org.springframework.cloud.gateway.config.GatewayProperties; -import org.springframework.cloud.gateway.config.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; - -import static org.springframework.cloud.gateway.filter.GatewayFilter.GATEWAY_ROUTE_ATTR; - -/** - * @author Spencer Gibb - */ -public class GatewayHostHandlerMapping extends AbstractHandlerMapping { - - private GatewayProperties properties; - private WebHandler webHandler; - - private final Map handlerMap = new LinkedHashMap<>(); - - public GatewayHostHandlerMapping(GatewayProperties properties, WebHandler webHandler) { - this.properties = properties; - this.webHandler = webHandler; - setOrder(-1); - } - - @Override - protected void initApplicationContext() throws BeansException { - super.initApplicationContext(); - registerHandlers(this.properties.getRoutes()); - setPathMatcher(new AntPathMatcher(".")); - } - - @Override - protected Mono getHandlerInternal(ServerWebExchange exchange) { - exchange.getAttributes().put(GatewayFilter.GATEWAY_HANDLER_MAPPER_ATTR, getClass().getSimpleName()); - 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. - * - *

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. - * - *

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 matches = new ArrayList<>(); - for (String pattern : this.handlerMap.keySet()) { - if (getPathMatcher().match(pattern, host)) { - matches.add(pattern); - } - } - String bestMatch = null; - Comparator 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(GATEWAY_ROUTE_ATTR, 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. - *

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 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; - } - } - -} diff --git a/src/main/java/org/springframework/cloud/gateway/handler/GatewayPredicateFactory.java b/src/main/java/org/springframework/cloud/gateway/handler/GatewayPredicateFactory.java new file mode 100644 index 00000000..0f050f94 --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/handler/GatewayPredicateFactory.java @@ -0,0 +1,15 @@ +package org.springframework.cloud.gateway.handler; + +import org.springframework.web.server.ServerWebExchange; + +import java.util.function.Predicate; + +/** + * @author Spencer Gibb + */ +public interface GatewayPredicateFactory { + + String getName(); + + Predicate create(String value); +} diff --git a/src/main/java/org/springframework/cloud/gateway/handler/GatewayUrlHandlerMapping.java b/src/main/java/org/springframework/cloud/gateway/handler/GatewayUrlHandlerMapping.java deleted file mode 100644 index f67bd9a4..00000000 --- a/src/main/java/org/springframework/cloud/gateway/handler/GatewayUrlHandlerMapping.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.springframework.cloud.gateway.handler; - -import org.springframework.beans.BeansException; -import org.springframework.cloud.gateway.filter.GatewayFilter; -import org.springframework.cloud.gateway.config.GatewayProperties; -import org.springframework.cloud.gateway.config.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; - setOrder(0); - } - - @Override - protected void initApplicationContext() throws BeansException { - super.initApplicationContext(); - registerHandlers(this.properties.getRoutes()); - } - - @Override - public Mono getHandler(ServerWebExchange exchange) { - return super.getHandler(exchange).map(o -> { - if (o instanceof RouteHolder) { - RouteHolder holder = (RouteHolder) o; - exchange.getAttributes().put(GatewayFilter.GATEWAY_HANDLER_MAPPER_ATTR, - GatewayUrlHandlerMapping.this.getClass().getSimpleName()); - exchange.getAttributes().put(GatewayFilter.GATEWAY_ROUTE_ATTR, holder.route); - return holder.webHandler; - } - return o; - }); - } - - protected void registerHandlers(Map 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; - } - } - -} diff --git a/src/main/java/org/springframework/cloud/gateway/handler/HostPredicateFactory.java b/src/main/java/org/springframework/cloud/gateway/handler/HostPredicateFactory.java new file mode 100644 index 00000000..80f6c324 --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/handler/HostPredicateFactory.java @@ -0,0 +1,33 @@ +package org.springframework.cloud.gateway.handler; + +import java.util.function.Predicate; + +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.web.server.ServerWebExchange; + +/** + * @author Spencer Gibb + */ +public class HostPredicateFactory implements GatewayPredicateFactory { + + private PathMatcher pathMatcher = new AntPathMatcher("."); + + public void setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + } + + @Override + public String getName() { + return "Host"; + } + + @Override + public Predicate create(String pattern) { + //TODO: caching can happen here + return exchange -> { + String host = exchange.getRequest().getHeaders().getFirst("Host"); + return this.pathMatcher.match(pattern, host); + }; + } +} diff --git a/src/main/java/org/springframework/cloud/gateway/handler/ServerWebExchangePredicateHandlerMapping.java b/src/main/java/org/springframework/cloud/gateway/handler/ServerWebExchangePredicateHandlerMapping.java new file mode 100644 index 00000000..ca2e562d --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/handler/ServerWebExchangePredicateHandlerMapping.java @@ -0,0 +1,147 @@ +package org.springframework.cloud.gateway.handler; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +import org.springframework.beans.BeansException; +import org.springframework.cloud.gateway.config.GatewayProperties; +import org.springframework.cloud.gateway.config.GatewayProperties.Route; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.web.reactive.handler.AbstractHandlerMapping; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebHandler; + +import reactor.core.publisher.Mono; + +import static org.springframework.cloud.gateway.filter.GatewayFilter.GATEWAY_HANDLER_MAPPER_ATTR; +import static org.springframework.cloud.gateway.filter.GatewayFilter.GATEWAY_ROUTE_ATTR; + +/** + * @author Spencer Gibb + */ +public class ServerWebExchangePredicateHandlerMapping extends AbstractHandlerMapping { + + private Map predicateFactories = new LinkedHashMap<>(); + private GatewayProperties properties; + private WebHandler webHandler; + + private List routes; + + public ServerWebExchangePredicateHandlerMapping(WebHandler webHandler, List predicateFactories, GatewayProperties properties) { + this.webHandler = webHandler; + this.properties = properties; + + for (GatewayPredicateFactory factory : predicateFactories) { + if (this.predicateFactories.containsKey(factory.getName())) { + this.logger.warn("A GatewayPredicateFactory named "+ factory.getName() + + " already exists, class: " + this.predicateFactories.get(factory.getName()) + + ". It will be overwritten."); + } + this.predicateFactories.put(factory.getName(), factory); + if (logger.isInfoEnabled()) { + logger.info("Loaded GatewayPredicateFactory [" + factory.getName() + "]"); + } + } + + setOrder(-1); + } + + @Override + protected void initApplicationContext() throws BeansException { + super.initApplicationContext(); + //TODO: move properties.getRoutes() to interface/impl + registerHandlers(this.properties.getRoutes()); + } + + protected void registerHandlers(List routes) { + this.routes = routes; + } + + @Override + protected Mono getHandlerInternal(ServerWebExchange exchange) { + exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getClass().getSimpleName()); + + Route route; + try { + route = lookupRoute(this.routes, exchange); + } + catch (Exception ex) { + return Mono.error(ex); + } + + if (route != null) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + route); + } + + exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, route); + return Mono.just(this.webHandler); + } + else if (this.logger.isTraceEnabled()) { + this.logger.trace("No Route found for [" + getExchangeDesc(exchange) + "]"); + } + + return Mono.empty(); + } + + //TODO: get desc from factory? + private String getExchangeDesc(ServerWebExchange exchange) { + StringBuilder out = new StringBuilder(); + out.append("Exchange: "); + out.append(exchange.getRequest().getMethod()); + out.append(" "); + out.append(exchange.getRequest().getURI()); + return out.toString(); + } + + + protected Route lookupRoute(List routes, ServerWebExchange exchange) throws Exception { + for (Route route : routes) { + if (!route.getPredicates().isEmpty()) { + //TODO: cache predicate + Predicate predicate = combinePredicates(route); + if (predicate.test(exchange)) { + validateRoute(route, exchange); + return route; + } + } + } + return null; + } + + + private Predicate combinePredicates(Route route) { + List predicates = route.getPredicates(); + Predicate predicate = lookup(predicates.get(0)); + + for (GatewayProperties.Predicate andPredicate : predicates.subList(1, predicates.size())) { + Predicate found = lookup(andPredicate); + predicate = predicate.and(found); + } + + return predicate; + } + + private Predicate lookup(GatewayProperties.Predicate predicate) { + GatewayPredicateFactory found = this.predicateFactories.get(predicate.getName()); + if (found == null) { + throw new IllegalArgumentException("Unable to find GatewayPredicateFactory with name " + predicate.getName()); + } + return found.create(predicate.getValue()); + } + + /** + * Validate the given handler against the current request. + *

The default implementation is empty. Can be overridden in subclasses, + * for example to enforce specific preconditions expressed in URL mappings. + * @param route the Route object to validate + * @param exchange current exchange + * @throws Exception if validation failed + */ + @SuppressWarnings("UnusedParameters") + protected void validateRoute(Route route, ServerWebExchange exchange) throws Exception { + } + +} diff --git a/src/main/java/org/springframework/cloud/gateway/handler/UrlPredicateFactory.java b/src/main/java/org/springframework/cloud/gateway/handler/UrlPredicateFactory.java new file mode 100644 index 00000000..447e3c90 --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/handler/UrlPredicateFactory.java @@ -0,0 +1,47 @@ +package org.springframework.cloud.gateway.handler; + +import java.util.function.Predicate; + +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.support.HttpRequestPathHelper; + +/** + * @author Spencer Gibb + */ +public class UrlPredicateFactory implements GatewayPredicateFactory { + + private PathMatcher pathMatcher = new AntPathMatcher(); + private HttpRequestPathHelper pathHelper = new HttpRequestPathHelper(); + + public PathMatcher getPathMatcher() { + return pathMatcher; + } + + public void setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + } + + public HttpRequestPathHelper getPathHelper() { + return pathHelper; + } + + public void setPathHelper(HttpRequestPathHelper pathHelper) { + this.pathHelper = pathHelper; + } + + @Override + public String getName() { + return "Url"; + } + + @Override + public Predicate create(String pattern) { + return exchange -> { + String lookupPath = getPathHelper().getLookupPathForRequest(exchange); + return getPathMatcher().match(pattern, lookupPath); + //TODO: support trailingSlashMatch + }; + } +} diff --git a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java index b5ba69c3..ea928744 100644 --- a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java +++ b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java @@ -5,11 +5,15 @@ import org.apache.commons.logging.LogFactory; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.embedded.LocalServerPort; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.config.GatewayProperties; +import org.springframework.cloud.gateway.config.GatewayProperties.Route; import org.springframework.cloud.gateway.filter.GatewayFilter; -import org.springframework.cloud.gateway.handler.GatewayHostHandlerMapping; -import org.springframework.cloud.gateway.handler.GatewayUrlHandlerMapping; +import org.springframework.cloud.gateway.handler.HostPredicateFactory; +import org.springframework.cloud.gateway.handler.ServerWebExchangePredicateHandlerMapping; +import org.springframework.cloud.gateway.handler.UrlPredicateFactory; import org.springframework.context.annotation.Bean; import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; @@ -32,6 +36,7 @@ import reactor.test.StepVerifier; public class GatewayIntegrationTests { public static final String HANDLER_MAPPER_HEADER = "X-Gateway-Handler-Mapper-Class"; + public static final String ROUTE_ID_HEADER = "X-Gateway-Route-Id"; @LocalServerPort private int port; @@ -49,7 +54,9 @@ public class GatewayIntegrationTests { .consumeNextWith( httpHeaders -> { assertThat(httpHeaders.getFirst(HANDLER_MAPPER_HEADER)) - .isEqualTo(GatewayUrlHandlerMapping.class.getSimpleName()); + .isEqualTo(ServerWebExchangePredicateHandlerMapping.class.getSimpleName()); + assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER)) + .isEqualTo("default_path_to_httpbin"); }) .expectComplete() .verify(); @@ -70,13 +77,40 @@ public class GatewayIntegrationTests { HttpHeaders httpHeaders = response.headers().asHttpHeaders(); HttpStatus statusCode = response.statusCode(); assertThat(httpHeaders.getFirst(HANDLER_MAPPER_HEADER)) - .isEqualTo(GatewayHostHandlerMapping.class.getSimpleName()); + .isEqualTo(ServerWebExchangePredicateHandlerMapping.class.getSimpleName()); + assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER)) + .isEqualTo("host_example_to_httpbin"); assertThat(statusCode).isEqualTo(HttpStatus.OK); }) .expectComplete() .verify(); } + @Test + public void compositeRouteWorks() { + Mono result = webClient.exchange( + GET("http://localhost:" + port + "/headers") + .header("Host", "www.foo.org") + .build() + ); + + StepVerifier + .create(result) + .consumeNextWith( + response -> { + HttpHeaders httpHeaders = response.headers().asHttpHeaders(); + HttpStatus statusCode = response.statusCode(); + assertThat(httpHeaders.getFirst(HANDLER_MAPPER_HEADER)) + .isEqualTo(ServerWebExchangePredicateHandlerMapping.class.getSimpleName()); + assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER)) + .isEqualTo("host_foo_path_headers_to_httpbin"); + assertThat(statusCode).isEqualTo(HttpStatus.OK); + }) + .expectComplete() + .verify(); + } + + @EnableAutoConfiguration @SpringBootConfiguration public static class TestConfig { @@ -89,6 +123,10 @@ public class GatewayIntegrationTests { log.info("modifyResponseFilter start"); String value = (String) exchange.getAttribute(GatewayFilter.GATEWAY_HANDLER_MAPPER_ATTR).orElse("N/A"); exchange.getResponse().getHeaders().add(HANDLER_MAPPER_HEADER, value); + Route route = (Route) exchange.getAttribute(GatewayFilter.GATEWAY_ROUTE_ATTR).orElse(null); + if (route != null) { + exchange.getResponse().getHeaders().add(ROUTE_ID_HEADER, route.getId()); + } return chain.filter(exchange); }; } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 506de8d8..8c60ea9e 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -3,12 +3,28 @@ spring: cloud: gateway: routes: - test1: - requestPath: /** - downstreamUrl: http://httpbin.org:80 - test2: - requestHost: '**.example.org' - downstreamUrl: http://httpbin.org:80 + # ===================================== + - id: host_example_to_httpbin + downstreamUrl: http://httpbin.org:80 + predicates: + - name: Host + value: '**.example.org' + + # ===================================== + - id: host_foo_path_headers_to_httpbin + downstreamUrl: http://httpbin.org:80 + predicates: + - name: Host + value: '**.foo.org' + - name: Url + value: '/headers' + + # ===================================== + - id: default_path_to_httpbin + downstreamUrl: http://httpbin.org:80 + predicates: + - name: Url + value: /** logging: level: org.springframework.cloud.gateway: TRACE