Create HystrixRouteFilter

This commit is contained in:
Spencer Gibb
2017-01-17 14:41:05 -07:00
parent 9c43fb6af1
commit 754fb537b0
7 changed files with 234 additions and 3 deletions

10
pom.xml
View File

@@ -64,6 +64,16 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava-reactive-streams</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>

View File

@@ -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<GlobalFilter> globalFilters,
Map<String, RouteFilter> 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();

View File

@@ -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<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Optional<URI> 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));
}
}

View File

@@ -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<Void> 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<Void> {
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<Void> construct() {
return RxReactiveStreams.toObservable(this.chain.filter(this.exchange));
}
}
}

View File

@@ -58,6 +58,11 @@ public class FilteringWebHandler extends WebHandlerDecorator {
private final List<GlobalFilter> globalFilters;
private final Map<String, RouteFilter> routeFilters = new HashMap<>();
public FilteringWebHandler(List<GlobalFilter> globalFilters,
Map<String, RouteFilter> routeFilters) {
this(new EmptyWebHandler(), globalFilters, routeFilters);
}
public FilteringWebHandler(WebHandler targetHandler, List<GlobalFilter> globalFilters,
Map<String, RouteFilter> 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<Void> handle(ServerWebExchange exchange) {
return Mono.empty();
}
}
}

View File

@@ -210,6 +210,44 @@ public class GatewayIntegrationTests {
);
}
@Test
public void hystrixFilterWorks() {
Mono<ClientResponse> 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<ClientResponse> 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<ClientResponse> result = webClient.exchange(
@@ -229,7 +267,7 @@ public class GatewayIntegrationTests {
assertThat(statusCode).isEqualTo(HttpStatus.OK);
})
.expectComplete()
.verify(Duration.ofMinutes(5))
.verify(DURATION)
);
}

View File

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