Merge branch '2.2.x'

This commit is contained in:
Spencer Gibb
2020-02-27 17:01:17 -05:00
8 changed files with 201 additions and 19 deletions

View File

@@ -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`.

View File

@@ -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<HttpClientCustomizer> 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;
}

View File

@@ -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);
}

View File

@@ -128,7 +128,7 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered {
.getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false);
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
Flux<HttpClientResponse> responseFlux = httpClientWithTimeoutFrom(route)
Flux<HttpClientResponse> 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

View File

@@ -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<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
ReactorLoadBalancer<ServiceInstance> loadBalancer = this.clientFactory

View File

@@ -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<T> extends Function<T, Publisher<Boolean>> {
@Override
public Publisher<Boolean> 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<T> extends Function<T, Publisher<Boolean>> {
@Override
public Publisher<Boolean> 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

View File

@@ -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;
};
}
}
}

View File

@@ -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<Object> left = new TestAsyncPredicate<>(o -> false);
TestAsyncPredicate<Object> right = new TestAsyncPredicate<>(o -> true);
Publisher<Boolean> andTest = left.and(right).apply(new Object());
StepVerifier.create(andTest).expectNext(false).expectComplete().verify();
left.assertTested();
right.assertUntested();
}
@Test
public void andPredicateShouldTestRightOperatorIfLeftOperatorIsTrue() {
TestAsyncPredicate<Object> left = new TestAsyncPredicate<>(o -> true);
TestAsyncPredicate<Object> right = new TestAsyncPredicate<>(o -> false);
Publisher<Boolean> andTest = left.and(right).apply(new Object());
StepVerifier.create(andTest).expectNext(false).expectComplete().verify();
left.assertTested();
right.assertTested();
}
@Test
public void orPredicateShouldNotTestRightOperatorIfLeftOperatorIsTrue() {
TestAsyncPredicate<Object> left = new TestAsyncPredicate<>(o -> true);
TestAsyncPredicate<Object> right = new TestAsyncPredicate<>(o -> false);
Publisher<Boolean> orTest = left.or(right).apply(new Object());
StepVerifier.create(orTest).expectNext(true).expectComplete().verify();
left.assertTested();
right.assertUntested();
}
@Test
public void orPredicateShouldTestRightOperatorIfLeftOperatorIsFalse() {
TestAsyncPredicate<Object> left = new TestAsyncPredicate<>(o -> false);
TestAsyncPredicate<Object> right = new TestAsyncPredicate<>(o -> true);
Publisher<Boolean> 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<T> implements AsyncPredicate<T> {
private final Predicate<T> delegate;
private boolean tested = false;
private TestAsyncPredicate(Predicate<T> predicate) {
this.delegate = predicate;
}
@Override
public Publisher<Boolean> 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);
}
}
}