Gh 1300 backport sc loadbalancer support (#1414)
* Gh 1294 add reactive sc loadbalancer support (#1295) * Initial ReactiveLoadBalancer support # Conflicts: # spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/LoadBalancerClientFilter.java * Optimise imports. Minor refactoring. * Use `LoadBalancerUriTools` for reconstructing uris. Handle 404 exceptions. Add more tests and javadocs. * Finish autoConfiguration and dependencies setup. * Rename to `ReactorLoadBalancerClientFilter`. Fix test. Add docs. * Fix after review. * Fix dependencies, docs and formatting after backporting changes.
This commit is contained in:
committed by
GitHub
parent
25f28f1f55
commit
64879c32f7
@@ -1263,6 +1263,45 @@ but the `ServiceInstance` indicates it is not secure, then the downstream reques
|
||||
route in the Gateway configuration, the prefix will be stripped and the resulting scheme from the
|
||||
route URL will override the `ServiceInstance` configuration.
|
||||
|
||||
WARNING: `LoadBalancerClientFilter` uses a blocking Ribbon `LoadBalancerClient` under the hood.
|
||||
We suggest you use <<reactive-loadbalancer-client-filter,`ReactiveLoadBalancerClientFilter` instead>>.
|
||||
You can switch to using it by adding `org.springframework.cloud:spring-cloud-loadbalancer` dependency to your project
|
||||
and setting the value of the `spring.cloud.loadbalancer.ribbon.enabled` to `false`.
|
||||
|
||||
[[reactive-loadbalancer-client-filter]]
|
||||
=== ReactiveLoadBalancerClientFilter
|
||||
|
||||
The `ReactiveLoadBalancerClientFilter` looks for a URI in the exchange attribute
|
||||
`ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`. If the url has a `lb` scheme (ie `lb://myservice`),
|
||||
it will use the Spring Cloud `ReactorLoadBalancer` to resolve the name (`myservice` in the previous example)
|
||||
to an actual host and port and replace the URI in the same attribute. The unmodified
|
||||
original url is appended to the list in the `ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR` attribute.
|
||||
The filter will also look in the `ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR` attribute to see if it equals
|
||||
`lb` and then the same rules apply.
|
||||
|
||||
.application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: myRoute
|
||||
uri: lb://service
|
||||
predicates:
|
||||
- Path=/service/**
|
||||
----
|
||||
|
||||
NOTE: By default when a service instance cannot be found by the `ReactorLoadBalancer`, a `503` will be returned.
|
||||
You can configure the Gateway to return a `404` by setting `spring.cloud.gateway.loadbalancer.use404=true`.
|
||||
|
||||
NOTE: The `isSecure` value of the `ServiceInstance` returned from the `ReactiveLoadBalancerClientFilter` will override
|
||||
the scheme specified in the request made to the Gateway. For example, if the request comes into the Gateway over `HTTPS`
|
||||
but the `ServiceInstance` indicates it is not secure, then the downstream request will be made over
|
||||
`HTTP`. The opposite situation can also apply. However if `GATEWAY_SCHEME_PREFIX_ATTR` is specified for the
|
||||
route in the Gateway configuration, the prefix will be stripped and the resulting scheme from the
|
||||
route URL will override the `ServiceInstance` configuration.
|
||||
|
||||
=== Netty Routing Filter
|
||||
|
||||
The Netty Routing Filter runs if the url located in the `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR` exchange attribute has a `http` or `https` scheme. It uses the Netty `HttpClient` to make the downstream proxy request. The response is put in the `ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR` exchange attribute for use in a later filter. (There is an experimental `WebClientHttpRoutingFilter` that performs the same function, but does not require netty)
|
||||
|
||||
@@ -48,6 +48,11 @@
|
||||
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-loadbalancer</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
|
||||
@@ -23,13 +23,17 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter;
|
||||
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
|
||||
import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.DispatcherHandler;
|
||||
|
||||
/**
|
||||
* AutoConfiguration for {@link LoadBalancerClientFilter}.
|
||||
*
|
||||
* @author Spencer Gibb
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass({ LoadBalancerClient.class, RibbonAutoConfiguration.class,
|
||||
@@ -38,11 +42,10 @@ import org.springframework.web.reactive.DispatcherHandler;
|
||||
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
||||
public class GatewayLoadBalancerClientAutoConfiguration {
|
||||
|
||||
// GlobalFilter beans
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(LoadBalancerClient.class)
|
||||
@ConditionalOnMissingBean(LoadBalancerClientFilter.class)
|
||||
@ConditionalOnMissingBean({ LoadBalancerClientFilter.class,
|
||||
ReactiveLoadBalancerClientFilter.class })
|
||||
public LoadBalancerClientFilter loadBalancerClientFilter(LoadBalancerClient client,
|
||||
LoadBalancerProperties properties) {
|
||||
return new LoadBalancerClientFilter(client, properties);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
|
||||
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
|
||||
import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.DispatcherHandler;
|
||||
|
||||
/**
|
||||
* AutoConfiguration for {@link ReactiveLoadBalancerClientFilter}.
|
||||
*
|
||||
* @author Spencer Gibb
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass({ ReactiveLoadBalancer.class, LoadBalancerAutoConfiguration.class,
|
||||
DispatcherHandler.class })
|
||||
@AutoConfigureBefore(GatewayLoadBalancerClientAutoConfiguration.class)
|
||||
@AutoConfigureAfter(LoadBalancerAutoConfiguration.class)
|
||||
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
||||
public class GatewayReactiveLoadBalancerClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(LoadBalancerClientFactory.class)
|
||||
@ConditionalOnMissingBean(ReactiveLoadBalancerClientFilter.class)
|
||||
@Conditional(OnNoRibbonDefaultCondition.class)
|
||||
public ReactiveLoadBalancerClientFilter gatewayLoadBalancerClientFilter(
|
||||
LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
|
||||
return new ReactiveLoadBalancerClientFilter(clientFactory, properties);
|
||||
}
|
||||
|
||||
private static final class OnNoRibbonDefaultCondition extends AnyNestedCondition {
|
||||
|
||||
private OnNoRibbonDefaultCondition() {
|
||||
super(ConfigurationPhase.REGISTER_BEAN);
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled", havingValue = "false")
|
||||
static class RibbonNotEnabled {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnMissingClass("org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient")
|
||||
static class RibbonLoadBalancerNotPresent {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.springframework.cloud.gateway.filter;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -26,6 +25,7 @@ import reactor.core.publisher.Mono;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
|
||||
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
|
||||
import org.springframework.cloud.gateway.support.NotFoundException;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
@@ -74,7 +74,9 @@ public class LoadBalancerClientFilter implements GlobalFilter, Ordered {
|
||||
// preserve the original url
|
||||
addOriginalRequestUrl(exchange, url);
|
||||
|
||||
log.trace("LoadBalancerClientFilter url before: " + url);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("LoadBalancerClientFilter url before: " + url);
|
||||
}
|
||||
|
||||
final ServiceInstance instance = choose(exchange);
|
||||
|
||||
@@ -95,7 +97,10 @@ public class LoadBalancerClientFilter implements GlobalFilter, Ordered {
|
||||
URI requestUrl = loadBalancer.reconstructURI(
|
||||
new DelegatingServiceInstance(instance, overrideScheme), uri);
|
||||
|
||||
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
|
||||
}
|
||||
|
||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
@@ -105,56 +110,4 @@ public class LoadBalancerClientFilter implements GlobalFilter, Ordered {
|
||||
((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
|
||||
}
|
||||
|
||||
class DelegatingServiceInstance implements ServiceInstance {
|
||||
|
||||
final ServiceInstance delegate;
|
||||
|
||||
private String overrideScheme;
|
||||
|
||||
DelegatingServiceInstance(ServiceInstance delegate, String overrideScheme) {
|
||||
this.delegate = delegate;
|
||||
this.overrideScheme = overrideScheme;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceId() {
|
||||
return delegate.getServiceId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
return delegate.getHost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPort() {
|
||||
return delegate.getPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return delegate.isSecure();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getUri() {
|
||||
return delegate.getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getMetadata() {
|
||||
return delegate.getMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getScheme() {
|
||||
String scheme = delegate.getScheme();
|
||||
if (scheme != null) {
|
||||
return scheme;
|
||||
}
|
||||
return this.overrideScheme;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.filter;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.Request;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.Response;
|
||||
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
|
||||
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
|
||||
import org.springframework.cloud.gateway.support.NotFoundException;
|
||||
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;
|
||||
|
||||
/**
|
||||
* A {@link GlobalFilter} implementation that routes requests using reactive Spring Cloud
|
||||
* LoadBalancer.
|
||||
*
|
||||
* @author Spencer Gibb
|
||||
* @author Tim Ysewyn
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
public class ReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {
|
||||
|
||||
private static final Log log = LogFactory
|
||||
.getLog(ReactiveLoadBalancerClientFilter.class);
|
||||
|
||||
private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
|
||||
|
||||
private final LoadBalancerClientFactory clientFactory;
|
||||
|
||||
private LoadBalancerProperties properties;
|
||||
|
||||
public ReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory,
|
||||
LoadBalancerProperties properties) {
|
||||
this.clientFactory = clientFactory;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return LOAD_BALANCER_CLIENT_FILTER_ORDER;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("Duplicates")
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
|
||||
String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
|
||||
if (url == null
|
||||
|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
// preserve the original url
|
||||
addOriginalRequestUrl(exchange, url);
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName()
|
||||
+ " url before: " + url);
|
||||
}
|
||||
|
||||
return choose(exchange).doOnNext(response -> {
|
||||
|
||||
if (!response.hasServer()) {
|
||||
throw NotFoundException.create(properties.isUse404(),
|
||||
"Unable to find instance for " + url.getHost());
|
||||
}
|
||||
|
||||
URI uri = exchange.getRequest().getURI();
|
||||
|
||||
// if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
|
||||
// if the loadbalancer doesn't provide one.
|
||||
String overrideScheme = null;
|
||||
if (schemePrefix != null) {
|
||||
overrideScheme = url.getScheme();
|
||||
}
|
||||
|
||||
DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(
|
||||
response.getServer(), overrideScheme);
|
||||
|
||||
URI requestUrl = LoadBalancerUriTools.reconstructURI(serviceInstance, uri);
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
|
||||
}
|
||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
|
||||
}).then(chain.filter(exchange));
|
||||
}
|
||||
|
||||
private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
|
||||
URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
|
||||
ReactorLoadBalancer<ServiceInstance> loadBalancer = this.clientFactory
|
||||
.getInstance(uri.getHost(), ReactorLoadBalancer.class,
|
||||
ServiceInstance.class);
|
||||
if (loadBalancer == null) {
|
||||
throw new NotFoundException("No loadbalancer available for " + uri.getHost());
|
||||
}
|
||||
return loadBalancer.choose(createRequest());
|
||||
}
|
||||
|
||||
private Request createRequest() {
|
||||
return ReactiveLoadBalancer.REQUEST;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
|
||||
/**
|
||||
* A {@link ServiceInstance} implementation that uses a delegate instance under the hood.
|
||||
*
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class DelegatingServiceInstance implements ServiceInstance {
|
||||
|
||||
final ServiceInstance delegate;
|
||||
|
||||
private String overrideScheme;
|
||||
|
||||
public DelegatingServiceInstance(ServiceInstance delegate, String overrideScheme) {
|
||||
this.delegate = delegate;
|
||||
this.overrideScheme = overrideScheme;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceId() {
|
||||
return delegate.getServiceId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
return delegate.getHost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPort() {
|
||||
return delegate.getPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
// TODO: move to map
|
||||
if ("https".equals(this.overrideScheme) || "wss".equals(this.overrideScheme)) {
|
||||
return true;
|
||||
}
|
||||
return delegate.isSecure();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getUri() {
|
||||
return delegate.getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getMetadata() {
|
||||
return delegate.getMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getScheme() {
|
||||
String scheme = delegate.getScheme();
|
||||
if (scheme != null) {
|
||||
return scheme;
|
||||
}
|
||||
return this.overrideScheme;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,7 +7,8 @@ org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfigur
|
||||
org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration,\
|
||||
org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration,\
|
||||
org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration,\
|
||||
org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration
|
||||
org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration,\
|
||||
org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration
|
||||
|
||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
||||
org.springframework.cloud.gateway.config.GatewayEnvironmentPostProcessor
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* 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.filter;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.LinkedHashSet;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
|
||||
import org.springframework.cloud.gateway.support.NotFoundException;
|
||||
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
|
||||
import org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceSuppliers;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactiveLoadBalancerClientFilter}.
|
||||
*
|
||||
* @author Spencer Gibb
|
||||
* @author Tim Ysewyn
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SuppressWarnings("UnassignedFluxMonoInstance")
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ReactiveLoadBalancerClientFilterTests {
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private LoadBalancerProperties properties;
|
||||
|
||||
@Mock
|
||||
private GatewayFilterChain chain;
|
||||
|
||||
@Mock
|
||||
private LoadBalancerClientFactory clientFactory;
|
||||
|
||||
@InjectMocks
|
||||
private ReactiveLoadBalancerClientFilter filter;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
properties = new LoadBalancerProperties();
|
||||
exchange = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("/mypath").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotFilterWhenGatewayRequestUrlIsMissing() {
|
||||
filter.filter(exchange, chain);
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verifyNoMoreInteractions(chain);
|
||||
verifyZeroInteractions(clientFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotFilterWhenGatewayRequestUrlSchemeIsNotLb() {
|
||||
URI uri = UriComponentsBuilder.fromUriString("http://myservice").build().toUri();
|
||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri);
|
||||
|
||||
filter.filter(exchange, chain);
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verifyNoMoreInteractions(chain);
|
||||
verifyZeroInteractions(clientFactory);
|
||||
}
|
||||
|
||||
@Test(expected = NotFoundException.class)
|
||||
public void shouldThrowExceptionWhenNoServiceInstanceIsFound() {
|
||||
URI uri = UriComponentsBuilder.fromUriString("lb://myservice").build().toUri();
|
||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri);
|
||||
|
||||
filter.filter(exchange, chain).block();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void shouldFilter() {
|
||||
URI url = UriComponentsBuilder.fromUriString("lb://myservice").build().toUri();
|
||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, url);
|
||||
|
||||
ServiceInstance serviceInstance = new DefaultServiceInstance("myservice",
|
||||
"localhost", 8080, true);
|
||||
|
||||
when(clientFactory.getInstance("myservice", ReactorLoadBalancer.class,
|
||||
ServiceInstance.class)).thenReturn(new RoundRobinLoadBalancer("myservice",
|
||||
ServiceInstanceSuppliers.toProvider("myservice", serviceInstance),
|
||||
-1));
|
||||
|
||||
when(chain.filter(exchange)).thenReturn(Mono.empty());
|
||||
|
||||
filter.filter(exchange, chain).block();
|
||||
|
||||
assertThat((LinkedHashSet<URI>) exchange
|
||||
.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR)).contains(url);
|
||||
|
||||
verify(clientFactory).getInstance("myservice", ReactorLoadBalancer.class,
|
||||
ServiceInstance.class);
|
||||
|
||||
verifyNoMoreInteractions(clientFactory);
|
||||
|
||||
assertThat((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR))
|
||||
.isEqualTo(URI.create("https://localhost:8080/mypath"));
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verifyNoMoreInteractions(chain);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void happyPath() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("http://localhost/get?a=b").build();
|
||||
|
||||
URI lbUri = URI.create("lb://service1?a=b");
|
||||
ServerWebExchange webExchange = testFilter(request, lbUri);
|
||||
URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
|
||||
assertThat(uri).hasScheme("http").hasHost("service1-host1").hasParameter("a",
|
||||
"b");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noQueryParams() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost/get")
|
||||
.build();
|
||||
|
||||
ServerWebExchange webExchange = testFilter(request, URI.create("lb://service1"));
|
||||
URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
|
||||
assertThat(uri).hasScheme("http").hasHost("service1-host1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodedParameters() {
|
||||
URI url = UriComponentsBuilder.fromUriString("http://localhost/get?a=b&c=d[]")
|
||||
.buildAndExpand().encode().toUri();
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest.method(HttpMethod.GET, url)
|
||||
.build();
|
||||
|
||||
URI lbUrl = UriComponentsBuilder.fromUriString("lb://service1?a=b&c=d[]")
|
||||
.buildAndExpand().encode().toUri();
|
||||
|
||||
// prove that it is encoded
|
||||
assertThat(lbUrl.getRawQuery()).isEqualTo("a=b&c=d%5B%5D");
|
||||
|
||||
assertThat(lbUrl).hasParameter("c", "d[]");
|
||||
|
||||
ServerWebExchange webExchange = testFilter(request, lbUrl);
|
||||
URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
|
||||
assertThat(uri).hasScheme("http").hasHost("service1-host1").hasParameter("a", "b")
|
||||
.hasParameter("c", "d[]");
|
||||
|
||||
// prove that it is not double encoded
|
||||
assertThat(uri.getRawQuery()).isEqualTo("a=b&c=d%5B%5D");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unencodedParameters() {
|
||||
URI url = URI.create("http://localhost/get?a=b&c=d[]");
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest.method(HttpMethod.GET, url)
|
||||
.build();
|
||||
|
||||
URI lbUrl = URI.create("lb://service1?a=b&c=d[]");
|
||||
|
||||
// prove that it is unencoded
|
||||
assertThat(lbUrl.getRawQuery()).isEqualTo("a=b&c=d[]");
|
||||
|
||||
ServerWebExchange webExchange = testFilter(request, lbUrl);
|
||||
|
||||
URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
|
||||
assertThat(uri).hasScheme("http").hasHost("service1-host1").hasParameter("a", "b")
|
||||
.hasParameter("c", "d[]");
|
||||
|
||||
// prove that it is NOT encoded
|
||||
assertThat(uri.getRawQuery()).isEqualTo("a=b&c=d[]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void happyPathWithAttributeRatherThanScheme() {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("ws://localhost/get?a=b").build();
|
||||
|
||||
URI lbUri = URI.create("ws://service1?a=b");
|
||||
|
||||
exchange = MockServerWebExchange.from(request);
|
||||
exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR, "lb");
|
||||
|
||||
ServerWebExchange webExchange = testFilter(exchange, lbUri);
|
||||
URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
|
||||
assertThat(uri).hasScheme("ws").hasHost("service1-host1").hasParameter("a", "b");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotFilterWhenGatewaySchemePrefixAttrIsNotLb() {
|
||||
URI uri = UriComponentsBuilder.fromUriString("http://myservice").build().toUri();
|
||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri);
|
||||
exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR, "xx");
|
||||
|
||||
filter.filter(exchange, chain);
|
||||
|
||||
verify(chain).filter(exchange);
|
||||
verifyNoMoreInteractions(chain);
|
||||
verifyZeroInteractions(clientFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldThrow4O4ExceptionWhenNoServiceInstanceIsFound() {
|
||||
URI uri = UriComponentsBuilder.fromUriString("lb://service1").build().toUri();
|
||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri);
|
||||
when(clientFactory.getInstance("service1", ReactorLoadBalancer.class,
|
||||
ServiceInstance.class))
|
||||
.thenReturn(new RoundRobinLoadBalancer("service1",
|
||||
ServiceInstanceSuppliers.toProvider("service1"), -1));
|
||||
properties.setUse404(true);
|
||||
ReactiveLoadBalancerClientFilter filter = new ReactiveLoadBalancerClientFilter(
|
||||
clientFactory, properties);
|
||||
when(chain.filter(exchange)).thenReturn(Mono.empty());
|
||||
try {
|
||||
filter.filter(exchange, chain).block();
|
||||
}
|
||||
catch (NotFoundException exception) {
|
||||
assertThat(exception.getStatus()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
private ServerWebExchange testFilter(MockServerHttpRequest request, URI uri) {
|
||||
return testFilter(MockServerWebExchange.from(request), uri);
|
||||
}
|
||||
|
||||
private ServerWebExchange testFilter(ServerWebExchange exchange, URI uri) {
|
||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri);
|
||||
|
||||
ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor
|
||||
.forClass(ServerWebExchange.class);
|
||||
when(chain.filter(captor.capture())).thenReturn(Mono.empty());
|
||||
|
||||
when(clientFactory.getInstance("service1", ReactorLoadBalancer.class,
|
||||
ServiceInstance.class))
|
||||
.thenReturn(new RoundRobinLoadBalancer("service1",
|
||||
ServiceInstanceSuppliers.toProvider("service1",
|
||||
new DefaultServiceInstance("service1_1",
|
||||
"service1", "service1-host1", 8081,
|
||||
false)),
|
||||
-1));
|
||||
|
||||
ReactiveLoadBalancerClientFilter filter = new ReactiveLoadBalancerClientFilter(
|
||||
clientFactory, properties);
|
||||
filter.filter(exchange, chain).block();
|
||||
|
||||
return captor.getValue();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user