diff --git a/pom.xml b/pom.xml index f1a76d1d0..685294713 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,21 @@ spring-boot-configuration-processor true + + org.springframework.cloud + spring-cloud-starter-ribbon + true + + + com.netflix.ribbon + ribbon-transport + + + io.reactivex + rxnetty + + + org.springframework.boot spring-boot-devtools @@ -75,6 +90,20 @@ pom import + + org.springframework.cloud + spring-cloud-commons-dependencies + 1.2.0.BUILD-SNAPSHOT + pom + import + + + org.springframework.cloud + spring-cloud-netflix-dependencies + 1.3.0.BUILD-SNAPSHOT + pom + import + diff --git a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 593fa02e4..ec626f3cd 100644 --- a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -4,12 +4,15 @@ import java.util.List; import java.util.Map; import org.springframework.boot.actuate.endpoint.Endpoint; +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.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.gateway.actuate.GatewayEndpoint; import org.springframework.cloud.gateway.api.RouteReader; import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter; import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter; import org.springframework.cloud.gateway.filter.WriteResponseFilter; import org.springframework.cloud.gateway.filter.route.AddRequestHeaderRouteFilter; @@ -86,6 +89,13 @@ public class GatewayAutoConfiguration { // GlobalFilter beans + @Bean + @ConditionalOnClass(LoadBalancerClient.class) + @ConditionalOnBean(LoadBalancerClient.class) + public LoadBalancerClientFilter loadBalancerClientFilter(LoadBalancerClient client) { + return new LoadBalancerClientFilter(client); + } + @Bean public RouteToRequestUrlFilter routeToRequestUrlFilter() { return new RouteToRequestUrlFilter(); diff --git a/src/main/java/org/springframework/cloud/gateway/filter/LoadBalancerClientFilter.java b/src/main/java/org/springframework/cloud/gateway/filter/LoadBalancerClientFilter.java new file mode 100644 index 000000000..b8edcd45b --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/filter/LoadBalancerClientFilter.java @@ -0,0 +1,59 @@ +package org.springframework.cloud.gateway.filter; + +import java.net.URI; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.core.Ordered; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.util.UriComponentsBuilder; + +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.getAttribute; + +import reactor.core.publisher.Mono; + +/** + * @author Spencer Gibb + */ +public class LoadBalancerClientFilter implements GlobalFilter, Ordered { + + private static final Log log = LogFactory.getLog(LoadBalancerClientFilter.class); + public static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10100; + + private final LoadBalancerClient loadBalancer; + + public LoadBalancerClientFilter(LoadBalancerClient loadBalancer) { + this.loadBalancer = loadBalancer; + } + + @Override + public int getOrder() { + return LOAD_BALANCER_CLIENT_FILTER_ORDER; + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + URI url = getAttribute(exchange, GATEWAY_REQUEST_URL_ATTR, URI.class); + if (url == null || !url.getScheme().equals("lb")) { + return chain.filter(exchange); + } + log.trace("LoadBalancerClientFilter url before: " + url); + + final ServiceInstance instance = loadBalancer.choose(url.getHost()); + + URI requestUrl = UriComponentsBuilder.fromUri(url) + .scheme(instance.isSecure()? "https" : "http") //TODO: support websockets + .host(instance.getHost()) + .port(instance.getPort()) + .build(true) + .toUri(); + log.trace("LoadBalancerClientFilter url chosen: " + requestUrl); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); + return chain.filter(exchange); + } + +} diff --git a/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java b/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java index 815cc9924..268666ac9 100644 --- a/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java +++ b/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java @@ -5,7 +5,6 @@ import java.net.URI; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.gateway.config.Route; -import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.core.Ordered; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilterChain; diff --git a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java index ff21418a7..e42d56c49 100644 --- a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java +++ b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java @@ -38,7 +38,8 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment = RANDOM_PORT) +@SpringBootTest(webEnvironment = RANDOM_PORT, + properties = "spring.cloud.bootstrap.enabled=false") @SuppressWarnings("unchecked") public class GatewayIntegrationTests { @@ -209,6 +210,29 @@ public class GatewayIntegrationTests { ); } + @Test + public void loadBalancerFilterWorks() { + Mono result = webClient.exchange( + GET("http://localhost:" + port + "/get") + .header("Host", "www.loadbalancerclient.org") + .build() + ); + + verify( () -> + StepVerifier.create(result) + .consumeNextWith( + response -> { + HttpHeaders httpHeaders = response.headers().asHttpHeaders(); + assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER)) + .isEqualTo("load_balancer_client_test"); + HttpStatus statusCode = response.statusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.OK); + }) + .expectComplete() + .verify(Duration.ofMinutes(5)) + ); + } + @Test public void postWorks() { ClientRequest> request = POST("http://localhost:" + port + "/post") diff --git a/src/test/java/org/springframework/cloud/gateway/test/GatewayTestApplication.java b/src/test/java/org/springframework/cloud/gateway/test/GatewayTestApplication.java index 86017a18d..47ea6b140 100644 --- a/src/test/java/org/springframework/cloud/gateway/test/GatewayTestApplication.java +++ b/src/test/java/org/springframework/cloud/gateway/test/GatewayTestApplication.java @@ -9,6 +9,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; public class GatewayTestApplication { public static void main(String[] args) { + System.setProperty("spring.cloud.bootstrap.enabled", "false"); //TODO: fix bootstrap SpringApplication.run(GatewayTestApplication.class, args); } } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 36e837402..11c76c046 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -47,6 +47,12 @@ spring: filters: - AddResponseHeader=X-Request-Foo, Bar + # ===================================== + - id: load_balancer_client_test + uri: lb://myservice + predicates: + - Host=**.loadbalancerclient.org + # ===================================== - id: redirect_to_test uri: http://httpbin.org:80 @@ -129,6 +135,12 @@ spring: predicates: - name: Url value: /** + +myservice: + ribbon: + NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList + listOfServers: httpbin.org:80 + logging: level: org.springframework.cloud.gateway: TRACE