Create HystrixRouteFilter
This commit is contained in:
10
pom.xml
10
pom.xml
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user