1. 程式人生 > 實用技巧 >第五章: 閘道器元件Gateway

第五章: 閘道器元件Gateway

官網文件: https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/

1. 概述

1.1 什麼是閘道器

微服務架構裡面還有一個非常重要的元件,就是閘道器,

在Spring Cloud 全家桶裡面也有這個角色, 在 1.x 版本中 採用的是 Zuul 閘道器,

但是因為 zuul的升級一直跳票,一直放鴿子, Spring Cloud 在2.x 中研發了一個自己的閘道器 替代了 Zuul, 那就是 Gateway

閘道器常見的功能有路由轉發、許可權校驗、限流控制等作用。

例如在微服務架構中, 如果可以使用閘道器對 請求進行轉發, 前端只需訪問一個地址,並攜帶需要呼叫的目標地址,由閘道器進行統一管理. 並且在請求過程中 對請求進行過濾,鑑權,使 微服務的 服務地址不直接暴露,保護了 微服務節點的安全

微服務架構中閘道器的位置:

1.2 Gateway 閘道器的基本特性

  1. Gateway 是在Spring 生態體系之上構建的 API 閘道器服務,由於底層使用 netty, 所以是基於 Spring5, Spring Boot2 和 Project Reactor等技術,Gateway旨在 提供一種簡單而有效的方式來對 API 進行路由, 以及提供一些強大的過濾器功能,例如 熔斷,限流,重試等
  2. Gateway作為 Spring Cloud 生態體系中的閘道器,目標是替代 Zuul, 在Spring Cloud 2.0 以上的版本中,沒有對新版本的Zuul 2.0 以上最新高效能版本進行整合, 仍然使用老的 非 Reactor 模式的 ,
  3. 為了提高效能, Gateway 是基於 WebFlux框架實現的, 而WebFlux 框架底層使用了高效能的 Reactor 模式的通訊框架 Netty
  4. Gateway 的目標提供統一的路由方式且基於 Filter 鏈的方式提供了閘道器基本的功能,例如 : 安全, 監控/指標, 和限流

原始碼架構:

什麼是WebFlux

這裡做基本介紹,詳細請自行官網學習

  1. 傳統的 Web框架, 比如說 spring mvc 等 都是 Servlet API 與 Servlet 容器基礎上執行的
  2. 但是, 在 Servlet 3.1 之後, 有了非同步非阻塞的支援, 而WebFlux 是一個典型的非阻塞非同步的框架,它的核心是基於Reactor模式的相關API 實現的,相對於傳統的web 框架來說, 它可以 執行在諸如Netty,Undertow 等支援Servlet3.1 的容器上, 非阻塞+ 函數語言程式設計(Spring5 必須使用java8)
  3. Spring WebFlux 是 Spring 5.0 引入的新的響應式框架, 區別於Springmvc, 它不需要依賴 Servlet API ,他是完全非同步非阻塞的,並且基於Reactor模式來實現響應式流規範

基本核心元件

Gateway 三個元件

  • 路由: 閘道器的基本構建模組,它是由ID、目標URl、斷言集合和過濾器集合定義, 如果集合斷言為真,則匹配路由。
  • Predicate(斷言):這是java 8的一個函式式介面predicate,可以用於lambda表 達式和方法引用,輸入型別是:Spring Framework ServerWebExchange,允許 開發人員匹配來自HTTP請求的任何內容,例如請求頭headers和引數paramers ,如果請求與斷言相匹配,則進行對應的路由
  • Filter(過濾器):這些是使用特定工廠構建的Spring Framework GatewayFilter 例項,這裡可以在傳送請求之前或之後修改請求和響應

2. 基本使用

2.1 工程搭建及測試

需要搭建 Gateway 閘道器的微服務, 並註冊到註冊中心.

pom依賴:

    <dependencies>
        <!--   gateway     -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--        &lt;!&ndash;   web Gateway不需要web依賴     &ndash;&gt;-->
        <!--        <dependency>-->
        <!--            <groupId>org.springframework.boot</groupId>-->
        <!--            <artifactId>spring-boot-starter-web</artifactId>-->
        <!--        </dependency>-->
        <!--        <dependency>-->
        <!--            <groupId>org.springframework.boot</groupId>-->
        <!--            <artifactId>spring-boot-starter-actuator</artifactId>-->
        <!--        </dependency>-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
       
    </dependencies>

yml配置:

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      routes:
        - id: payment_routh #路由的ID,沒有固定規則但要求唯一,建議配合服務名
          uri: http://localhost:8001   #匹配後提供服務的路由地址,即路由跳轉地址
          predicates:
            - Path=/payment/get/**   #路徑型別的斷言,路徑相匹配的則匹配路由

        - id: payment_routh2
          uri: http://localhost:8001
          predicates:
            - Path=/payment/lb/**   #斷言,路徑相匹配的進行路由


eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka
 

主啟動類:

@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
    public static void main(String[] args) {
            SpringApplication.run( GateWayMain9527.class,args);
        }
}

上面的程式碼中, 將微服務啟動, 並註冊到7001 微服務,並在配置檔案中, 對 路徑/payment/get/**/payment/lb/** 進行攔截,這就是斷言,若斷言為true,則匹配該路由,並跳轉到對應的uri屬性下的 地址中

測試結果:

  • 直接訪問 8001 微服務的 介面 http://127.0.0.1:8001/payment/lb

    返回結果 : "8001" 此介面返回 8001 微服務的埠

  • 訪問 9527 Gateway 微服務的 地址: http://127.0.0.1:9527/payment/lb

    斷言成功, 跳轉路由, 返回結果: "8001", 成功呼叫

若訪問的路徑,沒有任何路由匹配,則報錯404:

2.2 編碼方式配置路由

上面使用 yml 配置檔案的方式進行 配置路由規則, 也可以使用編碼的方式進行配置

下面我們使用編碼的方式配置路由,跳轉到百度的國內新聞

@Configuration
public class GateWayConfig {
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
		//路由構建器
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        //配置路由規則,對比 yml 檔案配置
        //  id: path_route , predicates: /guonei , uri: http://news.baidu.com/guonei
        routes.route("path_route"
                , r->r.path("/guonei").uri("http://news.baidu.com/guonei"))
                .build();
        return routes.build();
    }
}

下面進行測試:

  • 直接訪問百度國內新聞 http://news.baidu.com/guonei,成功跳轉
  • 通過Gateway 微服務訪問 http://127.0.0.1:9527/guonei ,也可以跳轉

2.3 使用微服務名跳轉

上面的程式碼中,我們跳轉到某個微服務,都是 直接寫對方的ip 地址,

Gateway 會自動 從註冊中心中獲取服務列表, 可以通過微服務的名稱作為路由轉發,那麼上面的程式碼就不用寫成

http://localhost:8001 而是 lb://cloud-payment-service lb 為負載均衡,若該微服務有兩個實現,則進行負載均衡

程式碼演示:

首先必須先開啟註冊中心路由功能: spring.cloud.gateway.discovery.locator.enabled=true

pom修改:

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true  #開啟從註冊中心動態建立路由的功能,利用微服務名進行路由
      routes:
        - id: payment_routh #路由的ID,沒有固定規則但要求唯一,建議配合服務名
          #uri: http://localhost:8001   #匹配後提供服務的路由地址
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/get/**   #斷言,路徑相匹配的進行路由

        - id: payment_routh2
          #uri: http://localhost:8001   #匹配後提供服務的路由地址
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/lb/**   #斷言,路徑相匹配的進行路由


eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka
 

開啟 8001,8002 微服務,呼叫9527地址: http://127.0.0.1:9527/payment/lb,

輪流返回 "8001","8002" 對應微服務的地址,呼叫成功,並負載均衡

3. 斷言工廠

3.1 概述

Gateway閘道器中 另一個非常重要的元件: 斷言, 它決定一個請求是否由匹配此路由. 在前面的案例中使用的就是其中的Path 斷言工廠生成的 斷言類, 並匹配請求,跳轉到指定路徑,

GateWay給我們提供了很多不同型別的斷言工廠,都是抽象類AbstractRoutePredicateFactory 的子類

詳細使用,請檢視官網文件 : https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gateway-request-predicates-factories

分類:

時間相關:

  • AfterRoutePredicateFactory: 匹配在指定日期時間之後發生的請求

    示例:

    # 表示在 2017年1月20日17:42:47 之後
    #此時間格式 可以使用 ZonedDateTime 類獲取
    #ZonedDateTime.now(); // 預設時區
    #ZonedDateTime.now(ZoneId.of("America/New_York")); // 用指定時區獲取當前時間
    predicates:
            - After=2017-01-20T17:42:47.789-07:00[America/Denver]
    
  • BeforeRoutePredicateFactory 匹配在指定日期時間之前發生的請求

  • BetweenRoutePredicateFactory 匹配在datetime1之後和在datetime2之前的請求。該datetime2引數必須datetime1之後

    示例:

    #表示在2017年1月20日17:42:47之後 並且 在2017年1月21日17:42:47之前
    predicates:
            - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
    

Cookie相關:

  • CookieRoutePredicateFactory 匹配具有指定Cookie,並且值與指定的正則匹配的請求

    示例:

    # 表示Cookie 中攜帶 鍵為chocolate,值為可以匹配正則"ch.p" 的字串
    predicates:
            - Cookie=chocolate, ch.p
    

Header相關:

  • HeaderRoutePredicateFactory, 匹配 請求頭中有指定的名,並且值匹配指定的正則表示式

    示例:

    predicates:
            - Header=X-Request-Id, \d+
    
  • HostRoutePredicateFactory, 匹配Host 域名列表

    示例:

    # 匹配路徑中host 為 *.baidu.com 的 和 *.souhu.com的
    predicates:
            - Host=**.baidu.com,**.sohu.com
    
  • RemoteAddrRoutePredicateFactory ,匹配請求的Remote(客戶端i來源ip)

    示例:

    # 匹配Remote 為 192.168.1.1 至 192.168.1.254
    # 斜槓後面的24 表示最後一位的最大值 即254
    #16 表示最後兩位 即 255.254
    #8 表示最後三位 即 255.255.254
    predicates:
            - RemoteAddr=192.168.1.1/24
    

請求相關:

  • MethodRoutePredicateFactory 匹配指定的請求方式

    示例:

    #匹配 GET,POST 請求
    predicates:
            - Method=GET,POST
    
  • QueryRoutePredicateFactory 匹配請求有指定的引數key,並且值匹配指定的正則

    示例:

    # 請求鍵為 red 值匹配正則 "gree."
    predicates:
            - Query=red, gree.
    
  • PathRoutePredicateFactory 匹配url 路徑,也就是我們上面案例中用到的

3.2 斷言工廠的工作原理

下面使用MethodRoutePredicateFactory 來進行演示

原始碼

public class MethodRoutePredicateFactory extends AbstractRoutePredicateFactory<MethodRoutePredicateFactory.Config> {
    /** @deprecated */
    @Deprecated
    public static final String METHOD_KEY = "method";
    public static final String METHODS_KEY = "methods";

    public MethodRoutePredicateFactory() {
        super(MethodRoutePredicateFactory.Config.class);
    }
    /**
    *  封裝是 config 類中使用哪個欄位接受引數
    */
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("methods");
    }

    public ShortcutType shortcutType() {
        return ShortcutType.GATHER_LIST;
    }

    /**
    *  實際生產 斷言操作類的方法
    */
    public Predicate<ServerWebExchange> apply(MethodRoutePredicateFactory.Config config) {
        return new GatewayPredicate() {
            /**
  			 *  斷言操作類的test方法,判斷請求是否符合條件
   			 */
            public boolean test(ServerWebExchange exchange) {
                HttpMethod requestMethod = exchange.getRequest().getMethod();
                return Arrays.stream(config.getMethods()).anyMatch((httpMethod) -> {
                    return httpMethod == requestMethod;
                });
            }

            public String toString() {
                return String.format("Methods: %s", Arrays.toString(config.getMethods()));
            }
        };
    }

    /**
    *  配置類,接受配置檔案中的配置的資訊,
    */
    @Validated
    public static class Config {
        // 一個列舉陣列, 接受請求方式,例如 [GET,POST]
        private HttpMethod[] methods;

        public Config() {
        }

        /** @deprecated */
        @Deprecated
        public HttpMethod getMethod() {
            return this.methods != null && this.methods.length > 0 ? this.methods[0] : null;
        }

        /** @deprecated */
        @Deprecated
        public void setMethod(HttpMethod method) {
            this.methods = new HttpMethod[]{method};
        }

        public HttpMethod[] getMethods() {
            return this.methods;
        }

        public void setMethods(HttpMethod... methods) {
            this.methods = methods;
        }
    }
}

上面的程式碼中, 可以看出其實MethodRoutePredicateFactory 的實現比較簡單.生產一個GatewayPredicate進行斷言.主要做了如下兩個操作

  • 獲取配置檔案中配置的引數
  • 判斷請求的方法是否匹配其中任意一個引數

3.3 自定義斷言工廠

根據上面的規則,我們可以實現自己的自定義斷言工廠

接收引數的Config 類:

public class TulingTimeBetweenConfig {

    private LocalTime startTime;

    private LocalTime endTime;

    public LocalTime getStartTime() {
        return startTime;
    }

    public void setStartTime(LocalTime startTime) {
        this.startTime = startTime;
    }

    public LocalTime getEndTime() {
        return endTime;
    }

    public void setEndTime(LocalTime endTime) {
        this.endTime = endTime;
    }
}

斷言工廠類,注意工廠類的類名必須以"RoutePredicateFactory"開頭, "RoutePredicateFactory" 之前的一部分則作為配置檔案中的鍵

@Component
public class TulingTimeBetweenRoutePredicateFactory extends AbstractRoutePredicateFactory<TulingTimeBetweenConfig> {

    public TulingTimeBetweenRoutePredicateFactory() {
        super(TulingTimeBetweenConfig.class);
    }
    /**
     * 真正的業務判斷邏輯
     */
    @Override
    public Predicate<ServerWebExchange> apply(TulingTimeBetweenConfig config) {

        LocalTime startTime = config.getStartTime();

        LocalTime endTime = config.getEndTime();

        return new Predicate<ServerWebExchange>() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                LocalTime now = LocalTime.now();
                //判斷當前時間是否在在配置的時間範圍類
                return now.isAfter(startTime) && now.isBefore(endTime);
            }
        };

    }

    /**
     * 用於接受yml中的配置 ‐ TulingTimeBetween=上午7:00,下午11:00
     */
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("startTime", "endTime");
    }

}

yaml配置檔案,使用逗號分隔

predicates:
    - TulingTimeBetween=上午7:00,下午11:00

測試,當請求時間為上午七點到下午十一點前的所有請求,都會被攔截

4. 過濾器工廠

上面的操作中,我們僅僅只是將 請求攔截,並跳轉到某個地址,貌似沒做什麼操作,作用很小,下面介紹過濾器的使用,將在攔截過程中做一些操作

,SpringCloudGateway 也提供了很多的過濾器工廠,我們通過一些過濾器工廠可以進行一些業務邏輯處理器,比如新增剔除響應頭,新增去除引數等.

官網文件: https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gatewayfilter-factories

4.1 常用過濾器簡單介紹

下面簡單介紹幾種常用的:

新增請求頭:

給攔截到請求中加入指定的請求頭和指定的值

predicates:
  ‐ TulingTimeBetween=上午7:00,下午11:00
filters:
  ‐ AddRequestHeader=X‐Request‐Company,tuling

新增請求引數

給請求加上指定的 Parameter 引數,和指定的值

predicates:
 ‐ TulingTimeBetween=上午7:00,下午11:00
filters:
 ‐ AddRequestParameter=company,tuling

為匹配的路由統一新增字首

給請求加上指定的字首,比如下面的配置中,請求http://localhost:8888/selectProductInfoById/1會轉發到

http://localhost:8888/product‐api/selectProductInfoById/1

predicates:
 ‐ TulingTimeBetween=上午7:00,下午11:00
filters:
 ‐ PrefixPath=/product‐api

更多詳細使用者請參考官網,提供了豐富的過濾器工廠

4.1 自定義過濾器工廠

過濾器工廠的實現思路和斷言工廠類似,也可以參考著自定義自己的過濾器工廠,下面我們來實現一個記錄整個過濾鏈執行時間的過濾器工廠類

過濾器操作類:

在檢視原始碼過程中,發現其過濾器工廠返回過濾器操作類程式碼中,都是使用匿名內部類的方式,但是這樣過濾器的執行順序無法保證,只能按照載入順序執行,所以這裡我們將操作類單獨定義,實現Ordered 介面,保證載入順序優先

public class TimeMonitorGatewayFilter implements GatewayFilter, Ordered {

    private static final String COUNT_START_TIME = "countStartTime";
    private AbstractNameValueGatewayFilterFactory.NameValueConfig config;

    public TimeMonitorGatewayFilter(AbstractNameValueGatewayFilterFactory.NameValueConfig config) {
        this.config = config;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain
            chain) {
        //獲取配置檔案yml中的
        // filters:
        // ‐ TimeMonitor=enabled,true
        String name = config.getName();
        String value = config.getValue();

        if (value.equals("false")) {
            return null;
        }
        //在請求中記錄開始時間
        exchange.getAttributes().put(COUNT_START_TIME,
                System.currentTimeMillis());

        //then方法相當於aop的後置通知一樣,當整個過濾鏈執行完畢時 ,將呼叫此方法,
        return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
            @Override
            public void run() {
                //結束時間
                Long startTime = exchange.getAttribute(COUNT_START_TIME);
                //獲取開始時間 並計算差值
                if (startTime != null) {
                    StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath())
                            .append(": ")
                            .append(System.currentTimeMillis() - startTime)
                            .append("ms");
                    sb.append(" params:").append(exchange.getRequest().getQueryParams());
                    //列印執行時間
                    System.out.println(sb.toString());
                }
            }
        }));
    }

    /**
     *  數字越小 Spring 載入此類越優先
     * @return
     */
    @Override
    public int getOrder() {
        return -100;
    }
}

此類在執行鏈開始時執行,並記錄開始時間,並定義了結束過濾鏈結束時,計算差值

過濾器工廠類:

和斷言工廠一樣, 也是使用指定的字尾,來確定配置檔案中的配置方式,必須為"GatewayFilterFactory" 結尾

並繼承了 AbstractNameValueGatewayFilterFactory 類, 可以接受配置檔案中的 name,value 形式的引數

但是本例中只使用 value來定義

@Component
public class TimeMonitorGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {

    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return new TimeMonitorGatewayFilter(config) ;
    }
}

yaml配置檔案:

接受到的引數 : name為enabled ,value為true,但是上面的程式碼中 只用到了 true引數,含義為開啟此功能

predicates:
 ‐ Query=company,product
filters:
 ‐ TimeMonitor=enabled,true

測試: 呼叫本閘道器,[127.0.0.1:9527/payment/lb?name=10](http://127.0.0.1:9527/payment/lb?name=10)

列印日誌資訊: /payment/lb: 8ms params:{name=[10]}

4.3 自定義全域性過濾器

GateWay 框架中,還有一種特殊的過濾器, 為全域性過濾器,只要是被攔截的請求,都會被執行,上面的負載均衡功能就是

LoadBalancerClientFilter 全域性過濾器起的作用

其他全域性過濾器使用,請檢視官網: https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#global-filters

同樣,我們也可以自定義全域性過濾器:

@Component
public class MyLogGateWayFilter implements GlobalFilter,Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("進來");
        //獲取 url上第一個 uname param
        String uname = exchange.getRequest().getQueryParams().getFirst("uname");
        if (uname==null){
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }

        return chain.filter(exchange);
    }

    /**
     * 過濾鏈的排序
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

上面的過濾器中,進行了簡單的鑑權操作,若請求引數中沒有username,則拒絕轉發,