Spring Cloud Gateway編碼實現任意地址跳轉

2023-06-27 09:00:37

歡迎存取我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套原始碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 作為《Spring Cloud Gateway實戰》系列的第十四篇,本文會繼續發掘Spring Cloud Gateway的潛力,通過編碼體驗操控閘道器的樂趣,開發出一個實用的功能:讓Spring Cloud Gateway應用在收到請求後,可以按照業務的需要跳轉到任意的地址去

一般路由規則

  • 先來看一個普通的路由規則,如下所示,意思是將所有/hello/**的請求轉發到http://127.0.0.1:8082這個地址去:
spring:
  application:
    name: hello-gateway
  cloud:
    gateway:
      routes:
        - id: path_route
          uri: http://127.0.0.1:8082
          predicates:
          - Path=/hello/**
  • 上述規則的功能如下圖所示,假設這就是生產環境的樣子,192.168.50.99:8082是提供服務的後臺應用:

特殊規則

  • 以上是常規情況,但也有些特殊情況,要求SpringCloud Gateway把瀏覽器的請求轉發到不同的服務上去
  • 如下圖所示,在之前的環境中增加了另一個服務(即藍色塊),假設藍色服務代表測試環境
  • 瀏覽器發起的/hello/str請求中,如果header中帶有tag-test-user,並且值等於true,此時要求SpringCloud Gateway把這個請求轉發到測試環境
  • 如果瀏覽器的請求header中沒有tag-test-user,SpringCloud Gateway需要像以前那樣繼續轉發到192.168.50.99:8082
  • 很明顯,上述需求難以通過設定來實現,因為轉發的地址和轉發邏輯都是圍繞業務邏輯來客製化的,這也就是本篇的目標:對同一個請求path,可以通過編碼轉發到不同的地方去
  • 實現上述功能的具體做法是:自定義過濾器

設計

  • 編碼之前先設計,把關鍵點想清楚再動手
  • 今天咱們要開發一個SpringCloud Gateway應用,裡面新增一個自定義過濾器
  • 實現這個功能需要三個知識點作為基礎,也就是說,您會通過本篇實戰掌握以下知識點:
  1. 自定義過濾器
  2. 自定義過濾器的設定引數和bean的對映
  3. 編碼構造Route範例
  • 用思維導圖將具體工作內容展開,如下圖所示,咱們就按部就班的實現吧:

原始碼下載

名稱 連結 備註
專案主頁 https://github.com/zq2599/blog_demos 該專案在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該專案原始碼的倉庫地址,https協定
git倉庫地址(ssh) [email protected]:zq2599/blog_demos.git 該專案原始碼的倉庫地址,ssh協定
  • 這個git專案中有多個資料夾,本篇的原始碼在spring-cloud-tutorials資料夾下,如下圖紅框所示:
    - spring-cloud-tutorials內部有多個子專案,本篇的原始碼在gateway-dynamic-route資料夾下,如下圖紅框所示:

編碼

  • 新建名為gateway-dynamic-route的maven工程,其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-tutorials</artifactId>
        <groupId>com.bolingcavalry</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>gateway-dynamic-route</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.bolingcavalry</groupId>
            <artifactId>common</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!-- 如果父工程不是springboot,就要用以下方式使用外掛,才能生成正常的jar -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.bolingcavalry.gateway.GatewayDynamicRouteApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
  • 啟動類是普通的SpringBoot啟動類:
package com.bolingcavalry.gateway;

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

@SpringBootApplication
public class GatewayDynamicRouteApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayDynamicRouteApplication.class,args);
    }
}
  • 接下來是本篇的核心,自定義過濾器類,程式碼中已經新增了詳細的註釋,有幾處要注意的地方稍後會提到:
package com.bolingcavalry.gateway.filter;

import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;

@Component
@Slf4j
public class BizLogicRouteGatewayFilterFactory extends AbstractGatewayFilterFactory<BizLogicRouteGatewayFilterFactory.BizLogicRouteConfig> {

    private static final String TAG_TEST_USER = "tag-test-user";

    public BizLogicRouteGatewayFilterFactory() {
        super(BizLogicRouteConfig.class);
    }

    @Override
    public GatewayFilter apply(BizLogicRouteConfig config) {

        return (exchange, chain) -> {
            // 本次的請求物件
            ServerHttpRequest request =  exchange.getRequest();

            // 呼叫方請求時的path
            String rawPath = request.getURI().getRawPath();

            log.info("rawPath [{}]", rawPath);

            // 請求頭
            HttpHeaders headers = request.getHeaders();

            // 請求方法
            HttpMethod httpMethod = request.getMethod();

            // 請求引數
            MultiValueMap<String, String> queryParams = request.getQueryParams();

            // 這就是客製化的業務邏輯,isTestUser等於ture代表當前請求來自測試使用者,需要被轉發到測試環境
            boolean isTestUser = false;

            // 如果header中有tag-test-user這個key,並且值等於true(不區分大小寫),
            // 就認為當前請求是測試使用者發來的
            if (headers.containsKey(TAG_TEST_USER)) {
                String tageTestUser = headers.get(TAG_TEST_USER).get(0);

                if ("true".equalsIgnoreCase(tageTestUser)) {
                    isTestUser = true;
                }
            }

            URI uri;

            if (isTestUser) {
                log.info("這是測試使用者的請求");
                // 從組態檔中得到測試環境的uri
                uri = UriComponentsBuilder.fromHttpUrl(config.getTestEnvUri() + rawPath).queryParams(queryParams).build().toUri();
            } else {
                log.info("這是普通使用者的請求");
                // 從組態檔中得到正式環境的uri
                uri = UriComponentsBuilder.fromHttpUrl(config.getProdEnvUri() + rawPath).queryParams(queryParams).build().toUri();
            }

            // 生成新的Request物件,該物件放棄了常規路由設定中的spring.cloud.gateway.routes.uri欄位
            ServerHttpRequest serverHttpRequest = request.mutate().uri(uri).method(httpMethod).headers(httpHeaders -> httpHeaders = httpHeaders).build();

            // 取出當前的route物件
            Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
            //從新設定Route地址
            Route newRoute =
                    Route.async().asyncPredicate(route.getPredicate()).filters(route.getFilters()).id(route.getId())
                            .order(route.getOrder()).uri(uri).build();
            // 放回exchange中
            exchange.getAttributes().put(GATEWAY_ROUTE_ATTR,newRoute);

            // 鏈式處理,交給下一個過濾器
            return chain.filter(exchange.mutate().request(serverHttpRequest).build());
        };
    }

    /**
     * 這是過濾器的設定類,設定資訊會儲存在此處
     */
    @Data
    @ToString
    public static class BizLogicRouteConfig {
        // 生產環境的服務地址
        private String prodEnvUri;

        // 測試環境的服務地址
        private String testEnvUri;
    }
}
  • 上述程式碼中要注意的地方如下:
  1. BizLogicRouteConfig是過濾器的設定類,可以在使用過濾器時在組態檔中設定prodEnvUri和testEnvUri的值,在程式碼中可以通過這兩個欄位取得設定值
  2. 過濾器的工廠類名為BizLogicRouteGatewayFilterFactory,按照規則,過濾器的名字是BizLogicRoute
  3. 在apply方法中,重新建立ServerHttpRequest和Route物件,它們的引數可以按照業務需求隨意設定,然後再將這兩個物件設定給SpringCloud gateway的處理鏈中,接下來,處理鏈上的其他過濾拿到的就是新的ServerHttpRequest和Route物件了

設定

  • 假設生產環境地址是http://127.0.0.1:8082,測試環境地址是http://127.0.0.1:8087,整個SpringCloud Gateway應用的組態檔如下,可見使用了剛剛建立的過濾器,並且為此過濾器設定了兩個引數:
server:
  #伺服器埠
  port: 8086
spring:
  application:
    name: gateway-dynamic-route
  cloud:
    gateway:
      routes:
        - id: path_route
          uri: http://0.0.0.0:8082
          predicates:
          - Path=/hello/**
          filters:
            - name: BizLogicRoute
              args:
                prodEnvUri: http://127.0.0.1:8082
                testEnvUri: http://127.0.0.1:8087
  • 至此,編碼完成了,啟動這個服務

開發和啟動後臺服務,模擬生產和測試環境

  • 接下來開始驗證功能是否生效,咱們要準備兩個後臺服務:
  1. 模擬生產環境的後臺服務是provider-hello,監聽埠是8082,其/hello/str介面的返回值是Hello World, 2021-12-12 10:53:09
  2. 模擬測試環境的後臺服務是provider-for-test-user,監聽埠是8087,其/hello/str介面的返回值是Hello World, 2021-12-12 10:57:11 (from test enviroment)(和生產環境相比,返回內容多了個(from test enviroment)),對應Controller參考如下:
package com.bolingcavalry.provider.controller;

import com.bolingcavalry.common.Constants;
import org.springframework.web.bind.annotation.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;

@RestController
@RequestMapping("/hello")
public class Hello {

    private String dateStr(){
        return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
    }

    /**
     * 返回字串型別
     * @return
     */
    @GetMapping("/str")
    public String helloStr() {
        return Constants.HELLO_PREFIX + ", " + dateStr() + " (from test enviroment)";
    }
}
  • 以上兩個服務,對應的程式碼都在我的Github倉庫中,如下圖紅框所示:
  • 啟動gateway-dynamic-routeprovider-helloprovider-for-test-user服務
  • 此時,SpringCloud gateway應用和兩個後臺服務都啟動完成,情況如下圖,接下來驗證剛才開發的過濾器能不能像預期那樣轉發:

驗證

  • 用postman工具向gateway-dynamic-route應用發起一次請求,返回值如下圖紅框所示,證明這是provider-hello的響應,看來咱們的請求已經正常到達:
  • 再傳送一次請求,如下圖,這次在header中加入鍵值對,得到的結果是provider-for-test-user的響應
  • 至此,過濾器的開發和驗證已經完成,通過編碼,可以把外部請求轉發到任何咱們需要的地址去,並且支援引數設定,這個過濾器還有一定的可設定下,減少了寫死的比率,如果您正在琢磨如何深度操控SpringCloud Gateway,希望本文能給您一些參考;

歡迎關注部落格園:程式設計師欣宸

學習路上,你不孤單,欣宸原創一路相伴...