diff --git a/pom.xml b/pom.xml
index 68529471..654a4263 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,6 +64,16 @@
+
+ org.springframework.cloud
+ spring-cloud-starter-hystrix
+ true
+
+
+ io.reactivex
+ rxjava-reactive-streams
+ 1.2.1
+
org.springframework.boot
spring-boot-devtools
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 ec626f3c..bd7e3167 100644
--- a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
+++ b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
@@ -14,10 +14,12 @@ 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.RoutingFilter;
import org.springframework.cloud.gateway.filter.WriteResponseFilter;
import org.springframework.cloud.gateway.filter.route.AddRequestHeaderRouteFilter;
import org.springframework.cloud.gateway.filter.route.AddRequestParameterRouteFilter;
import org.springframework.cloud.gateway.filter.route.AddResponseHeaderRouteFilter;
+import org.springframework.cloud.gateway.filter.route.HystrixRouteFilter;
import org.springframework.cloud.gateway.filter.route.RedirectToRouteFilter;
import org.springframework.cloud.gateway.filter.route.RemoveRequestHeaderRouteFilter;
import org.springframework.cloud.gateway.filter.route.RemoveResponseHeaderRouteFilter;
@@ -74,10 +76,10 @@ public class GatewayAutoConfiguration {
}
@Bean
- public FilteringWebHandler filteringWebHandler(RoutingWebHandler webHandler,
+ public FilteringWebHandler filteringWebHandler(//RoutingWebHandler webHandler,
List globalFilters,
Map routeFilters) {
- return new FilteringWebHandler(webHandler, globalFilters, routeFilters);
+ return new FilteringWebHandler(/*webHandler,*/ globalFilters, routeFilters);
}
@Bean
@@ -96,6 +98,11 @@ public class GatewayAutoConfiguration {
return new LoadBalancerClientFilter(client);
}
+ @Bean
+ public RoutingFilter routingFilter(HttpClient httpClient) {
+ return new RoutingFilter(httpClient);
+ }
+
@Bean
public RouteToRequestUrlFilter routeToRequestUrlFilter() {
return new RouteToRequestUrlFilter();
@@ -155,6 +162,11 @@ public class GatewayAutoConfiguration {
return new AddResponseHeaderRouteFilter();
}
+ @Bean(name = "HystrixRouteFilter")
+ public HystrixRouteFilter hystrixRouteFilter() {
+ return new HystrixRouteFilter();
+ }
+
@Bean(name = "RedirectToRouteFilter")
public RedirectToRouteFilter redirectToRouteFilter() {
return new RedirectToRouteFilter();
diff --git a/src/main/java/org/springframework/cloud/gateway/filter/RoutingFilter.java b/src/main/java/org/springframework/cloud/gateway/filter/RoutingFilter.java
new file mode 100644
index 00000000..7bd6054d
--- /dev/null
+++ b/src/main/java/org/springframework/cloud/gateway/filter/RoutingFilter.java
@@ -0,0 +1,75 @@
+package org.springframework.cloud.gateway.filter;
+
+import java.net.URI;
+import java.util.Optional;
+
+import org.springframework.core.Ordered;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilterChain;
+import org.springframework.web.server.WebHandler;
+
+import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR;
+import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
+
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.http.DefaultHttpHeaders;
+import io.netty.handler.codec.http.HttpMethod;
+import reactor.core.publisher.Mono;
+import reactor.ipc.netty.NettyPipeline;
+import reactor.ipc.netty.http.client.HttpClient;
+
+/**
+ * @author Spencer Gibb
+ */
+public class RoutingFilter implements GlobalFilter, Ordered {
+
+ private final HttpClient httpClient;
+
+ public RoutingFilter(HttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ @Override
+ public int getOrder() {
+ return 2000000;
+ }
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
+ Optional requestUrl = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
+ ServerHttpRequest request = exchange.getRequest();
+
+ final HttpMethod method = HttpMethod.valueOf(request.getMethod().toString());
+ final String url = requestUrl.get().toString();
+
+ final DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
+ request.getHeaders().forEach(httpHeaders::set);
+
+ return this.httpClient.request(method, url, req ->
+ req.options(NettyPipeline.SendOptions::flushOnEach)
+ .headers(httpHeaders)
+ .sendHeaders()
+ .send(request.getBody()
+ .map(DataBuffer::asByteBuffer)
+ .map(Unpooled::wrappedBuffer)))
+ .then(res -> {
+ ServerHttpResponse response = exchange.getResponse();
+ // put headers and status so filters can modify the response
+ final HttpHeaders headers = new HttpHeaders();
+ res.responseHeaders().forEach(entry -> headers.add(entry.getKey(), entry.getValue()));
+
+ response.getHeaders().putAll(headers);
+ response.setStatusCode(HttpStatus.valueOf(res.status().code()));
+
+ // Defer committing the response until all route filters have run
+ // Put client response as ServerWebExchange attribute and write response later WriteResponseFilter
+ exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
+ return Mono.empty();
+ }).then(chain.filter(exchange));
+ }
+}
diff --git a/src/main/java/org/springframework/cloud/gateway/filter/route/HystrixRouteFilter.java b/src/main/java/org/springframework/cloud/gateway/filter/route/HystrixRouteFilter.java
new file mode 100644
index 00000000..7c8a8616
--- /dev/null
+++ b/src/main/java/org/springframework/cloud/gateway/filter/route/HystrixRouteFilter.java
@@ -0,0 +1,64 @@
+package org.springframework.cloud.gateway.filter.route;
+
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+
+import com.netflix.hystrix.HystrixCommandGroupKey;
+import com.netflix.hystrix.HystrixCommandKey;
+import com.netflix.hystrix.HystrixObservableCommand;
+
+import reactor.core.publisher.Mono;
+import rx.Observable;
+import rx.RxReactiveStreams;
+import rx.Subscription;
+
+/**
+ * @author Spencer Gibb
+ */
+public class HystrixRouteFilter implements RouteFilter {
+
+ @Override
+ public WebFilter apply(String commandName, String[] args) {
+ //validate(args, 1);
+
+ final HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey(getClass().getSimpleName());
+ final HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey(commandName);
+
+ final HystrixObservableCommand.Setter setter = HystrixObservableCommand.Setter
+ .withGroupKey(groupKey)
+ .andCommandKey(commandKey);
+
+ //TODO: caching can happen here
+ return (exchange, chain) -> {
+ try {
+ RouteHystrixCommand command = new RouteHystrixCommand(setter, exchange, chain);
+ final Observable observable = command.toObservable();
+
+ return Mono.create(s -> {
+ final Subscription subscription = observable.subscribe(s::success, s::error, s::success);
+ s.setCancellation(subscription::unsubscribe);
+ });
+ } catch (Exception e) {
+ throw new RuntimeException("Error running RouteHystrixCommand: " + commandName, e);
+ }
+ };
+ }
+
+ //TODO: replace with HystrixMonoCommand that we write
+ private class RouteHystrixCommand extends HystrixObservableCommand {
+ private final ServerWebExchange exchange;
+ private final WebFilterChain chain;
+
+ protected RouteHystrixCommand(Setter setter, ServerWebExchange exchange, WebFilterChain chain) {
+ super(setter);
+ this.exchange = exchange;
+ this.chain = chain;
+ }
+
+ @Override
+ protected Observable construct() {
+ return RxReactiveStreams.toObservable(this.chain.filter(this.exchange));
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java b/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java
index b95f834c..032e9c8d 100644
--- a/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java
+++ b/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java
@@ -58,6 +58,11 @@ public class FilteringWebHandler extends WebHandlerDecorator {
private final List globalFilters;
private final Map routeFilters = new HashMap<>();
+ public FilteringWebHandler(List globalFilters,
+ Map routeFilters) {
+ this(new EmptyWebHandler(), globalFilters, routeFilters);
+ }
+
public FilteringWebHandler(WebHandler targetHandler, List globalFilters,
Map routeFilters) {
super(targetHandler);
@@ -92,6 +97,8 @@ public class FilteringWebHandler extends WebHandlerDecorator {
AnnotationAwareOrderComparator.sort(routeFilters);
+ logger.debug("Sorted routeFilters: "+ routeFilters);
+
return new DefaultWebFilterChain(routeFilters, getDelegate()).filter(exchange);
}
@@ -211,4 +218,11 @@ public class FilteringWebHandler extends WebHandlerDecorator {
}
}
+ private static class EmptyWebHandler implements WebHandler {
+ @Override
+ public Mono handle(ServerWebExchange exchange) {
+ return Mono.empty();
+ }
+ }
+
}
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 e42d56c4..2e3202b6 100644
--- a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java
+++ b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java
@@ -210,6 +210,44 @@ public class GatewayIntegrationTests {
);
}
+ @Test
+ public void hystrixFilterWorks() {
+ Mono result = webClient.exchange(
+ GET("http://localhost:" + port + "/get")
+ .header("Host", "www.hystrixsuccess.org")
+ .build()
+ );
+
+ verify( () ->
+ StepVerifier.create(result)
+ .consumeNextWith(
+ response -> {
+ HttpHeaders httpHeaders = response.headers().asHttpHeaders();
+ assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER))
+ .isEqualTo("hystrix_success_test");
+ HttpStatus statusCode = response.statusCode();
+ assertThat(statusCode).isEqualTo(HttpStatus.OK);
+ })
+ .expectComplete()
+ .verify(DURATION)
+ );
+ }
+
+ @Test
+ public void hystrixFilterTimesout() {
+ Mono result = webClient.exchange(
+ GET("http://localhost:" + port + "/delay/3")
+ .header("Host", "www.hystrixfailure.org")
+ .build()
+ );
+
+ verify( () ->
+ StepVerifier.create(result)
+ .expectError()
+ .verify()
+ );
+ }
+
@Test
public void loadBalancerFilterWorks() {
Mono result = webClient.exchange(
@@ -229,7 +267,7 @@ public class GatewayIntegrationTests {
assertThat(statusCode).isEqualTo(HttpStatus.OK);
})
.expectComplete()
- .verify(Duration.ofMinutes(5))
+ .verify(DURATION)
);
}
diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml
index 11c76c04..e9b59078 100644
--- a/src/test/resources/application.yml
+++ b/src/test/resources/application.yml
@@ -47,6 +47,22 @@ spring:
filters:
- AddResponseHeader=X-Request-Foo, Bar
+ # =====================================
+ - id: hystrix_failure_test
+ uri: http://httpbin.org:80
+ predicates:
+ - Host=**.hystrixfailure.org
+ filters:
+ - Hystrix=failcmd
+
+ # =====================================
+ - id: hystrix_success_test
+ uri: http://httpbin.org:80
+ predicates:
+ - Host=**.hystrixsuccess.org
+ filters:
+ - Hystrix=successcmd
+
# =====================================
- id: load_balancer_client_test
uri: lb://myservice
@@ -141,6 +157,8 @@ myservice:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
listOfServers: httpbin.org:80
+#hystrix.command.failcmd.execution.isolation.thread.timeoutInMilliseconds: 1000
+
logging:
level:
org.springframework.cloud.gateway: TRACE