From 60653ee24068c0b688b6365269ffe8a94c58ee6e Mon Sep 17 00:00:00 2001 From: Guy Lewin Date: Mon, 24 Feb 2020 15:42:36 -0500 Subject: [PATCH 1/7] Allow HttpClient creation to be customized by subclasses. fixes gh-1577 --- .../cloud/gateway/filter/NettyRoutingFilter.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java index fc5af7b6..c8ffc72a 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java @@ -128,7 +128,7 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { .getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false); Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); - Flux responseFlux = httpClientWithTimeoutFrom(route) + Flux responseFlux = getHttpClient(route, exchange) .headers(headers -> { headers.add(httpHeaders); // Will either be set below, or later by Netty @@ -242,7 +242,16 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { } } - private HttpClient httpClientWithTimeoutFrom(Route route) { + /** + * Creates a new HttpClient with per route timeout configuration. Sub-classes that + * override, should call super.getHttpClient() if they want to honor the per route + * timeout configuration. + * @param route the current route. + * @param exchange the current ServerWebExchange. + * @param chain the current GatewayFilterChain. + * @return + */ + protected HttpClient getHttpClient(Route route, ServerWebExchange exchange) { Integer connectTimeout = (Integer) route.getMetadata().get(CONNECT_TIMEOUT_ATTR); if (connectTimeout != null) { return this.httpClient.tcpConfiguration((tcpClient) -> tcpClient From 799577edb7b6955374e780ed4943a072bf3bd1bb Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Thu, 27 Feb 2020 16:35:14 -0500 Subject: [PATCH 2/7] Creates HttpClientCustomizer. This allows users to customize the Reactor Netty HttpClient without loosing auto configuration. fixes gh-1546 --- .../config/GatewayAutoConfiguration.java | 15 +++++++-- .../gateway/config/HttpClientCustomizer.java | 31 +++++++++++++++++++ .../config/GatewayAutoConfigurationTests.java | 26 ++++++++++++++-- 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientCustomizer.java 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 dc27a9bb..741ae0d0 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2020 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. @@ -137,9 +137,11 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Primary; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.ConversionService; import org.springframework.core.env.Environment; import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.validation.Validator; import org.springframework.web.reactive.DispatcherHandler; @@ -579,7 +581,8 @@ public class GatewayAutoConfiguration { @Bean @ConditionalOnMissingBean - public HttpClient gatewayHttpClient(HttpClientProperties properties) { + public HttpClient gatewayHttpClient(HttpClientProperties properties, + List customizers) { // configure pool resources HttpClientProperties.Pool pool = properties.getPool(); @@ -599,6 +602,7 @@ public class GatewayAutoConfiguration { } HttpClient httpClient = HttpClient.create(connectionProvider) + // TODO: move customizations to HttpClientCustomizers .httpResponseDecoder(spec -> { if (properties.getMaxHeaderSize() != null) { // cast to int is ok, since @Max is Integer.MAX_VALUE @@ -677,6 +681,13 @@ public class GatewayAutoConfiguration { httpClient = httpClient.wiretap(true); } + if (!CollectionUtils.isEmpty(customizers)) { + customizers.sort(AnnotationAwareOrderComparator.INSTANCE); + for (HttpClientCustomizer customizer : customizers) { + httpClient = customizer.customize(httpClient); + } + } + return httpClient; } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientCustomizer.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientCustomizer.java new file mode 100644 index 00000000..e10ecb7e --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientCustomizer.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.config; + +import reactor.netty.http.client.HttpClient; + +@FunctionalInterface +public interface HttpClientCustomizer { + + /** + * Customize the specified {@link HttpClient}. + * @param httpClient the http client to customize. + * @return the customized HttpClient. + */ + HttpClient customize(HttpClient httpClient); + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java index a49eb870..7c574267 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2020 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. @@ -16,6 +16,8 @@ package org.springframework.cloud.gateway.config; +import java.util.concurrent.atomic.AtomicBoolean; + import org.junit.Test; import reactor.netty.http.client.HttpClient; @@ -30,6 +32,8 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContex import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint; import org.springframework.cloud.gateway.actuate.GatewayLegacyControllerEndpoint; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; @@ -81,7 +85,7 @@ public class GatewayAutoConfigurationTests { .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class, - GatewayAutoConfiguration.class)) + GatewayAutoConfiguration.class, HttpClientCustomizedConfig.class)) .withPropertyValues( "spring.cloud.gateway.httpclient.ssl.use-insecure-trust-manager=true", "spring.cloud.gateway.httpclient.connect-timeout=10", @@ -113,6 +117,9 @@ public class GatewayAutoConfigurationTests { .getBean(ReactorNettyWebSocketClient.class); assertThat(webSocketClient.getMaxFramePayloadLength()) .isEqualTo(1024); + HttpClientCustomizedConfig config = context + .getBean(HttpClientCustomizedConfig.class); + assertThat(config.called.get()).isTrue(); }); } @@ -143,4 +150,19 @@ public class GatewayAutoConfigurationTests { } + @Configuration + protected static class HttpClientCustomizedConfig { + + private final AtomicBoolean called = new AtomicBoolean(); + + @Bean + HttpClientCustomizer myCustomCustomizer() { + return httpClient -> { + called.compareAndSet(false, true); + return httpClient; + }; + } + + } + } From b017891fe7eb9675a7b4e4d0c31dd17104d666a5 Mon Sep 17 00:00:00 2001 From: Ingyu Hwang Date: Wed, 5 Feb 2020 16:56:15 +0900 Subject: [PATCH 3/7] Adjust position of AfterRoutePredicateFactory fixes gh-1557 --- .../cloud/gateway/config/GatewayAutoConfiguration.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 741ae0d0..ec2d2e5b 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 @@ -330,11 +330,6 @@ public class GatewayAutoConfiguration { return new WeightCalculatorWebFilter(routeLocator, configurationService); } - @Bean - public AfterRoutePredicateFactory afterRoutePredicateFactory() { - return new AfterRoutePredicateFactory(); - } - /* * @Bean //TODO: default over netty? configurable public WebClientHttpRoutingFilter * webClientHttpRoutingFilter() { //TODO: WebClient bean return new @@ -346,6 +341,11 @@ public class GatewayAutoConfiguration { // Predicate Factory beans + @Bean + public AfterRoutePredicateFactory afterRoutePredicateFactory() { + return new AfterRoutePredicateFactory(); + } + @Bean public BeforeRoutePredicateFactory beforeRoutePredicateFactory() { return new BeforeRoutePredicateFactory(); From 47f640c915265b0647caa9c14110d2754cef4bfe Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 8 Feb 2020 14:37:12 -0600 Subject: [PATCH 4/7] Adds protected reconstructURI method. Allows URI mapping behavior to be overridden for ReactiveLoadBalancerClientFilter. Fixes gh-1524 --- .../gateway/filter/ReactiveLoadBalancerClientFilter.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilter.java index 7b462653..86fe669a 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilter.java @@ -105,7 +105,7 @@ public class ReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered { DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance( response.getServer(), overrideScheme); - URI requestUrl = LoadBalancerUriTools.reconstructURI(serviceInstance, uri); + URI requestUrl = reconstructURI(serviceInstance, uri); if (log.isTraceEnabled()) { log.trace("LoadBalancerClientFilter url chosen: " + requestUrl); @@ -114,6 +114,10 @@ public class ReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered { }).then(chain.filter(exchange)); } + protected URI reconstructURI(ServiceInstance serviceInstance, URI original) { + return LoadBalancerUriTools.reconstructURI(serviceInstance, original); + } + private Mono> choose(ServerWebExchange exchange) { URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); ReactorLoadBalancer loadBalancer = this.clientFactory From 67241a3e6f9da9d6c45a481a14aa9837a828f0b8 Mon Sep 17 00:00:00 2001 From: Young Jun Seo Date: Mon, 10 Feb 2020 16:48:45 +0900 Subject: [PATCH 5/7] Fixed typo fateway -> gateway fixes gh-1565 --- docs/src/main/asciidoc/spring-cloud-gateway.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index ec6d6c87..4d83e3f0 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -2179,7 +2179,7 @@ To enable this, set `spring.cloud.gateway.discovery.locator.enabled=true` and ma ==== Configuring Predicates and Filters For `DiscoveryClient` Routes -By default, the fateway defines a single predicate and filter for routes created with a `DiscoveryClient`. +By default, the gateway defines a single predicate and filter for routes created with a `DiscoveryClient`. The default predicate is a path predicate defined with the pattern `/serviceId/**`, where `serviceId` is the ID of the service from the `DiscoveryClient`. From 7b673a8e87899203c3a48db63e32d1e84f131762 Mon Sep 17 00:00:00 2001 From: Adriano Scheffer Date: Wed, 12 Feb 2020 23:49:11 -0500 Subject: [PATCH 6/7] Lazily test AsyncPredicate for 'and' and 'or' operations. fixes gh-1571 --- .../cloud/gateway/handler/AsyncPredicate.java | 9 +- .../gateway/handler/AsyncPredicateTest.java | 106 ++++++++++++++++++ 2 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateTest.java diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java index 505e1e67..144a0890 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java @@ -20,7 +20,6 @@ import java.util.function.Function; import java.util.function.Predicate; import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate; @@ -106,8 +105,8 @@ public interface AsyncPredicate extends Function> { @Override public Publisher apply(T t) { - return Flux.zip(left.apply(t), right.apply(t)) - .map(tuple -> tuple.getT1() && tuple.getT2()); + return Mono.from(left.apply(t)).flatMap( + result -> !result ? Mono.just(false) : Mono.from(right.apply(t))); } @Override @@ -133,8 +132,8 @@ public interface AsyncPredicate extends Function> { @Override public Publisher apply(T t) { - return Flux.zip(left.apply(t), right.apply(t)) - .map(tuple -> tuple.getT1() || tuple.getT2()); + return Mono.from(left.apply(t)).flatMap( + result -> result ? Mono.just(true) : Mono.from(right.apply(t))); } @Override diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateTest.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateTest.java new file mode 100644 index 00000000..25e10486 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.handler; + +import java.util.function.Predicate; + +import org.junit.Assert; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +public class AsyncPredicateTest { + + @Test + public void andPredicateShouldNotTestRightOperatorIfLeftOperatorIsFalse() { + TestAsyncPredicate left = new TestAsyncPredicate<>(o -> false); + TestAsyncPredicate right = new TestAsyncPredicate<>(o -> true); + Publisher andTest = left.and(right).apply(new Object()); + + StepVerifier.create(andTest).expectNext(false).expectComplete().verify(); + + left.assertTested(); + right.assertUntested(); + } + + @Test + public void andPredicateShouldTestRightOperatorIfLeftOperatorIsTrue() { + TestAsyncPredicate left = new TestAsyncPredicate<>(o -> true); + TestAsyncPredicate right = new TestAsyncPredicate<>(o -> false); + Publisher andTest = left.and(right).apply(new Object()); + + StepVerifier.create(andTest).expectNext(false).expectComplete().verify(); + + left.assertTested(); + right.assertTested(); + } + + @Test + public void orPredicateShouldNotTestRightOperatorIfLeftOperatorIsTrue() { + TestAsyncPredicate left = new TestAsyncPredicate<>(o -> true); + TestAsyncPredicate right = new TestAsyncPredicate<>(o -> false); + Publisher orTest = left.or(right).apply(new Object()); + + StepVerifier.create(orTest).expectNext(true).expectComplete().verify(); + + left.assertTested(); + right.assertUntested(); + } + + @Test + public void orPredicateShouldTestRightOperatorIfLeftOperatorIsFalse() { + TestAsyncPredicate left = new TestAsyncPredicate<>(o -> false); + TestAsyncPredicate right = new TestAsyncPredicate<>(o -> true); + Publisher orTest = left.or(right).apply(new Object()); + + StepVerifier.create(orTest).expectNext(true).expectComplete().verify(); + + left.assertTested(); + right.assertTested(); + } + + /** + * An AsyncPredicate decorator that records if the apply method was called. + */ + private final static class TestAsyncPredicate implements AsyncPredicate { + + private final Predicate delegate; + + private boolean tested = false; + + private TestAsyncPredicate(Predicate predicate) { + this.delegate = predicate; + } + + @Override + public Publisher apply(T t) { + tested = true; + return Mono.just(delegate.test(t)); + } + + public void assertTested() { + Assert.assertTrue("predicate must have been tested", tested); + } + + public void assertUntested() { + Assert.assertFalse("predicate must not have been tested", tested); + } + + } + +} From 356c0032601d101cefc988b7db00ddc4c841acf1 Mon Sep 17 00:00:00 2001 From: Alexander Holbreich Date: Mon, 24 Feb 2020 19:18:37 +0100 Subject: [PATCH 7/7] fixing copy paste error on path_route fixes gh-1581 --- docs/src/main/asciidoc/spring-cloud-gateway.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 4d83e3f0..3c5d20da 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -281,7 +281,7 @@ spring: cloud: gateway: routes: - - id: host_route + - id: path_route uri: https://example.org predicates: - Path=/red/{segment},/blue/{segment}