1. 程式人生 > >微服務架構spring cloud

微服務架構spring cloud

上一篇文章中,我們瞭解了 Spring Cloud Gateway 作為閘道器所具備的基礎功能:路由。本篇我們將關注它的另一個功能:過濾器。

Spring Cloud Gateway 已經內建了很多實用的過濾器,但並不能完全滿足我們的需求。本文我們就來實現自定義過濾器。雖然現在 Spring Cloud Gateway 的文件還不完善,但是我們依舊可以照貓畫虎來定製自己的過濾器。

Filter 的作用

其實前邊在介紹 Zuul 的的時候已經介紹過 Zuul 的 Filter 的作用了,同作為閘道器服務,Spring Cloud Gateway 的 Filter 作用也類似。

這裡就簡單用兩張圖來解釋一下吧。

當使用微服務構建整個 API 服務時,一般有許多不同的應用在執行,如上圖所示的mst-user-servicemst-good-servicemst-order-service,這些服務都需要對客戶端的請求的進行 Authentication。最簡單粗暴的方法就是像上圖一樣,為每個微服務應用都實現一套用於校驗的過濾器或攔截器。

對於這樣的問題,更好的做法是通過前置的閘道器服務來完成這些非業務性質的校驗,就像下圖

Filter 的生命週期

Spring Cloud Gateway 的 Filter 的生命週期不像 Zuul 的那麼豐富,它只有兩個:“pre” 和 “post”。

image-20180508184542206

“pre”和 “post” 分別會在請求被執行前呼叫和被執行後呼叫,和 Zuul Filter 或 Spring Interceptor 中相關生命週期類似,但在形式上有些不一樣。

Zuul 的 Filter 是通過filterType()方法來指定,一個 Filter 只能對應一種型別,要麼是 “pre” 要麼是“post”。Spring Interceptor 是通過重寫HandlerInterceptor中的三個方法來實現的。而 Spring Cloud Gateway 基於 Project Reactor 和 WebFlux,採用響應式程式設計風格,開啟它的 Filter 的介面GatewayFilter

你會發現它只有一個方法filter

僅通過這一個方法,怎麼來區分是 “pre” 還是 “post” 呢?我們下邊就通過自定義過濾器來看看。

自定義過濾器

現在假設我們要統計某個服務的響應時間,我們可以在程式碼中

複製

1
2
3
4
long beginTime = System.currentTimeMillis();
// do something...
long elapsed = System.currentTimeMillis() - beginTime;
log.info("elapsed: {}ms", elapsed);

每次都要這麼寫是不是很煩?Spring 告訴我們有個東西叫 AOP。但是我們是微服務啊,在每個服務裡都寫也很煩。這時候就該閘道器的過濾器登臺表演了。

自定義過濾器需要實現GatewayFilterOrdered。其中GatewayFilter中的這個方法就是用來實現你的自定義的邏輯的

複製

1
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

Ordered中的int getOrder()方法是來給過濾器設定優先級別的,值越大則優先順序越低。

好了,讓我們來擼程式碼吧

複製

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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";

    @Override
    public 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");
                    }
                })
        );
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

我們在請求剛剛到達時,往ServerWebExchange中放入了一個屬性elapsedTimeBegin,屬性值為當時的毫秒級時間戳。然後在請求執行結束後,又從中取出我們之前放進去的那個時間戳,與當前時間的差值即為該請求的耗時。因為這是與業務無關的日誌所以將Ordered設為Integer.MAX_VALUE以降低優先順序。

現在再來看我們之前的問題:怎麼來區分是 “pre” 還是 “post” 呢?其實就是chain.filter(exchange)之前的就是 “pre” 部分,之後的也就是then裡邊的是 “post” 部分。

建立好 Filter 之後我們將它新增到我們的 Filter Chain 裡邊

複製

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
    // @formatter:off
    return 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
}

複製

1
2018-05-08 16:07:04.197  INFO 83726 --- [ctor-http-nio-4] o.s.cloud.gateway.filter.GatewayFilter   : /hello/windmt: 40ms

自定義全域性過濾器

前邊講了自定義的過濾器,那個過濾器只是區域性的,如果我們有多個路由就需要一個一個來配置,並不能通過像下面這樣來實現全域性有效(也未在 Fluent Java API 中找到能設定 defaultFilters 的方法)

複製

1
2
3
4
@Bean
public ElapsedFilter elapsedFilter(){
    return new ElapsedFilter();
}

這在我們要全域性統一處理某些業務的時候就顯得比較麻煩,比如像最開始我們說的要做身份校驗,有沒有簡單的方法呢?這時候就該全域性過濾器出場了。

有了前邊的基礎,我們建立全域性過濾器就簡單多了。只需要把實現的介面GatewayFilter換成GlobalFilter,就完事大吉了。比如下面的 Demo 就是從請求引數中獲取token欄位,如果能獲取到就 pass,獲取不到就直接返回401錯誤,雖然簡單,但足以說明問題了。

複製

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 {

    @Override
    public 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);
    }

    @Override
    public int getOrder() {
        return -100;
    }
}

然後在 Spring Config 中配置這個 Bean

複製

1
2
3
4
@Bean
public TokenFilter tokenFilter(){
	return new TokenFilter();
}

重啟應用就能看到效果了

複製

1
2
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/[email protected], gatewayFilters=[OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory$$Lambda$337/[email protected], order=1}, OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory$$Lambda$339/[email protected], 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{[email protected]}, order=-100}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=or[email protected]70e889e9}, order=-1}, OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory$$Lambda$337/[email protected], order=1}, OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory$$Lambda$339/[email protected], order=2}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=o[email protected]51351f28}, order=10000}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=or[email protected]724c5cbe}, order=10100}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.s[email protected]418c020b}, order=2147483637}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=[email protected]15f2eda3}, order=2147483646}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{deleg[email protected]70101687}, order=2147483647}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegat[email protected]21618fa7}, order=2147483647}]

官方說,未來的版本將對這個介面作出一些調整:
This interface and usage are subject to change in future milestones.
from Spring Cloud Gateway - Global Filters

自定義過濾器工廠

如果你還對上一篇關於路由的文章有印象,你應該還得我們在配置中有這麼一段

複製

1
2
3
filters:
  - StripPrefix=1
  - AddResponseHeader=X-Response-Default-Foo, Default-Bar

StripPrefixAddResponseHeader這兩個實際上是兩個過濾器工廠(GatewayFilterFactory),用這種配置的方式更靈活方便。

我們就將之前的那個ElapsedFilter改造一下,讓它能接收一個boolean型別的引數,來決定是否將請求引數也打印出來。

複製

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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";

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(KEY);
    }

    public ElapsedGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public 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,我們可以直接繼承它的兩個抽象類來簡化開發AbstractGatewayFilterFactoryAbstractNameValueGatewayFilterFactory,這兩個抽象類的區別就是前者接收一個引數(像StripPrefix和我們建立的這種),後者接收兩個引數(像AddResponseHeader)。

GatewayFilter apply(Config config)方法內部實際上是建立了一個GatewayFilter的匿名類,具體實現和之前的幾乎一樣,就不解釋了。

靜態內部類Config就是為了接收那個boolean型別的引數服務的,裡邊的變數名可以隨意寫,但是要重寫List<String> shortcutFieldOrder()這個方法。

這裡注意一下,一定要呼叫一下父類的構造器把Config型別傳過去,否則會報ClassCastException

複製

1
2
3
public ElapsedGatewayFilterFactory() {
	super(Config.class);
}

工廠類我們有了,再把它註冊到 Spring 當中

複製

1
2
3
4
@Bean
public ElapsedGatewayFilterFactory elapsedGatewayFilterFactory() {
    return new ElapsedGatewayFilterFactory();
}

然後新增配置(主要改動在第 8 行)

複製

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      default-filters:
        - Elapsed=true
      routes:
        - id: service_customer
          uri: lb://CONSUMER
          order: 0
          predicates:
            - Path=/customer/**
          filters:
            - StripPrefix=1
            - AddResponseHeader=X-Response-Default-Foo, Default-Bar

複製

1
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]}

總結

本文主要介紹了 Spring Cloud Gateway 的過濾器,我們實現了自定義區域性過濾器、自定義全域性過濾器和自定義過濾器工廠,相信大家對 Spring Cloud Gateway 的過濾器有了一定的瞭解。之後我們將繼續在過濾器的基礎上研究 如何使用 Spring Cloud Gateway 實現限流和 fallback。

參考