第二代微服務網關組件 - Spring Cloud Gateway

[TOC]


初識Spring Cloud Gateway

簡介:

Spring Cloud Gateway是Spring Cloud體系的第二代網關組件,基於Spring 5.0的新特性WebFlux進行開發,底層網絡通信框架使用的是Netty,所以其吞吐量高、性能強勁,未來將會取代第一代的網關組件Zuul。Spring Cloud Gateway可以通過服務發現組件自動轉發請求,默認集成了Ribbon做負載均衡,以及默認使用Hystrix對網關進行保護,當然也可以選擇其他的容錯組件,例如Sentinel

優點:

  • 性能強勁:是第一代網關Zuul的1.6倍
  • 功能強大:內置了很多實用的功能,例如轉發、監控、限流等
  • 設計優雅,容易擴展

缺點:

  • 其實現依賴Netty與WebFlux,不是傳統的Servlet編程模型,有一定的學習成本
  • 不能在Servlet容器下工作,也不能構建成WAR包,即不能將其部署在Tomcat、Jetty等Servlet容器裏,只能打成jar包執行
  • 不支持Spring Boot 1.x,需2.0及更高的版本

如果對網關概念或Zuul不瞭解的話,可以參考另一篇文章:

核心概念:

1、Route(路由):

Spring Cloud Gateway的基礎元素,可簡單理解成一條轉發規則。包含:ID、目標URL、Predicate集合以及Filter集合

這是一段比較典型的Gateway路由配置:

spring:
  cloud:
    gateway:
      routes:
      - id: user-center  # 唯一標識,通常使用服務id
        uri: lb://user-center  # 目標URL,lb代表從註冊中心獲取服務,lb是Load Balance的縮寫
        predicates:
        # Predicate集合
        - Path=/zj/cloud/v1/user-center/**  # 匹配轉發路徑
        filters:
        # Filter集合
        - StripPrefix=4  # 從第幾級開始轉發

2、Predicate(謂詞):

java.util.function.Predicate這個接口,Gateway使用Predicate實現路由的匹配條件

3、Filter(過濾器):

與我們平時使用的Servlet編程模型裏的過濾器概念類似,同樣可以用於修改請求以及響應數據,可以利用Filter實現鑑權、訪問日誌記錄,接口耗時記錄等功能

Spring Cloud Gateway架構圖:
第二代微服務網關組件 - Spring Cloud Gateway

簡單解讀一下這個圖:

Gateway Client發送請求給Spring Cloud Gateway,Gateway Handler Mapping會判斷請求的路徑是否匹配路由的配置,如果匹配則會進入Gateway Web Handler,Web Handler會讀取路由上所配置的過濾器,然後將該請求交給過濾器去處理,最後轉發到路由配置的微服務上

  • Gateway Client:泛指外部請求,例如瀏覽器、app、小程序等
  • Proxied Service:指的是被網關代理的微服務

相關源碼:

  • Gateway Handler Mapping:org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping
  • Gateway Web Handler:org.springframework.cloud.gateway.handler.FilteringWebHandler

由於Webflux大量運用函數式編程思想,所以本文中的示例代碼都會使用lambda表達式及函數式API來簡化。若對此不瞭解的話,可以參考相關文章,篇幅有限這裏就不進行介紹了:


創建Spring Cloud Gateway項目

這裏使用IDEA的Spring Initializr進行項目的創建,到選擇依賴這一步勾選gateway依賴,如下圖:
第二代微服務網關組件 - Spring Cloud Gateway

網關組件一般都配合服務發現組件使用,我這裏使用Nacos作爲服務發現組件,具體的依賴如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!-- Nacos Client -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- actuator -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <!--整合Spring Cloud-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.SR2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--整合Spring Cloud Alibaba-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.1.0.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

如果對Nacos不熟悉的話可以參考另一篇關於Nacos的文章,或者採用Eureka也是一樣的:

然後編寫配置文件內容如下:

server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        # 指定nacos server的地址
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          # 讓gateway通過服務發現組件找到其他的微服務,從而自動轉發請求
          enabled: true

# actuator相關配置
management:
  endpoints:
    web:
      exposure:
        # 暴露所有監控端點
        include: '*'
  endpoint:
    health:
      # 總是顯示健康檢測詳情
      show-details: always

完成以上步驟後,我們來啓動這個網關服務,進行一個簡單的測試,看看是否能將請求正常地轉發到指定的微服務上。此時有一個名爲user-center的微服務,該微服務有一個按id獲取用戶信息的接口,接口路徑爲/users/{id}。若通過網關服務來訪問這個接口,要如何做呢?很簡單,gateway配合服務發現組件使用時,會有一個默認的轉發規則,如下:

  • ${GATEWAY_URL}/{微服務名稱}/{接口路徑}

所以按該規則得出來的具體url爲:localhost:8040/user-center/users/{id},訪問結果如下:
第二代微服務網關組件 - Spring Cloud Gateway

從測試結果可以看到,gateway可以根據url上的微服務名稱將訪問請求轉發到該微服務上。

以上這種是Gateway最簡單的使用方式,但通常在實際開發中,可能不希望使用默認的轉發規則,因爲這種方式不太靈活,例如一些服務接口是存在版本劃分的,需要根據不同版本的訪問路徑轉發到不同版本的微服務上。此時就需要自定義轉發路由,實際上在第一小節的時候就已經給出過配置示例了。修改配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: user-center  # 唯一標識,通常使用服務id
        uri: lb://user-center  # 目標URL,lb代表從註冊中心獲取服務
        predicates:
        # Predicate集合
        - Path=/zj/cloud/v1/user-center/**  # 匹配轉發路徑
        filters:
        # Filter集合
        - StripPrefix=4  # 從第幾級開始轉發,數字從0開始

自定義路由的注意事項:

  • predicates配置項必須有,且必須配置一個及以上的Predicate,但不一定非要配置Path,可以配置其他的Predicate,例如AfterBefore等,此時Path的默認值爲/**

重啓項目,此時訪問的url爲:localhost:8040/zj/cloud/v1/user-center/users/{id},訪問結果如下:
第二代微服務網關組件 - Spring Cloud Gateway


路由配置的兩種形式

Spring Cloud Gateway的路由配置有兩種形式,分別是路由到指定的URL以及路由到指定的微服務,在上一小節的示例中我們就已經使用過路由到微服務的這種配置形式了。在這兩種形式中,均支持訪問路徑的通配及精確匹配,在之前的示例中我們只使用了通配。所以本小節將給出具體的配置示例,以此直觀的瞭解這兩種形式及不同匹配方式在配置上的區別。

1、路由到指定的URL

通配,使用通配符/**進行匹配,示例:

spring:
  cloud:
    gateway:
      routes:
        - id: test_route  # 路由的唯一標識
          uri: http://www.xxx.com
          predicates:
            # 使用通配符匹配
            - Path=/**
  • 該配置使訪問 GATEWAY_URL/** 時會轉發到 http://www.xxx.com/**

精確匹配,配置具體的接口路徑即可,示例:

spring:
  cloud:
    gateway:
      routes:
        - id: test_route  # 路由的唯一標識
          uri: http://www.xxx.com/user/order/detail
          predicates:
            # 指定具體的路徑進行匹配
            - Path=/user/order/detail
  • 該配置使訪問 GATEWAY_URL/user/order/detail 時會轉發到 http://www.xxx.com/user/order/detail

2、路由到指定的微服務

通配,示例:

spring:
  cloud:
    gateway:
      routes:
        - id: user-center  # 路由的唯一標識,這種形式下通常是微服務名稱
          uri: lb://user-center  # lb代表從註冊中心獲取服務
          predicates:
            # 使用通配符匹配
            - Path=/**
  • 該配置使訪問 GATEWAY_URL/** 時會轉發到 user-center微服務的/**

精確匹配,示例:

spring:
  cloud:
    gateway:
      routes:
        - id: user-center  # 路由的唯一標識,這種形式下通常是微服務名稱
          uri: lb://user-center/users/info  # lb代表從註冊中心獲取服務
          predicates:
            # 指定具體的路徑進行匹配
            - Path=/users/info
  • 該配置使訪問 GATEWAY_URL/users/info 時會轉發到 user-center微服務的/users/info

路由謂詞工廠

前面提到過謂詞是路由的判斷條件,而路由謂詞工廠就是作用到指定路由上的一堆謂詞判斷條件。在之前的示例裏,我們就已經使用過路由謂詞工廠了,就是自定義轉發路徑時所配置的Path。

內置的路由謂詞工廠

Spring Cloud Gateway內置了衆多路由謂詞工廠,這些路由謂詞工廠爲路由匹配的判斷提供了有力的支持,而我們之前所使用的Path就是內置的路由謂詞工廠之一,用於判斷當前訪問的接口路徑是否與該路由所配置的路徑相匹配,若匹配則進行轉發。由於Gateway內置的路由謂詞工廠比較多,篇幅有限就不在本文中介紹了,可以參考另一篇文章:

自定義路由謂詞工廠

現在我們已經知道Spring Cloud Gateway內置了一系列的路由謂詞工廠,但如果這些內置的路由謂詞工廠不能滿足業務需求的話,我們可以自定義路由謂詞工廠來實現特定的需求。例如有某個服務限制用戶只允許在09:00 - 17:00這個時間段內纔可以訪問,內置的路由謂詞工廠是無法滿足這個需求的,所以此時我們就需要自定義能夠實現該需求的路由謂詞工廠。

首先定義一個配置類,用於承載時間段的配置參數:

@Data
public class TimeBetweenConfig {
    /**
     * 開始時間
     */
    private LocalTime start;

    /**
     * 結束時間
     */
    private LocalTime end;
}

然後定義一個路由謂詞工廠,具體代碼如下:

package com.zj.node.gateway.predicate;

import com.zj.node.gateway.config.TimeBetweenConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import java.time.LocalTime;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

/**
 * 路由謂詞工廠必須以RoutePredicateFactory結尾,
 * 這是Spring Cloud Gateway的約定
 *
 * @author 01
 * @date 2019-08-14
 **/
@Slf4j
@Component
public class TimeBetweenRoutePredicateFactory extends AbstractRoutePredicateFactory<TimeBetweenConfig> {

    public TimeBetweenRoutePredicateFactory() {
        super(TimeBetweenConfig.class);
    }

    /**
     * 實現謂詞判斷的方法
     */
    @Override
    public Predicate<ServerWebExchange> apply(TimeBetweenConfig config) {
        return exchange -> {
            LocalTime start = config.getStart();
            LocalTime end = config.getEnd();

            // 判斷當前時間是否爲允許訪問的時間段內
            LocalTime now = LocalTime.now();
            return now.isAfter(start) && now.isBefore(end);
        };
    }

    /**
     * 控制配置類(TimeBetweenConfig)屬性和配置文件中配置項(TimeBetween)的映射關係
     */
    @Override
    public List<String> shortcutFieldOrder() {
        /*
         * 例如我們的配置項是:TimeBetween=上午9:00, 下午5:00
         * 那麼按照順序,start對應的是上午9:00;end對應的是下午5:00
         **/
        return Arrays.asList("start", "end");
    }
}

最後需要在配置文件中啓用該路由謂詞工廠,並且需要禁止gateway通過服務發現組件轉發請求到其他的微服務,修改Gateway相關配置如下:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          # 禁止gateway通過服務發現組件轉發請求到其他的微服務
          enabled: false
      routes:
        - id: user-center
          # 目標URL,lb代表從註冊中心獲取服務
          uri: lb://user-center
          predicates:
            # 注意名稱必須爲路由謂詞工廠類名的前綴,參數爲允許訪問的時間段
            - TimeBetween=上午9:00,下午5:00

可以看到這裏主要是配置了我們自定義的路由謂詞工廠類名的前綴以及允許訪問的時間段,這個時間格式不是隨便配置的,而是Spring Cloud Gateway的默認時間格式,相關源碼如下:

  • org.springframework.format.support.DefaultFormattingConversionService#addDefaultFormatters

時間格式是可以註冊的,關於時間格式註冊的相關源碼如下:

  • org.springframework.format.datetime.standard.DateTimeFormatterRegistrar#registerFormatters

另外,這裏之所以要禁止gateway通過服務發現組件轉發請求到其他的微服務,是因爲開啓該配置項的話會導致我們自定義的路由謂詞工廠不生效。不生效也是有原因的,開啓該配置項會令Gateway優先將請求按照該配置項進行轉發,那麼我們自定義的路由就不會生效。

到此爲止我們就實現了一個自定義路由謂詞工廠,若此時不在允許的訪問時間段內,訪問就會報404,如下:
第二代微服務網關組件 - Spring Cloud Gateway


過濾器工廠

前面提到了過濾器可以爲請求和響應添加一些業務邏輯或者修改請求和響應對象等,適當地使用過濾器可以讓我們的工作事半功倍,而本小節將要介紹的過濾器工廠就是用來創建過濾器的。在此之前我們已經學習過路由謂詞工廠了,而過濾器工廠與路由謂詞工廠在使用上是類似的,只不過實現的功能不一樣。

內置的過濾器工廠

同樣的Spring Cloud Gateway內置了非常多的過濾器工廠,有二十多個。通過這些內置的過濾器工廠就已經可以靈活且方便地處理請求和響應數據,由於Gateway內置的過濾器工廠實在太多,而篇幅有限就不在本文中介紹了,可以參考另一篇文章:

自定義過濾器工廠

若Spring Cloud Gateway內置的過濾器工廠無法滿足我們的業務需求,那麼此時就需要自定義自己的過濾器工廠以實現特定功能。所謂過濾器工廠實際上就是用於創建過濾器實例的,而創建的過濾器實例都實現於GatewayFilter接口。

過濾器的生命週期:

  • Gateway以轉發請求爲邊界,所以其生命週期只包含pre和post:
    • pre:Gateway轉發請求之前
    • post:Gateway轉發請求之後

自定義過濾器工廠的方式:

  1. 繼承AbstractGatewayFilterFactory,參考源碼:org.springframework.cloud.gateway.filter.factory.RequestSizeGatewayFilterFactory。使用該方式實現的過濾器工廠的配置形式如下:

    spring:
    cloud:
    gateway:
      routes:
        filters:
        # 過濾器工廠的名稱
        - name: RequestSize
          # 該過濾器工廠的參數
          args:
            maxSize: 500000
  2. 繼承AbstractNameValueGatewayFilterFactory,參考源碼:org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory。使用該方式實現的過濾器工廠的配置形式如下:
    spring:
    cloud:
    gateway:
      routes:
        filters:
        # 過濾器工廠的名稱及參數以name-value的形式配置
        - AddRequestHeader=S-Header, Bar

注:AbstractNameValueGatewayFilterFactory繼承了AbstractGatewayFilterFactory,所以實際上第二種方式是第一種方式的簡化

核心API:

  • exchange.getRequest().mutate().xxx:修改request
  • exchange.mutate().xxx:修改exchange
  • chain.filter(exchange):傳遞給下一個過濾器處理
  • exchange.getResponse():獲取響應對象

注:這裏的exchange實際類型爲ServerWebExchangechain實際類型爲GatewayFilter

最後我們來實際動手編寫一個自定義過濾器工廠,需求是記錄訪問日誌,這裏爲了簡單起見採用第二種方式實現,具體代碼如下:

package com.zj.node.gateway.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

/**
 * 過濾器工廠必須以GatewayFilterFactory結尾,
 * 這是Spring Cloud Gateway的約定
 *
 * @author 01
 * @date 2019-08-15
 **/
@Slf4j
@Component
public class PreLogGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {

    @Override
    public GatewayFilter apply(NameValueConfig config) {
        // 使用lambda表達式來創建GatewayFilter的實例,實際就是匿名內部類的簡寫
        return (exchange, chain) -> {
            // 通過config獲取配置的參數
            log.info("配置參數:{}, {}", config.getName(), config.getValue());

            // 修改request,可以添加一些header什麼的
            ServerHttpRequest modifiedRequest = exchange.getRequest()
                    .mutate()
                    .header("X-GatewayHeader","A","B")
                    .build();

            // 打印訪問的接口地址
            String path = modifiedRequest.getURI().getPath();
            log.info("訪問的接口爲:{}", path);

            // 修改exchange
            ServerWebExchange modifiedExchange = exchange.mutate()
                    .request(modifiedRequest).build();

            // 傳遞給下一個過濾器處理
            return chain.filter(modifiedExchange);
        };
    }
}

最後需要添加相關配置以啓用這個過濾器工廠,如下:

spring:
  cloud:
    gateway:
      routes:
        - id: user-center
          uri: lb://user-center
          predicates:
            - TimeBetween=上午9:00,下午5:00
          filters:
            # 名稱必須爲過濾器工廠類名的前綴,並且參數只能有兩個,因爲NameValueConfig裏只定義了兩個屬性
            - PreLog=testName,testValue

啓動項目,訪問user-center的接口,此時控制檯輸出的日誌如下:
第二代微服務網關組件 - Spring Cloud Gateway


全局過濾器

現在我們已經知道前面所介紹的過濾器工廠實際用於創建GatewayFilter實例,並且這些GatewayFilter實例僅作用於指定的路由上,那麼有沒有可以作用於全部路由上的過濾器呢?答案是有的,這就是本小節將要介紹的全局過濾器。Spring Cloud Gateway默認就內置了許多全局過濾器,本文僅介紹如何自定義全局過濾器,關於Gateway內置的過濾器可以參考另一篇文章:

自定義全局過濾需要實現GlobalFilter 接口,該接口和 GatewayFilter 有一樣的方法定義,只不過 GlobalFilter 的實例會作用於所有的路由。

Tips:

官方聲明:GlobalFilter的接口定義以及用法在未來的版本可能會發生變化。

個人判斷:GlobalFilter可用於生產;如果有自定義GlobalFilter的需求,理論上也可放心使用。因爲未來即使接口定義以及使用方式發生變化,理應也是平滑過渡的(比如Zuul的Fallback,原先叫ZuulFallbackProvider,後來改叫FallbackProvider,中間就有段時間新舊使用方式都支持,後面才逐步廢棄老的使用方式)。

接下來我們自定義一個全局過濾器,需求是打印訪問的接口路徑以及打印該接口的訪問耗時。具體代碼如下:

package com.zj.node.gateway.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 自定義全局過濾器
 *
 * @author 01
 * @date 2019-08-17
 **/
@Slf4j
public class MyGlobalFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();
        log.info("[MyGlobalFilter] 訪問的接口:{}", path);

        long start = System.currentTimeMillis();
        return chain.filter(exchange)
                // then的內容會在過濾器返回的時候執行,即最後執行
                .then(Mono.fromRunnable(() ->
                        log.info("[ {} ] 接口的訪問耗時:{} /ms", 
                        path, System.currentTimeMillis() - start))
                );
    }
}

最後需要使該全局過濾器生效,方法有很多種,可以直接在該類上加@Component註解,也可以通過代碼配置(@Bean),還有其他的一些方式。這裏個人比較傾向於使用一個專門的配置類去實例化這些全局過濾器並交給Spring容器管理。代碼如下:

package com.zj.node.gateway.config;

import com.zj.node.gateway.filter.MyGlobalFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

@Slf4j
@Configuration
public class FilterConfig {

    @Bean
    // 該註解用於指定過濾器的執行順序,數字越小越優先執行
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter myGlobalFilter(){
        log.info("create myGlobalFilter...");
        return new MyGlobalFilter();
    }
}

啓動項目,看看我們自定義的全局過濾器是否已生效,訪問Gateway控制檯輸出如下:
第二代微服務網關組件 - Spring Cloud Gateway

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章