SpringCloud升級之路2020.0.x版-41. SpringCloudGateway 基本流程講解(1)

乾貨滿滿張雜湊發表於2021-11-24

本系列程式碼地址:https://github.com/JoJoTec/spring-cloud-parent

接下來,將進入我們升級之路的又一大模組,即閘道器模組。閘道器模組我們廢棄了已經進入維護狀態的 zuul,選用了 Spring Cloud Gateway 作為內部閘道器。為何選擇 Spring Cloud Gateway 而不是 nginx 還有 Kong 的原因是:

  1. 專案組對於 Java 更加熟悉,並且對於 Project Reactor 非同步程式設計也比較熟悉,這個比較重要
  2. 需要在閘道器中使用我們之前實現的基於請求的有狀態重試的壓力敏感的負載均衡器
  3. 需要在閘道器中實現重試
  4. 需要在閘道器中實現例項路徑斷路
  5. 需要在閘道器中進行業務統一加解密
  6. 需要在閘道器中實現 BFF(Backends For Frontends)介面,即根據客戶端請求,將某幾個不同介面的請求一次性組合返回
  7. 需要在閘道器中使用 Redis 記錄一些與 Token 相關的值

因此,我們使用了 Spring Cloud Gateway 作為內部閘道器,接下來,我們就來依次實現上面說的這些功能。同時在本次升級使用過程中, Spring Cloud Gateway 也有一些坑,例如:

  1. 結合使用 spring-cloud-sleuth 會有鏈路資訊追蹤,但是某些情況鏈路資訊會丟失
  2. 對於三方 Reactor 封裝的非同步 API (例如前面提到的操作 Redis 使用的 spring-data-redis)理解不到位導致關鍵執行緒被佔用

但是首先,我們需要簡單理解下 Spring Cloud Gateway 究竟包括哪些元件以及整個呼叫流程是什麼樣子的。由於 Spring Cloud Gateway 基於 Spring-Boot 和 Spring-Webflux 實現,所以我們會從外層 WebFilter 開始說明,然後分析如何走到 Spring Cloud Gateway 的封裝邏輯,以及 Spring Cloud Gateway 包含的元件,請求是如何轉發出去,回來後又經過了哪些處理,這些我們都會逐一分析。

建立一個簡單的 API 閘道器

為了詳細分析流程,我們先來建立一個簡單的閘道器,用於快速上手並分析。

首先建立依賴:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<parent>
		<artifactId>spring-cloud-parent</artifactId>
		<groupId>com.github.jojotech</groupId>
		<version>2020.0.3-SNAPSHOT</version>
	</parent>
	<modelVersion>4.0.0</modelVersion>

	<artifactId>spring-cloud-api-gateway</artifactId>

	<dependencies>
		<dependency>
			<groupId>com.github.jojotech</groupId>
			<artifactId>spring-cloud-webflux</artifactId>
			<version>${project.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-gateway</artifactId>
		</dependency>
	</dependencies>
</project>

parent 指向了我們專案的 spring-cloud-parent,同時加入了上一節實現的 spring-cloud-webflux 依賴,同時還需要加入 spring-cloud-starter-gateway,由於在我們的 spring-cloud-parent 已經指定了 spring-cloud-parent 的版本依賴管理,所以這裡不需要指定 spring-cloud-starter-gateway 的版本

然後,我們開始編寫配置檔案:

application.yml

server:
  ##埠為 8181
  port: 8181
spring:
  application:
    # 微服務名稱是 apiGateway
    name: apiGateway
  cloud:
    gateway:
      httpclient:
        # 閘道器轉發到其他微服務的 HTTP 連線超時為 500ms
        connect-timeout: 500
        # 閘道器轉發到其他微服務的 HTTP 響應超時為 500ms
        response-timeout: 60000
      routes:
        # 編寫轉發規則
        - id: first_route
          # 轉發到微服務 test-service
          uri: lb://test-service
          # 包含哪些路徑
          predicates:
            - Path=/test-ss/**
          #  轉發到的微服務訪問路徑,去掉路徑中的第一塊,即去掉 /test-ss
          filters:
            - StripPrefix=1
    loadbalancer:
      # 指定 zone,因為我們之前在負載均衡中加入了只有同一個 zone 的例項才能互相訪問的邏輯
      zone: test
      ribbon:
        # 關閉ribbon
        enabled: false
      cache:
        # 本地微服務例項列表快取時間
        ttl: 5
        # 快取大小,你的微服務呼叫多少個其他微服務,大小就設定為多少,預設256
        capacity: 256
    discovery:
      client:
        simple:
          # 使用 spring-common 中的簡單 DiscoveryClient 服務發現客戶端,就是將微服務例項寫死在配置檔案中
          instances:
            # 指定微服務 test-service 的例項列表
            test-service:
              - host: httpbin.org
                port: 80
                metadata:
                  # 指定該例項的 zone,因為我們之前在負載均衡中加入了只有同一個 zone 的例項才能互相訪問的邏輯 
                  zone: test
eureka:
  client:
    # 關掉 eureka
    enabled: false

最後編寫啟動入口類:

package com.github.jojotech.spring.cloud.apigateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.github.jojotech.spring.cloud.apigateway")
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

啟動,訪問路徑: http://127.0.0.1:8181/test-ss/anything,可以看到請求被髮送到 httpbin.org 的 anything 路徑中,這個介面會返回請求中的所有資訊。

這樣,我們就實現了一個簡單的閘道器。接下來我們來詳細分析其工作流程和原始碼。

非同步環境下請求處理的核心 - Spring Boot + Spring WebFlux 的 WebHandler

我們建立的簡易閘道器,外層的服務容器其實就是基於 Netty 和 Project Reactor 的容器,我們跳過這些,直接進入 Spring Boot 相關的處理邏輯。我們只需要知道,請求和其對應的響應,會被外層的容器封裝成為 ServerHttpRequest requestServerHttpResponse response(都在 org.springframework.http.server.reactive 這個包下)。

然後,會交由 WebHandler 進行處理。WebHandler 的實現,其實是一種責任鏈裝飾模式,如下圖所示。每一層的 WebHandler 會將 requestresponse 進行對應自己責任的裝飾,然後交給內層的 WebHandler 處理。

image

HttpWebHandlerAdapter - 將請求封裝成 ServerWebExchange

WebHandler 的介面定義是:

public interface WebHandler {
	Mono<Void> handle(ServerWebExchange exchange);
}

但是最外層傳進來的引數是 requestresponse,需要將他們封裝成 ServerWebExchange,這個工作就是在 HttpWebHandlerAdapter 中做的。HttpWebHandlerAdapter 其實主要任務就是將各種引數封裝成 ServerWebExchange(除了和本次請求相關的 requestresponse,還有會話管理器 SessionManager,編碼解碼器配置,國際化配置還有 ApplicationContext 用於擴充套件)。

除了這些,處理 Forwarded 還有 X-Forwarded* 相關的 Header 的配置邏輯,也在這裡進行。然後將封裝好的 ServerWebExchange 交給內層的 WebHandlerExceptionHandlingWebHandler 繼續處理。同時,從原始碼中可以看出,交給內層處理的 Mono 還加入了異常處理和記錄響應資訊的邏輯:

HttpWebHandlerAdapter.java

//交給內層處理封裝好的 `ServerWebExchange`
return getDelegate().handle(exchange)
        //記錄響應日誌,trace 級別,一般用不上
		.doOnSuccess(aVoid -> logResponse(exchange))
		//處理內層沒有處理的異常,一般不會走到這裡
		.onErrorResume(ex -> handleUnresolvedError(exchange, ex))
		//在所有處理完成後,將 response 設為 complete
		.then(Mono.defer(response::setComplete));

剩下的內層的 WebHandler,我們將在下一節中繼續分析

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

相關文章