Spring Cloud Gateway 默認的filter功能和執行順序介紹
Spring Cloud Gateway 默認的filter功能和執行順序
有效性
Spring Cloud Gateway 2.0.0.RELEASE
調試方法
新建一個GlobalFilter,在filter中加斷點即可調試filter,通過chain參數可以查看其它的filter及執行順序(order)
filters(按執行順序)
1. AdaptCachedBodyGlobalFilter
核心代碼
public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 1000; } public static final String CACHED_REQUEST_BODY_KEY = "cachedRequestBody"; public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { Flux<DataBuffer> body = exchange.getAttributeOrDefault(CACHED_REQUEST_BODY_KEY, null); if (body != null) { ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public Flux<DataBuffer> getBody() { return body; } }; return chain.filter(exchange.mutate().request(decorator).build()); } return chain.filter(exchange); }
提供替換request 的 body的能力
2.NettyWriteResponseFilter
核心代碼
public static final int WRITE_RESPONSE_FILTER_ORDER = -1; public int getOrder() { return WRITE_RESPONSE_FILTER_ORDER; } public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { return chain.filter(exchange).then(Mono.defer(() -> { //見 後文的 NettyRoutingFilter HttpClientResponse clientResponse = exchange.getAttribute(CLIENT_RESPONSE_ATTR); ServerHttpResponse response = exchange.getResponse(); NettyDataBufferFactory factory = (NettyDataBufferFactory) response.bufferFactory(); final Flux<NettyDataBuffer> body = clientResponse.receive() .map(factory::wrap); MediaType contentType = response.getHeaders().getContentType(); return (isStreamingMediaType(contentType) ? response.writeAndFlushWith(body.map(Flux::just)) : response.writeWith(body)); })); }
具體的將被代理的服務的內容返回的類,文檔
3.ForwardPathFilter
核心代碼
public int getOrder() { return 0; } public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); URI routeUri = route.getUri(); String scheme = routeUri.getScheme(); if (isAlreadyRouted(exchange) || !"forward".equals(scheme)) { return chain.filter(exchange); } exchange = exchange.mutate().request( exchange.getRequest().mutate().path(routeUri.getPath()).build()) .build(); return chain.filter(exchange); }
forward協議的url替換類
4.在Route中配置的各種GatewayFilter
核心代碼
/** * RouteDefinitionRouteLocator#loadGatewayFilters GatewayFilter的order */ ArrayList<GatewayFilter> ordered = new ArrayList<>(filters.size()); for (int i = 0; i < filters.size(); i++) { GatewayFilter gatewayFilter = filters.get(i); if (gatewayFilter instanceof Ordered) { ordered.add(gatewayFilter); } else { ordered.add(new OrderedGatewayFilter(gatewayFilter, i + 1)); } } return ordered;
根據配置不同實現具體的功能,詳見文檔
5.RouteToRequestUrlFilter
核心代碼
public static final int ROUTE_TO_URL_FILTER_ORDER = 10000; public int getOrder() { return ROUTE_TO_URL_FILTER_ORDER; } public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); if (route == null) { return chain.filter(exchange); } URI uri = exchange.getRequest().getURI(); boolean encoded = containsEncodedParts(uri); URI routeUri = route.getUri(); //匹配 http:http://locahost:80/a/b/c?q=1,並把第一個 http: 去掉 if (hasAnotherScheme(routeUri)) { // uri格式 [scheme:]scheme-specific-part[#fragment] exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR, routeUri.getScheme()); routeUri = URI.create(routeUri.getSchemeSpecificPart()); } URI requestUrl = UriComponentsBuilder.fromUri(uri) .uri(routeUri) .build(encoded) .toUri(); exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); return chain.filter(exchange); } private static final String SCHEME_REGEX = "[a-zA-Z]([a-zA-Z]|\\d|\\+|\\.|-)*:.*"; static final Pattern schemePattern = Pattern.compile(SCHEME_REGEX); static boolean hasAnotherScheme(URI uri) { return schemePattern.matcher(uri.getSchemeSpecificPart()).matches() && uri.getHost() == null && uri.getRawPath() == null; }
路由功能的具體執行類,文檔
6.LoadBalancerClientFilter(如果啟用瞭eureka)
核心代碼
public static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10100; public int getOrder() { return LOAD_BALANCER_CLIENT_FILTER_ORDER; } 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); } //一大波轉換操作 addOriginalRequestUrl(exchange, url); final ServiceInstance instance = loadBalancer.choose(url.getHost()); if (instance == null) { throw new NotFoundException("Unable to find instance for " + url.getHost()); } URI uri = exchange.getRequest().getURI(); String overrideScheme = null; if (schemePrefix != null) { overrideScheme = url.getScheme(); } URI requestUrl = loadBalancer.reconstructURI(new DelegatingServiceInstance(instance, overrideScheme), uri); //轉換後的url填入 GATEWAY_REQUEST_URL_ATTR 屬性 exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); return chain.filter(exchange); }
lb協議的路由功能,文檔
7.WebsocketRoutingFilter
核心代碼
public int getOrder() { return Ordered.LOWEST_PRECEDENCE - 1; } public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //upgrade頭 見https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism //或見 https://httpwg.org/specs/rfc7230.html#header.upgrade changeSchemeIfIsWebSocketUpgrade(exchange); //跳過一大波參數檢查與參數獲取 return this.webSocketService.handleRequest(exchange, new ProxyWebSocketHandler(requestUrl, this.webSocketClient, filtered, protocols)); } /** * ProxyWebSocketHandler#handle 橋接兩個webSocket */ public Mono<Void> handle(WebSocketSession session) { //session為客戶端 return client.execute(url, this.headers, new WebSocketHandler() { @Override public Mono<Void> handle(WebSocketSession proxySession) { //proxySession為被代理的WebSocket Mono<Void> proxySessionSend = proxySession .send(session.receive().doOnNext(WebSocketMessage::retain)); Mono<Void> serverSessionSend = session .send(proxySession.receive().doOnNext(WebSocketMessage::retain)); return Mono.zip(proxySessionSend, serverSessionSend).then(); } //省略其它方法 }); }
WebSocket的代理功能,文檔
8.NettyRoutingFilter
核心代碼
public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); //省略一大波參數獲取和參數校驗 final HttpMethod method = HttpMethod.valueOf(request.getMethod().toString()); final String url = requestUrl.toString(); return this.httpClient.request(method, url, req -> { //省略http數據發送代碼 }).doOnNext(res -> { ServerHttpResponse response = exchange.getResponse(); HttpHeaders headers = new HttpHeaders(); res.responseHeaders().forEach(entry -> headers.add(entry.getKey(), entry.getValue())); //註意,如果ContentType為null會 NPE,特別是301或302跳轉 exchange.getAttributes().put("original_response_content_type", headers.getContentType()); //省略其它http解析代碼 exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res); //與前面的 NettyWriteResponseFilter 對應 }).then(chain.filter(exchange)); } }
http協議的代理功能,文檔
9.ForwardRoutingFilter
核心代碼
public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); String scheme = requestUrl.getScheme(); if (isAlreadyRouted(exchange) || !"forward".equals(scheme)) { return chain.filter(exchange); } setAlreadyRouted(exchange); if (log.isTraceEnabled()) { log.trace("Forwarding to URI: "+requestUrl); } return this.dispatcherHandler.handle(exchange); }
將未處理的forward協議的請求交由spring來處理,文檔
其中 NettyRoutingFilter 和 NettyWriteResponseFilter 內置有 WebClientHttpRoutingFilter和WebClientWriteResponseFilter 作為備用替換版本。
spring cloud gateway之filter實戰
1、filter的作用和生命周期
由filter工作流程點,可以知道filter有著非常重要的作用,在“pre”類型的過濾器可以做參數校驗、權限校驗、流量監控、日志輸出、協議轉換等,在“post”類型的過濾器中可以做響應內容、響應頭的修改,日志的輸出,流量監控等。首先需要弄清一點為什麼需要網關這一層,這就不得不說下filter的作用瞭。
作用
當我們有很多個服務時,比如下圖中的user-service、goods-service、sales-service等服務,客戶端請求各個服務的Api時,每個服務都需要做相同的事情,比如鑒權、限流、日志輸出等。
對於這樣重復的工作,有沒有辦法做的更好,答案是肯定的。在微服務的上一層加一個全局的權限控制、限流、日志輸出的Api Gatewat服務,然後再將請求轉發到具體的業務服務層。這個Api Gateway服務就是起到一個服務邊界的作用,外接的請求訪問系統,必須先通過網關層。
生命周期
Spring Cloud Gateway同zuul類似,有“pre”和“post”兩種方式的filter。客戶端的請求先經過“pre”類型的filter,然後將請求轉發到具體的業務服務,比如上圖中的user-service,收到業務服務的響應之後,再經過“post”類型的filter處理,最後返回響應到客戶端。
與zuul不同的是,filter除瞭分為“pre”和“post”兩種方式的filter外,在Spring Cloud Gateway中,filter從作用范圍可分為另外兩種,一種是針對於單個路由的gateway filter,它在配置文件中的寫法同predict類似;另外一種是針對於所有路由的global gateway filer。現在從作用范圍劃分的維度來講解這兩種filter。
gateway filter
過濾器允許以某種方式修改傳入的HTTP請求或傳出的HTTP響應。過濾器可以限定作用在某些特定請求路徑上。 Spring Cloud Gateway包含許多內置的GatewayFilter工廠。
GatewayFilter工廠同上一篇介紹的Predicate工廠類似,都是在配置文件application.yml中配置,遵循瞭約定大於配置的思想,隻需要在配置文件配置GatewayFilter Factory的名稱,而不需要寫全部的類名,比如AddRequestHeaderGatewayFilterFactory隻需要在配置文件中寫AddRequestHeader,而不是全部類名。在配置文件中配置的GatewayFilter Factory最終都會相應的過濾器工廠類處理。
Spring Cloud Gateway 內置的過濾器工廠一覽表如下:
現在挑幾個常見的過濾器工廠來講解,每一個過濾器工廠在官方文檔都給出瞭詳細的使用案例,如果不清楚的還可以在org.springframework.cloud.gateway.filter.factory看每一個過濾器工廠的源碼。
2、AddRequestHeader GatewayFilter Factory
A.創建子工程gateway-filter
B.引入相關的依賴
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> </dependencies>
C.application.yml
server: port: 8081 spring: profiles: active: add_request_header_route --- spring: cloud: gateway: routes: - id: add_request_header_route uri: http://httpbin.org:80/get filters: - AddRequestHeader=X-Request-Foo, Bar predicates: - After=2017-01-20T17:42:47.789-07:00[America/Denver] profiles: add_request_header_route
在上述的配置中,工程的啟動端口為8081,配置文件為add_request_header_route,在add_request_header_route配置中,配置瞭roter的id為add_request_header_route,路由地址為http://httpbin.org:80/get,該router有AfterPredictFactory,有一個filter為AddRequestHeaderGatewayFilterFactory(約定寫成AddRequestHeader),AddRequestHeader過濾器工廠會在請求頭加上一對請求頭,名稱為X-Request-Foo,值為Bar。為瞭驗證AddRequestHeaderGatewayFilterFactory是怎麼樣工作的,查看它的源碼,AddRequestHeaderGatewayFilterFactory的源碼如下:
public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory { @Override public GatewayFilter apply(NameValueConfig config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest().mutate() .header(config.getName(), config.getValue()) .build(); return chain.filter(exchange.mutate().request(request).build()); }; } }
由上面的代碼可知,根據舊的ServerHttpRequest創建新的 ServerHttpRequest ,在新的ServerHttpRequest加瞭一個請求頭,然後創建新的 ServerWebExchange ,提交過濾器鏈繼續過濾。
啟動工程,通過curl命令來模擬請求:
curl localhost:8081
最終顯示瞭從 http://httpbin.org:80/get得到瞭請求,響應如下:
{ "args": {}, "headers": { "Accept": "*/*", "Connection": "close", "Forwarded": "proto=http;host=\"localhost:8081\";for=\"0:0:0:0:0:0:0:1:56248\"", "Host": "httpbin.org", "User-Agent": "curl/7.58.0", "X-Forwarded-Host": "localhost:8081", "X-Request-Foo": "Bar" }, "origin": "0:0:0:0:0:0:0:1, 210.22.21.66", "url": "http://localhost:8081/get" }
可以上面的響應可知,確實在請求頭中加入瞭X-Request-Foo這樣的一個請求頭,在配置文件中配置的AddRequestHeader過濾器工廠生效。
跟AddRequestHeader過濾器工廠類似的還有AddResponseHeader過濾器工廠,在此就不再重復。
RewritePath GatewayFilter Factory
在Nginx服務啟中有一個非常強大的功能就是重寫路徑,Spring Cloud Gateway默認也提供瞭這樣的功能,這個功能是Zuul沒有的。在配置文件中加上以下的配置:
spring: profiles: active: rewritepath_route --- spring: cloud: gateway: routes: - id: rewritepath_route uri: https://blog.csdn.net predicates: - Path=/foo/** filters: - RewritePath=/foo/(?<segment>.*), /$\{segment} profiles: rewritepath_route
上面的配置中,所有的/foo/**開始的路徑都會命中配置的router,並執行過濾器的邏輯,在本案例中配置瞭RewritePath過濾器工廠,此工廠將/foo/(?.*)重寫為{segment},然後轉發到https://blog.csdn.net。比如在網頁上請求localhost:8081/foo/forezp,此時會將請求轉發到https://blog.csdn.net/forezp的頁面,比如在網頁上請求localhost:8081/foo/forezp/1,頁面顯示404,就是因為不存在https://blog.csdn.net/forezp/1這個頁面。
自定義過濾器
Spring Cloud Gateway內置瞭19種強大的過濾器工廠,能夠滿足很多場景的需求,那麼能不能自定義自己的過濾器呢,當然是可以的。在spring Cloud Gateway中,過濾器需要實現GatewayFilter和Ordered2個接口。寫一個RequestTimeFilter,代碼如下:
public class RequestTimeFilter implements GatewayFilter, Ordered { private static final Log log = LogFactory.getLog(GatewayFilter.class); private static final String REQUEST_TIME_BEGIN = "requestTimeBegin"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis()); return chain.filter(exchange).then( Mono.fromRunnable(() -> { Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN); if (startTime != null) { log.info(exchange.getRequest().getURI().getRawPath() + ": " + (System.currentTimeMillis() - startTime) + "ms"); } }) ); } @Override public int getOrder() { return 0; } }
在上面的代碼中,Ordered中的int getOrder()方法是來給過濾器設定優先級別的,值越大則優先級越低。還有有一個filterI(exchange,chain)方法,在該方法中,先記錄瞭請求的開始時間,並保存在ServerWebExchange中,此處是一個“pre”類型的過濾器,然後再chain.filter的內部類中的run()方法中相當於”post”過濾器,在此處打印瞭請求所消耗的時間。然後將該過濾器註冊到router中,代碼如下:
@Bean public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) { // @formatter:off return builder.routes() .route(r -> r.path("/customer/**") .filters(f -> f.filter(new RequestTimeFilter()) .addResponseHeader("X-Response-Default-Foo", "Default-Bar")) .uri("http://httpbin.org:80/get") .order(0) .id("customer_filter_router") ) .build(); // @formatter:on }
自定義過濾器工廠
在上面的自定義過濾器中,有沒有辦法自定義過濾器工廠類呢?這樣就可以在配置文件中配置過濾器瞭。現在需要實現一個過濾器工廠,在打印時間的時候,可以設置參數來決定是否打印請參數。查看GatewayFilterFactory的源碼,可以發現GatewayFilterfactory的層級如下:
過濾器工廠的頂級接口是GatewayFilterFactory,我們可以直接繼承它的兩個抽象類來簡化開發AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,這兩個抽象類的區別就是前者接收一個參數(像StripPrefix和我們創建的這種),後者接收兩個參數(像AddResponseHeader)。
過濾器工廠的頂級接口是GatewayFilterFactory,有2個兩個較接近具體實現的抽象類,分別為AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,這2個類前者接收一個參數,比如它的實現類RedirectToGatewayFilterFactory;後者接收2個參數,比如它的實現類AddRequestHeaderGatewayFilterFactory類。現在需要將請求的日志打印出來,需要使用一個參數,這時可以參照RedirectToGatewayFilterFactory的寫法。
public class RequestTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimeGatewayFilterFactory.Config> { private static final Log log = LogFactory.getLog(GatewayFilter.class); private static final String REQUEST_TIME_BEGIN = "requestTimeBegin"; private static final String KEY = "withParams"; @Override public List<String> shortcutFieldOrder() { return Arrays.asList(KEY); } public RequestTimeGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis()); return chain.filter(exchange).then( Mono.fromRunnable(() -> { Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN); if (startTime != null) { StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath()) .append(": ") .append(System.currentTimeMillis() - startTime) .append("ms"); if (config.isWithParams()) { sb.append(" params:").append(exchange.getRequest().getQueryParams()); } log.info(sb.toString()); } }) ); }; } public static class Config { private boolean withParams; public boolean isWithParams() { return withParams; } public void setWithParams(boolean withParams) { this.withParams = withParams; } } }
在上面的代碼中 apply(Config config)方法內創建瞭一個GatewayFilter的匿名類,具體的實現邏輯跟之前一樣,隻不過加瞭是否打印請求參數的邏輯,而這個邏輯的開關是config.isWithParams()。靜態內部類類Config就是為瞭接收那個boolean類型的參數服務的,裡邊的變量名可以隨意寫,但是要重寫List shortcutFieldOrder()這個方法。。
需要註意的是,在類的構造器中一定要調用下父類的構造器把Config類型傳過去,否則會報ClassCastException
最後,需要在工程的啟動文件Application類中,向Srping Ioc容器註冊RequestTimeGatewayFilterFactory類的Bean。
@Bean public RequestTimeGatewayFilterFactory elapsedGatewayFilterFactory() { return new RequestTimeGatewayFilterFactory(); }
然後可以在配置文件中配置如下:
spring: profiles: active: elapse_route --- spring: cloud: gateway: routes: - id: elapse_route uri: http://httpbin.org:80/get filters: - RequestTime=false predicates: - After=2017-01-20T17:42:47.789-07:00[America/Denver] profiles: elapse_route
啟動工程,在瀏覽器上訪問localhost:8081?name=forezp,可以在控制臺上看到,日志輸出瞭請求消耗的時間和請求參數。
global filter
Spring Cloud Gateway根據作用范圍劃分為GatewayFilter和GlobalFilter,二者區別如下:
- GatewayFilter : 需要通過spring.cloud.routes.filters 配置在具體路由下,隻作用在當前路由上或通過spring.cloud.default-filters配置在全局,作用在所有路由上
- GlobalFilter : 全局過濾器,不需要在配置文件中配置,作用在所有的路由上,最終通過GatewayFilterAdapter包裝成GatewayFilterChain可識別的過濾器,它為請求業務以及路由的URI轉換為真實業務服務的請求地址的核心過濾器,不需要配置,系統初始化時加載,並作用在每個路由上。
Spring Cloud Gateway框架內置的GlobalFilter如下:
上圖中每一個GlobalFilter都作用在每一個router上,能夠滿足大多數的需求。但是如果遇到業務上的定制,可能需要編寫滿足自己需求的GlobalFilter。在下面的案例中將講述如何編寫自己GlobalFilter,該GlobalFilter會校驗請求中是否包含瞭請求參數“token”,如何不包含請求參數“token”則不轉發路由,否則執行正常的邏輯。代碼如下:
public class TokenFilter implements GlobalFilter, Ordered { Logger logger=LoggerFactory.getLogger( TokenFilter.class ); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getQueryParams().getFirst("token"); if (token == null || token.isEmpty()) { logger.info( "token is empty..." ); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return -100; } }
在上面的TokenFilter需要實現GlobalFilter和Ordered接口,這和實現GatewayFilter很類似。然後根據ServerWebExchange獲取ServerHttpRequest,然後根據ServerHttpRequest中是否含有參數token,如果沒有則完成請求,終止轉發,否則執行正常的邏輯。
然後需要將TokenFilter在工程的啟動類中註入到Spring Ioc容器中,代碼如下:
@Bean public TokenFilter tokenFilter(){ return new TokenFilter(); }
啟動工程,使用curl命令請求:
curl localhost:8081/customer/123
可以看到請沒有被轉發,請求被終止,並在控制臺打印瞭如下日志:
2018-11-16 15:30:13.543 INFO 19372 — [ctor-http-nio-2] gateway.TokenFilter
上面的日志顯示瞭請求進入瞭沒有傳“token”的邏輯。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- 網關Gateway過濾器的使用詳解
- gateway網關接口請求的校驗方式
- Gateway網關自定義攔截器的不可重復讀取數據問題
- spring cloud gateway集成hystrix實戰篇
- gateway基本配置教程