diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index b7a3d1e8..802b3732 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} @@ -2073,7 +2073,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`. 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 5fa124a0..66013a59 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. @@ -133,9 +133,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; @@ -324,11 +326,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 @@ -340,6 +337,11 @@ public class GatewayAutoConfiguration { // Predicate Factory beans + @Bean + public AfterRoutePredicateFactory afterRoutePredicateFactory() { + return new AfterRoutePredicateFactory(); + } + @Bean public BeforeRoutePredicateFactory beforeRoutePredicateFactory() { return new BeforeRoutePredicateFactory(); @@ -575,7 +577,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(); @@ -595,6 +598,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 @@ -673,6 +677,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/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 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 850969f1..c4d556c0 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 @@ -108,7 +108,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); @@ -117,6 +117,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 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/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; + }; + } + + } + } 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); + } + + } + +}