javascript
Spring Cloud Gateway(过滤器)
在上一篇文章中,我們了解了 Spring Cloud Gateway 作為網(wǎng)關(guān)所具備的基礎(chǔ)功能:路由。本篇我們將關(guān)注它的另一個(gè)功能:過濾器。
Spring Cloud Gateway 已經(jīng)內(nèi)置了很多實(shí)用的過濾器,但并不能完全滿足我們的需求。本文我們就來實(shí)現(xiàn)自定義過濾器。雖然現(xiàn)在 Spring Cloud Gateway 的文檔還不完善,但是我們依舊可以照貓畫虎來定制自己的過濾器。
Filter 的作用
其實(shí)前邊在介紹 Zuul 的的時(shí)候已經(jīng)介紹過 Zuul 的 Filter 的作用了,同作為網(wǎng)關(guān)服務(wù),Spring Cloud Gateway 的 Filter 作用也類似。
這里就簡單用兩張圖來解釋一下吧。
當(dāng)使用微服務(wù)構(gòu)建整個(gè) API 服務(wù)時(shí),一般有許多不同的應(yīng)用在運(yùn)行,如上圖所示的mst-user-service、mst-good-service和mst-order-service,這些服務(wù)都需要對客戶端的請求的進(jìn)行 Authentication。最簡單粗暴的方法就是像上圖一樣,為每個(gè)微服務(wù)應(yīng)用都實(shí)現(xiàn)一套用于校驗(yàn)的過濾器或攔截器。
對于這樣的問題,更好的做法是通過前置的網(wǎng)關(guān)服務(wù)來完成這些非業(yè)務(wù)性質(zhì)的校驗(yàn),就像下圖
Filter 的生命周期
Spring Cloud Gateway 的 Filter 的生命周期不像 Zuul 的那么豐富,它只有兩個(gè):“pre” 和 “post”。
“pre”和 “post” 分別會在請求被執(zhí)行前調(diào)用和被執(zhí)行后調(diào)用,和 Zuul Filter 或 Spring Interceptor 中相關(guān)生命周期類似,但在形式上有些不一樣。
Zuul 的 Filter 是通過filterType()方法來指定,一個(gè) Filter 只能對應(yīng)一種類型,要么是 “pre” 要么是“post”。Spring Interceptor 是通過重寫HandlerInterceptor中的三個(gè)方法來實(shí)現(xiàn)的。而 Spring Cloud Gateway 基于 Project Reactor 和 WebFlux,采用響應(yīng)式編程風(fēng)格,打開它的 Filter 的接口GatewayFilter你會發(fā)現(xiàn)它只有一個(gè)方法filter。
僅通過這一個(gè)方法,怎么來區(qū)分是 “pre” 還是 “post” 呢?我們下邊就通過自定義過濾器來看看。
自定義過濾器
現(xiàn)在假設(shè)我們要統(tǒng)計(jì)某個(gè)服務(wù)的響應(yīng)時(shí)間,我們可以在代碼中
long beginTime = System.currentTimeMillis(); // do something... long elapsed = System.currentTimeMillis() - beginTime; log.info("elapsed: {}ms", elapsed);
自定義過濾器需要實(shí)現(xiàn)GatewayFilter和Ordered。其中GatewayFilter中的這個(gè)方法就是用來實(shí)現(xiàn)你的自定義的邏輯的每次都要這么寫是不是很煩?Spring 告訴我們有個(gè)東西叫 AOP。但是我們是微服務(wù)啊,在每個(gè)服務(wù)里都寫也很煩。這時(shí)候就該網(wǎng)關(guān)的過濾器登臺表演了。
好了,讓我們來擼代碼吧而Ordered中的int getOrder()方法是來給過濾器設(shè)定優(yōu)先級別的,值越大則優(yōu)先級越低。
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.core.Ordered; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;public class ElapsedFilter implements GatewayFilter, Ordered {private static final Log log = LogFactory.getLog(GatewayFilter.class);private static final String ELAPSED_TIME_BEGIN = "elapsedTimeBegin";@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {exchange.getAttributes().put(ELAPSED_TIME_BEGIN, System.currentTimeMillis());return chain.filter(exchange).then(Mono.fromRunnable(() -> {Long startTime = exchange.getAttribute(ELAPSED_TIME_BEGIN);if (startTime != null) {log.info(exchange.getRequest().getURI().getRawPath() + ": " + (System.currentTimeMillis() - startTime) + "ms");}}));}@Overridepublic int getOrder() {return Ordered.LOWEST_PRECEDENCE;} }我們在請求剛剛到達(dá)時(shí),往ServerWebExchange中放入了一個(gè)屬性elapsedTimeBegin,屬性值為當(dāng)時(shí)的毫秒級時(shí)間戳。然后在請求執(zhí)行結(jié)束后,又從中取出我們之前放進(jìn)去的那個(gè)時(shí)間戳,與當(dāng)前時(shí)間的差值即為該請求的耗時(shí)。因?yàn)檫@是與業(yè)務(wù)無關(guān)的日志所以將Ordered設(shè)為Integer.MAX_VALUE以降低優(yōu)先級。
現(xiàn)在再來看我們之前的問題:怎么來區(qū)分是 “pre” 還是 “post” 呢?其實(shí)就是chain.filter(exchange)之前的就是 “pre” 部分,之后的也就是then里邊的是 “post” 部分。
創(chuàng)建好 Filter 之后我們將它添加到我們的 Filter Chain 里邊
@Bean public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {// @formatter:offreturn builder.routes().route(r -> r.path("/fluent/customer/**").filters(f -> f.stripPrefix(2).filter(new ElapsedFilter()).addResponseHeader("X-Response-Default-Foo", "Default-Bar")).uri("lb://CONSUMER").order(0).id("fluent_customer_service")).build();// @formatter:on }現(xiàn)在再嘗試訪問?http://localhost:10000/customer/hello/windmt?即可在控制臺里看到請求路徑與對應(yīng)的耗時(shí)
2018-05-08 16:07:04.197 INFO 83726 --- [ctor-http-nio-4] o.s.cloud.gateway.filter.GatewayFilter : /hello/windmt: 40ms實(shí)際在使用 Spring Cloud 的過程中,我們會使用 Sleuth+Zipkin 來進(jìn)行耗時(shí)分析。
自定義全局過濾器
前邊講了自定義的過濾器,那個(gè)過濾器只是局部的,如果我們有多個(gè)路由就需要一個(gè)一個(gè)來配置,并不能通過像下面這樣來實(shí)現(xiàn)全局有效(也未在 Fluent Java API 中找到能設(shè)置 defaultFilters 的方法)
@Bean public ElapsedFilter elapsedFilter(){return new ElapsedFilter(); }這在我們要全局統(tǒng)一處理某些業(yè)務(wù)的時(shí)候就顯得比較麻煩,比如像最開始我們說的要做身份校驗(yàn),有沒有簡單的方法呢?這時(shí)候就該全局過濾器出場了。
有了前邊的基礎(chǔ),我們創(chuàng)建全局過濾器就簡單多了。只需要把實(shí)現(xiàn)的接口GatewayFilter換成GlobalFilter,就完事大吉了。比如下面的 Demo 就是從請求參數(shù)中獲取token字段,如果能獲取到就 pass,獲取不到就直接返回401錯(cuò)誤,雖然簡單,但足以說明問題了。
import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;public class TokenFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String token = exchange.getRequest().getQueryParams().getFirst("token");if (token == null || token.isEmpty()) {exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);return exchange.getResponse().setComplete();}return chain.filter(exchange);}@Overridepublic int getOrder() {return -100;} }然后在 Spring Config 中配置這個(gè) Bean
@Bean public TokenFilter tokenFilter(){return new TokenFilter(); }重啟應(yīng)用就能看到效果了
2018-05-08 20:41:06.528 DEBUG 87751 --- [ctor-http-nio-2] o.s.c.g.h.RoutePredicateHandlerMapping : Mapping [Exchange: GET http://localhost:10000/customer/hello/windmt?token=1000] to Route{id='service_customer', uri=lb://CONSUMER, order=0, predicate=org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$334/1871259950@2aa090be, gatewayFilters=[OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory$$Lambda$337/577037372@22e84be7, order=1}, OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory$$Lambda$339/1061806694@1715f608, order=2}]} 2018-05-08 20:41:06.530 DEBUG 87751 --- [ctor-http-nio-2] o.s.c.g.handler.FilteringWebHandler : Sorted gatewayFilterFactories: [OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=com.windmt.filter.TokenFilter@309028af}, order=-100}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@70e889e9}, order=-1}, OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory$$Lambda$337/577037372@22e84be7, order=1}, OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory$$Lambda$339/1061806694@1715f608, order=2}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@51351f28}, order=10000}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@724c5cbe}, order=10100}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@418c020b}, order=2147483637}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@15f2eda3}, order=2147483646}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@70101687}, order=2147483647}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@21618fa7}, order=2147483647}]官方說,未來的版本將對這個(gè)接口作出一些調(diào)整:
This interface and usage are subject to change in future milestones.
from?Spring Cloud Gateway - Global Filters
自定義過濾器工廠
如果你還對上一篇關(guān)于路由的文章有印象,你應(yīng)該還得我們在配置中有這么一段
filters:- StripPrefix=1- AddResponseHeader=X-Response-Default-Foo, Default-BarStripPrefix、AddResponseHeader這兩個(gè)實(shí)際上是兩個(gè)過濾器工廠(GatewayFilterFactory),用這種配置的方式更靈活方便。
我們就將之前的那個(gè)ElapsedFilter改造一下,讓它能接收一個(gè)boolean類型的參數(shù),來決定是否將請求參數(shù)也打印出來。
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import reactor.core.publisher.Mono; import java.util.Arrays; import java.util.List;public class ElapsedGatewayFilterFactory extends AbstractGatewayFilterFactory<ElapsedGatewayFilterFactory.Config> {private static final Log log = LogFactory.getLog(GatewayFilter.class);private static final String ELAPSED_TIME_BEGIN = "elapsedTimeBegin";private static final String KEY = "withParams";@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList(KEY);}public ElapsedGatewayFilterFactory() {super(Config.class);}@Overridepublic GatewayFilter apply(Config config) {return (exchange, chain) -> {exchange.getAttributes().put(ELAPSED_TIME_BEGIN, System.currentTimeMillis());return chain.filter(exchange).then(Mono.fromRunnable(() -> {Long startTime = exchange.getAttribute(ELAPSED_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;}} }過濾器工廠的頂級接口是GatewayFilterFactory,我們可以直接繼承它的兩個(gè)抽象類來簡化開發(fā)AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,這兩個(gè)抽象類的區(qū)別就是前者接收一個(gè)參數(shù)(像StripPrefix和我們創(chuàng)建的這種),后者接收兩個(gè)參數(shù)(像AddResponseHeader)。
GatewayFilter apply(Config config)方法內(nèi)部實(shí)際上是創(chuàng)建了一個(gè)GatewayFilter的匿名類,具體實(shí)現(xiàn)和之前的幾乎一樣,就不解釋了。
靜態(tài)內(nèi)部類Config就是為了接收那個(gè)boolean類型的參數(shù)服務(wù)的,里邊的變量名可以隨意寫,但是要重寫List<String> shortcutFieldOrder()這個(gè)方法。
這里注意一下,一定要調(diào)用一下父類的構(gòu)造器把Config類型傳過去,否則會報(bào)ClassCastException
public ElapsedGatewayFilterFactory() {super(Config.class); }工廠類我們有了,再把它注冊到 Spring 當(dāng)中
@Bean public ElapsedGatewayFilterFactory elapsedGatewayFilterFactory() {return new ElapsedGatewayFilterFactory(); }然后添加配置(主要改動在第 8 行)
spring:cloud:gateway:discovery:locator:enabled: truedefault-filters:- Elapsed=trueroutes:- id: service_customeruri: lb://CONSUMERorder: 0predicates:- Path=/customer/**filters:- StripPrefix=1- AddResponseHeader=X-Response-Default-Foo, Default-Bar然后我們再次訪問?http://localhost:10000/customer/hello/windmt?token=1000?即可在控制臺看到以下內(nèi)容
2018-05-08 16:53:02.030 INFO 84423 --- [ctor-http-nio-1] o.s.cloud.gateway.filter.GatewayFilter : /hello/windmt: 656ms params:{token=[1000]}總結(jié)
本文主要介紹了 Spring Cloud Gateway 的過濾器,我們實(shí)現(xiàn)了自定義局部過濾器、自定義全局過濾器和自定義過濾器工廠,相信大家對 Spring Cloud Gateway 的過濾器有了一定的了解。之后我們將繼續(xù)在過濾器的基礎(chǔ)上研究 如何使用 Spring Cloud Gateway 實(shí)現(xiàn)限流和 fallback。
示例代碼可以從 Github 獲取:https://github.com/zhaoyibo/spring-cloud-study
參考
spring-cloud/spring-cloud-gateway
Spring Cloud Gateway
- 本文作者:?Yibo
- 本文鏈接:?https://windmt.com/2018/05/08/spring-cloud-14-spring-cloud-gateway-filter/
- 版權(quán)聲明:?本博客所有文章除特別聲明外,均采用?CC BY-NC-SA 4.0?許可協(xié)議。轉(zhuǎn)載請注明出處!
總結(jié)
以上是生活随笔為你收集整理的Spring Cloud Gateway(过滤器)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Cloud Gateway
- 下一篇: Spring Cloud Gateway