diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 284fbe01..45811fe2 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -80,6 +80,8 @@ import org.springframework.cloud.gateway.route.RouteDefinitionRepository; import org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator; import org.springframework.cloud.gateway.route.RouteDefinitionWriter; import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -141,6 +143,11 @@ public class GatewayAutoConfiguration { } } + @Bean + public RouteLocatorBuilder routeLocatorBuilder(ConfigurableApplicationContext context) { + return new RouteLocatorBuilder(context); + } + @Bean @ConditionalOnMissingBean public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(GatewayProperties properties) { @@ -223,7 +230,7 @@ public class GatewayAutoConfiguration { //TODO: default over netty? configurable public WebClientHttpRoutingFilter webClientHttpRoutingFilter() { //TODO: WebClient bean - return new WebClientHttpRoutingFilter(WebClient.builder().build()); + return new WebClientHttpRoutingFilter(WebClient.routes().build()); } @Bean diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/GatewayFilters.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/GatewayFilters.java index b3096397..66346688 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/GatewayFilters.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/GatewayFilters.java @@ -30,8 +30,10 @@ import com.netflix.hystrix.HystrixObservableCommand; import static org.springframework.tuple.TupleBuilder.tuple; /** + * @deprecated inject {@link org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder} bean instead * @author Spencer Gibb */ +@Deprecated public class GatewayFilters { public static final Tuple EMPTY_TUPLE = tuple().build(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RemoveNonProxyHeadersGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RemoveNonProxyHeadersGatewayFilterFactory.java index 94ad43e2..f531def4 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RemoveNonProxyHeadersGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RemoveNonProxyHeadersGatewayFilterFactory.java @@ -63,11 +63,14 @@ public class RemoveNonProxyHeadersGatewayFilterFactory implements GatewayFilterF @Override public GatewayFilter apply(Tuple args) { //TODO: support filter args + return apply(this.headers); + } + public GatewayFilter apply(List headersToRemove) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest().mutate() .headers(httpHeaders -> { - for (String header : this.headers) { + for (String header : headersToRemove) { httpHeaders.remove(header); } }) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestRateLimiterGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestRateLimiterGatewayFilterFactory.java index 14378a1a..c41893bb 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestRateLimiterGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestRateLimiterGatewayFilterFactory.java @@ -39,6 +39,10 @@ public class RequestRateLimiterGatewayFilterFactory implements GatewayFilterFact this.defaultKeyResolver = defaultKeyResolver; } + public KeyResolver getDefaultKeyResolver() { + return defaultKeyResolver; + } + @SuppressWarnings("unchecked") @Override public GatewayFilter apply(Tuple args) { diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/RoutePredicates.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/RoutePredicates.java index 3450b675..8537804d 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/RoutePredicates.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/RoutePredicates.java @@ -18,25 +18,17 @@ package org.springframework.cloud.gateway.handler.predicate; import java.time.ZonedDateTime; -import java.util.Arrays; -import java.util.List; import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.IntStream; import org.springframework.http.HttpMethod; import org.springframework.util.PathMatcher; import org.springframework.web.server.ServerWebExchange; -import static org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory.DATETIME1_KEY; -import static org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory.DATETIME2_KEY; -import static org.springframework.cloud.gateway.handler.predicate.MethodRoutePredicateFactory.METHOD_KEY; -import static org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory.PATTERN_KEY; -import static org.springframework.tuple.TupleBuilder.tuple; - /** + * @deprecated inject {@link org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder} bean instead * @author Spencer Gibb */ +@Deprecated public class RoutePredicates { public static Predicate after(ZonedDateTime datetime) { 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 fa803309..3ee3eab9 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 @@ -102,6 +102,23 @@ public class Route implements Ordered { return this; } + public Builder and(Predicate predicate) { + Assert.notNull(this.predicate, "can not call and() on null predicate"); + this.predicate = this.predicate.and(predicate); + return this; + } + + public Builder or(Predicate predicate) { + Assert.notNull(this.predicate, "can not call or() on null predicate"); + this.predicate = this.predicate.or(predicate); + return this; + } + + public Builder negate() { + Assert.notNull(this.predicate, "can not call negate() on null predicate"); + this.predicate = this.predicate.negate(); + return this; + } public Builder gatewayFilters(List gatewayFilters) { this.gatewayFilters = gatewayFilters; return this; @@ -120,7 +137,7 @@ public class Route implements Ordered { public Route build() { Assert.notNull(this.id, "id can not be null"); Assert.notNull(this.uri, "uri can not be null"); - //TODO: Assert.notNull(this.predicate, "predicate can not be null"); + Assert.notNull(this.predicate, "predicate can not be null"); return new Route(this.id, this.uri, this.order, this.predicate, this.gatewayFilters); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Routes.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Routes.java index e89ad62e..d6d861f7 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Routes.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Routes.java @@ -31,8 +31,10 @@ import org.springframework.cloud.gateway.filter.GatewayFilter; import reactor.core.publisher.Flux; /** + * @deprecated inject {@link org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder} bean instead * @author Spencer Gibb */ +@Deprecated public class Routes { public static LocatorBuilder locator() { diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java new file mode 100644 index 00000000..52e790fa --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2017 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.route.builder; + +import org.springframework.cloud.gateway.route.Route; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +import java.util.function.Predicate; + +import static org.springframework.cloud.gateway.route.builder.BooleanSpec.Operator.*; + +public class BooleanSpec extends GatewayFilterSpec { + + enum Operator { AND, OR, NEGATE } + + public BooleanSpec(Route.Builder routeBuilder, RouteLocatorBuilder.Builder builder) { + super(routeBuilder, builder); + } + + public BooleanOpSpec and() { + return new BooleanOpSpec(routeBuilder, builder, AND); + } + + public BooleanOpSpec or() { + return new BooleanOpSpec(routeBuilder, builder, OR); + } + + public BooleanOpSpec negate() { + return new BooleanOpSpec(routeBuilder, builder, NEGATE); + } + + public static class BooleanOpSpec extends PredicateSpec { + + private Operator operator; + + BooleanOpSpec(Route.Builder routeBuilder, RouteLocatorBuilder.Builder builder, Operator operator) { + super(routeBuilder, builder); + Assert.notNull(operator, "operator may not be null"); + this.operator = operator; + } + + @Override + public BooleanSpec predicate(Predicate predicate) { + switch (this.operator) { + case AND: + this.routeBuilder.and(predicate); + break; + case OR: + this.routeBuilder.or(predicate); + break; + case NEGATE: + this.routeBuilder.negate(); + } + return gatewayFilterBuilder(); + } + } +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java new file mode 100644 index 00000000..6a689ae8 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -0,0 +1,182 @@ +/* + * Copyright 2013-2017 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.route.builder; + +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.RemoveNonProxyHeadersGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.RemoveRequestHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; +import org.springframework.cloud.gateway.route.Route; +import org.springframework.http.HttpStatus; +import org.springframework.tuple.Tuple; + +import com.netflix.hystrix.HystrixObservableCommand; + +import static org.springframework.tuple.TupleBuilder.tuple; + +public class GatewayFilterSpec extends UriSpec { + + static final Tuple EMPTY_TUPLE = tuple().build(); + + public GatewayFilterSpec(Route.Builder routeBuilder, RouteLocatorBuilder.Builder builder) { + super(routeBuilder, builder); + } + + public GatewayFilterSpec gatewayFilters(List gatewayFilters) { + this.addAll(gatewayFilters); + return this; + } + + public GatewayFilterSpec add(GatewayFilter gatewayFilter) { + return this.filter(gatewayFilter); + } + + public GatewayFilterSpec filter(GatewayFilter gatewayFilter) { + return this.filter(gatewayFilter, 0); + } + + public GatewayFilterSpec filter(GatewayFilter gatewayFilter, int order) { + this.routeBuilder.add(new OrderedGatewayFilter(gatewayFilter, order)); + return this; + } + + public GatewayFilterSpec addAll(Collection gatewayFilters) { + this.routeBuilder.addAll(gatewayFilters); + return this; + } + + + public GatewayFilterSpec addRequestHeader(String headerName, String headerValue) { + return filter(getBean(AddRequestHeaderGatewayFilterFactory.class).apply(headerName, headerValue)); + } + + public GatewayFilterSpec addRequestParameter(String param, String value) { + return filter(getBean(AddRequestParameterGatewayFilterFactory.class).apply(param, value)); + } + + public GatewayFilterSpec addResponseHeader(String headerName, String headerValue) { + return filter(getBean(AddResponseHeaderGatewayFilterFactory.class).apply(headerName, headerValue)); + } + + public GatewayFilterSpec hystrix(String commandName) { + return filter(getBean(HystrixGatewayFilterFactory.class).apply(commandName)); + } + + public GatewayFilterSpec hystrix(HystrixObservableCommand.Setter setter) { + return filter(getBean(HystrixGatewayFilterFactory.class).apply(setter)); + } + + public GatewayFilterSpec prefixPath(String prefix) { + return filter(getBean(PrefixPathGatewayFilterFactory.class).apply(prefix)); + } + + public GatewayFilterSpec redirect(int status, URI url) { + return redirect(String.valueOf(status), url.toString()); + } + + public GatewayFilterSpec redirect(int status, String url) { + return redirect(String.valueOf(status), url); + } + + public GatewayFilterSpec redirect(String status, URI url) { + return redirect(status, url.toString()); + } + + public GatewayFilterSpec redirect(String status, String url) { + return filter(getBean(RedirectToGatewayFilterFactory.class).apply(status, url)); + } + + public GatewayFilterSpec redirect(HttpStatus status, URL url) { + return filter(getBean(RedirectToGatewayFilterFactory.class).apply(status, url)); + } + + public GatewayFilterSpec removeNonProxyHeaders() { + return filter(getBean(RemoveNonProxyHeadersGatewayFilterFactory.class).apply(EMPTY_TUPLE)); + } + + public GatewayFilterSpec removeNonProxyHeaders(String... headersToRemove) { + return filter(getBean(RemoveNonProxyHeadersGatewayFilterFactory.class).apply(Arrays.asList(headersToRemove))); + } + + public GatewayFilterSpec removeRequestHeader(String headerName) { + return filter(getBean(RemoveRequestHeaderGatewayFilterFactory.class).apply(headerName)); + } + + public GatewayFilterSpec removeResponseHeader(String headerName) { + return filter(getBean(RemoveResponseHeaderGatewayFilterFactory.class).apply(headerName)); + } + + public GatewayFilterSpec requestRateLimiter(Tuple args) { + RequestRateLimiterGatewayFilterFactory factory = getBean(RequestRateLimiterGatewayFilterFactory.class); + KeyResolver keyResolver; + try { + keyResolver = getBean(KeyResolver.class); + } catch (NoSuchBeanDefinitionException e) { + keyResolver = factory.getDefaultKeyResolver(); + } + return filter(factory.apply(keyResolver, args)); + } + + public GatewayFilterSpec rewritePath(String regex, String replacement) { + return filter(getBean(RewritePathGatewayFilterFactory.class).apply(regex, replacement)); + } + + public GatewayFilterSpec secureHeaders() { + return filter(getBean(SecureHeadersGatewayFilterFactory.class).apply(EMPTY_TUPLE)); + } + + public GatewayFilterSpec setPath(String template) { + return filter(getBean(SetPathGatewayFilterFactory.class).apply(template)); + } + + public GatewayFilterSpec setResponseHeader(String headerName, String headerValue) { + return filter(getBean(SetResponseHeaderGatewayFilterFactory.class).apply(headerName, headerValue)); + } + + public GatewayFilterSpec setStatus(int status) { + return setStatus(String.valueOf(status)); + } + + public GatewayFilterSpec setStatus(String status) { + return filter(getBean(SetStatusGatewayFilterFactory.class).apply(status)); + } + + public GatewayFilterSpec setStatus(HttpStatus status) { + return filter(getBean(SetStatusGatewayFilterFactory.class).apply(status)); + } +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java new file mode 100644 index 00000000..b0bf442a --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java @@ -0,0 +1,107 @@ +/* + * Copyright 2013-2017 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.route.builder; + +import org.springframework.cloud.gateway.handler.predicate.AfterRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.BeforeRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.CookieRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.HeaderRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.HostRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.MethodRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory; +import org.springframework.cloud.gateway.route.Route; +import org.springframework.http.HttpMethod; +import org.springframework.web.server.ServerWebExchange; + +import java.time.ZonedDateTime; +import java.util.function.Predicate; + +public class PredicateSpec extends UriSpec { + + PredicateSpec(Route.Builder routeBuilder, RouteLocatorBuilder.Builder builder) { + super(routeBuilder, builder); + } + + public PredicateSpec order(int order) { + this.routeBuilder.order(order); + return this; + } + + public BooleanSpec predicate(Predicate predicate) { + this.routeBuilder.predicate(predicate); + return gatewayFilterBuilder(); + } + + protected BooleanSpec gatewayFilterBuilder() { + return new BooleanSpec(this.routeBuilder, this.builder); + } + + public BooleanSpec after(ZonedDateTime datetime) { + return predicate(getBean(AfterRoutePredicateFactory.class).apply(datetime)); + } + + public BooleanSpec before(ZonedDateTime datetime) { + return predicate(getBean(BeforeRoutePredicateFactory.class).apply(datetime)); + } + + public BooleanSpec between(ZonedDateTime datetime1, ZonedDateTime datetime2) { + return predicate(getBean(BetweenRoutePredicateFactory.class).apply(datetime1, datetime2)); + } + + public BooleanSpec cookie(String name, String regex) { + return predicate(getBean(CookieRoutePredicateFactory.class).apply(name, regex)); + } + + public BooleanSpec header(String header, String regex) { + return predicate(getBean(HeaderRoutePredicateFactory.class).apply(header, regex)); + } + + public BooleanSpec host(String pattern) { + return predicate(getBean(HostRoutePredicateFactory.class).apply(pattern)); + } + + public BooleanSpec method(String method) { + return predicate(getBean(MethodRoutePredicateFactory.class).apply(method)); + } + + public BooleanSpec method(HttpMethod method) { + return predicate(getBean(MethodRoutePredicateFactory.class).apply(method)); + } + + public BooleanSpec path(String pattern) { + return predicate(getBean(PathRoutePredicateFactory.class).apply(pattern)); + } + + public BooleanSpec query(String param, String regex) { + return predicate(getBean(QueryRoutePredicateFactory.class).apply(param, regex)); + } + + public BooleanSpec query(String param) { + return predicate(getBean(QueryRoutePredicateFactory.class).apply(param, null)); + } + + public BooleanSpec remoteAddr(String... addrs) { + return predicate(getBean(RemoteAddrRoutePredicateFactory.class).apply(addrs)); + } + + public BooleanSpec alwaysTrue() { + return predicate(exchange -> true); + } +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/RouteLocatorBuilder.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/RouteLocatorBuilder.java new file mode 100644 index 00000000..028f6c57 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/RouteLocatorBuilder.java @@ -0,0 +1,100 @@ +/* + * Copyright 2013-2017 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.route.builder; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.cloud.gateway.route.Route; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.context.ConfigurableApplicationContext; + +import reactor.core.publisher.Flux; + +public class RouteLocatorBuilder { + + private ConfigurableApplicationContext context; + + public RouteLocatorBuilder(ConfigurableApplicationContext context) { + this.context = context; + } + + public Builder routes() { + return new Builder(context); + } + + public static class Builder { + + private List routes = new ArrayList<>(); + private ConfigurableApplicationContext context; + + public Builder(ConfigurableApplicationContext context) { + this.context = context; + } + + public PredicateSpec route(String id) { + return new RouteSpec(this).id(id); + } + + private void add(Route route) { + this.routes.add(route); + } + + Builder uri(Route.Builder builder, String uri) { + Route route = builder.uri(uri).build(); + routes.add(route); + return this; + } + + Builder uri(Route.Builder builder, URI uri) { + Route route = builder.uri(uri).build(); + routes.add(route); + return this; + } + + public RouteLocator build() { + return () -> Flux.fromIterable(this.routes); + } + + ConfigurableApplicationContext getContext() { + return context; + } + } + + + public static class RouteSpec { + private final Route.Builder routeBuilder = Route.builder(); + private final Builder builder; + + private RouteSpec(Builder builder) { + this.builder = builder; + } + + public PredicateSpec id(String id) { + this.routeBuilder.id(id); + return predicateBuilder(); + } + + private PredicateSpec predicateBuilder() { + return new PredicateSpec(this.routeBuilder, this.builder); + } + + } + + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/UriSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/UriSpec.java new file mode 100644 index 00000000..cc3f82ee --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/UriSpec.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2017 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.route.builder; + +import org.springframework.cloud.gateway.route.Route; + +import java.net.URI; + +public class UriSpec { + final Route.Builder routeBuilder; + final RouteLocatorBuilder.Builder builder; + + UriSpec(Route.Builder routeBuilder, RouteLocatorBuilder.Builder builder) { + this.routeBuilder = routeBuilder; + this.builder = builder; + } + + public RouteLocatorBuilder.Builder uri(String uri) { + return this.builder.uri(this.routeBuilder, uri); + } + + public RouteLocatorBuilder.Builder uri(URI uri) { + return this.builder.uri(this.routeBuilder, uri); + } + + + T getBean(Class type) { + return this.builder.getContext().getBean(type); + } +} diff --git a/spring-cloud-gateway-core/src/main/kotlin/org/springframework/cloud/gateway/route/GatewayDsl.kt b/spring-cloud-gateway-core/src/main/kotlin/org/springframework/cloud/gateway/route/GatewayDsl.kt index f8e9e0e3..6cd30ddb 100644 --- a/spring-cloud-gateway-core/src/main/kotlin/org/springframework/cloud/gateway/route/GatewayDsl.kt +++ b/spring-cloud-gateway-core/src/main/kotlin/org/springframework/cloud/gateway/route/GatewayDsl.kt @@ -71,6 +71,11 @@ class RouteLocatorDsl { * A helper to return a composed [Predicate] that tests against this [Predicate] OR the [other] predicate */ infix fun Predicate.or(other: Predicate) = this.or(other) + + /** + * A helper to return a composed [Predicate] that negates this [Predicate] + */ + fun Predicate.negate() = this.negate() } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/PrincipalNameKeyResolverIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/PrincipalNameKeyResolverIntegrationTests.java index 305ee82c..d44345ab 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/PrincipalNameKeyResolverIntegrationTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ratelimit/PrincipalNameKeyResolverIntegrationTests.java @@ -14,9 +14,8 @@ import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory; import org.springframework.cloud.gateway.route.RouteLocator; -import org.springframework.cloud.gateway.route.Routes; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; @@ -32,10 +31,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT; -import static org.springframework.cloud.gateway.filter.factory.GatewayFilters.prefixPath; -import static org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter.BURST_CAPACITY_KEY; -import static org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter.REPLENISH_RATE_KEY; -import static org.springframework.cloud.gateway.handler.predicate.RoutePredicates.path; import static org.springframework.tuple.TupleBuilder.tuple; import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; @@ -96,12 +91,12 @@ public class PrincipalNameKeyResolverIntegrationTests { } @Bean - public RouteLocator customRouteLocator(RequestRateLimiterGatewayFilterFactory rateLimiterFactory) { - return Routes.locator() + public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() .route("protected-throttled") - .predicate(path("/myapi/**")) - .filter(rateLimiterFactory.apply(tuple().of(REPLENISH_RATE_KEY, 1, BURST_CAPACITY_KEY, 1))) - .filter(prefixPath("/downstream")) + .path("/myapi/**") + .requestRateLimiter(tuple().build()) + .prefixPath("/downstream") .uri("http://localhost:"+port) .build(); } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/websocket/WebSocketIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/websocket/WebSocketIntegrationTests.java index f3822887..dbf6664c 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/websocket/WebSocketIntegrationTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/websocket/WebSocketIntegrationTests.java @@ -36,7 +36,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.gateway.route.RouteLocator; -import org.springframework.cloud.gateway.route.Routes; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.cloud.gateway.test.PermitAllSecurityConfiguration; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.Lifecycle; @@ -285,11 +285,11 @@ public class WebSocketIntegrationTests { } } - // TODO: workaround for suspected RxNetty WebSocket client issue - // https://github.com/ReactiveX/RxNetty/issues/560 - private static Mono doSend(WebSocketSession session, Publisher output) { - return session.send(Mono.delay(Duration.ofMillis(100)).thenMany(output)); + return session.send(output); + // workaround for suspected RxNetty WebSocket client issue + // https://github.com/ReactiveX/RxNetty/issues/560 + // return session.send(Mono.delay(Duration.ofMillis(100)).thenMany(output)); } @Configuration @@ -301,10 +301,10 @@ public class WebSocketIntegrationTests { private int wsPort; @Bean - public RouteLocator wsRouteLocator() { - return Routes.locator() + public RouteLocator wsRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() .route("testws") - .predicate(alwaysTrue()) + .alwaysTrue() .uri("ws://localhost:"+this.wsPort) .build(); } diff --git a/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/GatewaySampleApplication.java b/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/GatewaySampleApplication.java index 15acdb59..3e09622c 100644 --- a/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/GatewaySampleApplication.java +++ b/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/GatewaySampleApplication.java @@ -17,11 +17,13 @@ package org.springframework.cloud.gateway.sample; +import java.util.concurrent.TimeUnit; + import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.cloud.gateway.route.RouteLocator; -import org.springframework.cloud.gateway.route.Routes; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.web.reactive.function.BodyInserters; @@ -30,10 +32,8 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; -import static org.springframework.cloud.gateway.filter.factory.GatewayFilters.addResponseHeader; import static org.springframework.cloud.gateway.handler.predicate.RoutePredicates.host; import static org.springframework.cloud.gateway.handler.predicate.RoutePredicates.path; -import static org.springframework.tuple.TupleBuilder.tuple; /** * @author Spencer Gibb @@ -44,24 +44,24 @@ import static org.springframework.tuple.TupleBuilder.tuple; public class GatewaySampleApplication { @Bean - public RouteLocator customRouteLocator(ThrottleGatewayFilterFactory throttle) { + public RouteLocator customRouteLocator(RouteLocatorBuilder builder, ThrottleGatewayFilterFactory throttle) { //@formatter:off - return Routes.locator() + return builder.routes() .route("test") - .predicate(host("**.abc.org").and(path("/image/png"))) + .host("**.abc.org").and().path("/image/png") .addResponseHeader("X-TestHeader", "foobar") .uri("http://httpbin.org:80") .route("test2") - .predicate(path("/image/webp")) - .add(addResponseHeader("X-AnotherHeader", "baz")) + .path("/image/webp") + .addResponseHeader("X-AnotherHeader", "baz") .uri("http://httpbin.org:80") .route("test3") .order(-1) .predicate(host("**.throttle.org").and(path("/get"))) - .add(throttle.apply(tuple().of("capacity", 1, - "refillTokens", 1, - "refillPeriod", 10, - "refillUnit", "SECONDS"))) + .add(throttle.apply(1, + 1, + 10, + TimeUnit.SECONDS)) .uri("http://httpbin.org:80") .build(); ////@formatter:on diff --git a/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/ThrottleGatewayFilterFactory.java b/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/ThrottleGatewayFilterFactory.java index ee9f3c01..382dad1f 100644 --- a/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/ThrottleGatewayFilterFactory.java +++ b/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/ThrottleGatewayFilterFactory.java @@ -41,6 +41,10 @@ public class ThrottleGatewayFilterFactory implements GatewayFilterFactory { int refillTokens = args.getInt("refillTokens"); int refillPeriod = args.getInt("refillPeriod"); TimeUnit refillUnit = TimeUnit.valueOf(args.getString("refillUnit")); + return apply(capacity, refillTokens, refillPeriod, refillUnit); + } + + public GatewayFilter apply(int capacity, int refillTokens, int refillPeriod, TimeUnit refillUnit) { final TokenBucket tokenBucket = TokenBuckets.builder() .withCapacity(capacity) diff --git a/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java b/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java index c077fd0b..d4e6ee1a 100644 --- a/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java +++ b/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java @@ -22,17 +22,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.web.reactive.function.client.ClientResponse; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; +import org.springframework.test.web.reactive.server.WebTestClient; -import java.time.Duration; - -import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; /** @@ -45,28 +37,31 @@ public class GatewaySampleApplicationTests { @LocalServerPort protected int port = 0; - protected WebClient webClient; + protected WebTestClient webClient; protected String baseUri; @Before public void setup() { baseUri = "http://localhost:" + port; - this.webClient = WebClient.create(baseUri); + this.webClient = WebTestClient.bindToServer().baseUrl(baseUri).build(); } @Test public void contextLoads() { - Mono result = webClient.get() + webClient.get() .uri("/get") - .exchange(); + .exchange() + .expectStatus().isOk(); + } + + @Test + public void complexPredicate() { + webClient.get() + .uri("/image/png") + .header("Host", "www.abc.org") + .exchange() + .expectHeader().valueEquals("X-TestHeader", "foobar") + .expectStatus().isOk(); - StepVerifier.create(result) - .consumeNextWith( - response -> { - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK); - HttpHeaders httpHeaders = response.headers().asHttpHeaders(); - }) - .expectComplete() - .verify(Duration.ofSeconds(5)); } }