知識庫 / Spring / Spring Cloud RSS 訂閱

Spring Cloud Gateway 流量重寫

Spring Cloud
HongKong
5
12:18 PM · Dec 06 ,2025

1. 簡介

Spring Cloud Gateway 的一個常見應用場景是作為一種面向服務的前置式代理(facade),為客户端提供更簡單的服務消費方式。

在本教程中,我們將展示如何通過在發送請求到後端之前重寫 URL 來自定義暴露的 API。

2. Spring Cloud Gateway 快速回顧

Spring Cloud Gateway 項目基於流行的 Spring Boot 2 和 Project Reactor 構建,因此繼承了它們的主要特性:

  • 低資源消耗,得益於其反應式特性
  • 支持 Spring Cloud 生態系統中的所有功能(如服務發現、配置等)
  • 易於使用標準 Spring 模式進行擴展和/或自定義

我們之前在之前的文章中已經涵蓋了它的主要功能,所以這裏我們只列出主要概念:

  • Route:一個匹配的傳入請求在網關中經過的處理步驟集
  • Predicate:一個 Java 8 Predicate,用於評估一個 ServerWebExchange
  • FiltersGatewayFilter 實例,可以檢查和/或修改一個 ServerWebExchange。網關支持全局過濾器和按路由過濾器。

簡而言之,傳入請求的處理序列如下:

  • 網關使用與每個路由關聯的 Predicates 來找到將處理該請求的路由
  • 一旦找到路由,請求(一個 ServerWebExchange 實例)將經過所有配置的過濾器,直到最終將其發送到後端
  • 當後端發送響應,或者發生錯誤(例如超時或連接重置),過濾器再次有機會處理響應,然後再將其發送回客户端。

3. 基於配置的URL重寫

回到本文的主題,讓我們看看如何定義一個重寫傳入URL的路由,在將它發送到後端之前。例如,假設對於傳入請求的格式為 /api/v1/customer/,後端URL應為 http://v1.customers/api/。這裏,我們使用“*”來表示“此處之後的所有內容”。

要創建基於配置的重寫,只需將幾個屬性添加到應用程序的配置中即可。這裏,我們使用基於YAML的配置以提高清晰度,但這些信息可以來自任何受支持的 PropertySource

spring:
  cloud:
    gateway:
      routes:
      - id: rewrite_v1
        uri: ${rewrite.backend.uri:http://example.com}
        predicates:
        - Path=/v1/customer/**
        filters:
        - RewritePath=/v1/customer/(?<segment>.*),/api/$\{segment}

讓我們分析一下這個配置。首先,我們有路由的 ID,即它的標識符。接下來,我們有通過 uri 屬性提供的後端 URI。 僅考慮主機名/端口,因為最終路徑來自重寫邏輯

predicates 屬性定義了為了激活此路由必須滿足的條件。 在我們的案例中,我們使用 Path 謂詞,它使用類似螞蟻的路徑表達式與傳入請求的路徑進行匹配。

最後,filters 屬性包含實際的重寫邏輯。 RewritePath 過濾器接受兩個參數:一個正則表達式和一個替換字符串。 過濾器的實現方法是簡單地在請求的 URI 上執行 replaceAll() 方法,並使用提供的參數作為參數。

Spring 處理配置文件的方式是,我們不能使用標準的 ${group} 替換表達式,因為 Spring 會將其視為屬性引用並嘗試替換其值。為了避免這種情況,我們需要在“$”和“{”字符之間添加反斜槓,該反斜槓將在過濾器實現中使用之前被刪除。

4. 基於DSL的URL重寫

雖然<em>RewritePath</em>功能強大且易於使用,但在某些規則具有動態方面的情況下,它無法滿足需求。根據具體情況,仍然可以使用謂詞作為每個分支的規則條件,編寫多個規則。

但是,如果情況並非如此,我們可以使用基於DSL的方法來創建路由。 我們的目標是創建一個<em>RouteLocator</em> Bean,該 Bean 實現路由的邏輯。例如,我們可以創建一個簡單的路由,就像之前一樣,使用正則表達式重寫傳入的URI。但這次,替換字符串將在每次請求時動態生成:

@Configuration
public class DynamicRewriteRoute {
    
    @Value("${rewrite.backend.uri}")
    private String backendUri;
    private static Random rnd = new Random();
    
    @Bean
    public RouteLocator dynamicZipCodeRoute(RouteLocatorBuilder builder) {
        return builder.routes()
          .route("dynamicRewrite", r ->
             r.path("/v2/zip/**")
              .filters(f -> f.filter((exchange, chain) -> {
                  ServerHttpRequest req = exchange.getRequest();
                  addOriginalRequestUrl(exchange, req.getURI());
                  String path = req.getURI().getRawPath();
                  String newPath = path.replaceAll(
                    "/v2/zip/(?<zipcode>.*)", 
                    "/api/zip/${zipcode}-" + String.format("%03d", rnd.nextInt(1000)));
                  ServerHttpRequest request = req.mutate().path(newPath).build();
                  exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, request.getURI());
                  return chain.filter(exchange.mutate().request(request).build());
              }))
              .uri(backendUri))
          .build();
    }
}

這裏,動態部分只是隨機數追加到替換字符串中。在實際應用中,邏輯可能會更復雜,但基本機制與示例所示相同。

以下是這段代碼經過的幾個關鍵步驟的説明:首先,它調用了 addOriginalRequestUrl() 方法,該方法來自 ServerWebExchangeUtils 類,將原始 URL 存儲在 exchange 的屬性 GATEWAY_ORIGINAL_REQUEST_URL_ATTR 下。 此屬性的值是一個列表,我們將接收到的 URL 在進行任何修改之前附加到其中,並且由網關在 X-Forwarded-For 標頭處理中內部使用。

其次,在應用重寫邏輯後,必須將修改後的 URL 存儲在 exchange 的屬性 GATEWAY_REQUEST_URL_ATTR 中。 雖然此步驟未在文檔中明確説明,但它確保了我們的自定義過濾器能夠與其它可用的過濾器和諧共存。

5. 測試

為了測試我們的重寫規則,我們將使用標準的 JUnit 5 類,並進行一些小修改:我們將啓動一個簡單的服務器,基於 Java SDK 的 com.sun.net.httpserver.HttpServer 類。服務器將在一個隨機端口上啓動,從而避免端口衝突。

然而,這種方法的缺點是,我們需要確定服務器實際分配的端口並將其傳遞給 Spring,以便我們可以使用它來設置路由的 uri 屬性。 幸運的是,Spring 提供瞭解決此問題的優雅方案:@DynamicPropertySource。在這裏,我們將使用它來啓動服務器並註冊一個綁定端口值的屬性:

@DynamicPropertySource
static void registerBackendServer(DynamicPropertyRegistry registry) {
    registry.add("rewrite.backend.uri", () -> {
        HttpServer s = startTestServer();
        return "http://localhost:" + s.getAddress().getPort();
    });
}

測試處理程序只是將收到的 URI 反映迴響應體中。這允許我們驗證重寫規則是否按預期工作。例如,這是...

@Test
void testWhenApiCall_thenRewriteSuccess(@Autowired WebTestClient webClient) {
    webClient.get()
      .uri("http://localhost:" + localPort + "/v1/customer/customer1")
      .exchange()
      .expectBody()
      .consumeWith((result) -> {
          String body = new String(result.getResponseBody());
          assertEquals("/api/customer1", body);
      });
}

6. 結論

在本快速教程中,我們展示了使用 Spring Cloud Gateway 庫重寫 URL 的不同方法。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.