构建网关

本指南将引导您了解如何使用 Spring Cloud Gateway

您将构建什么

您将使用 Spring Cloud Gateway 构建一个网关。

您需要什么

  • 大约 15 分钟

  • 一个喜欢的文本编辑器或 IDE

  • Java 17+

如何完成本指南

像大多数 Spring 入门指南一样,您可以从头开始完成每个步骤,或者绕过您已经熟悉的基本设置步骤。无论哪种方式,您最终都会得到可工作的代码。

从头开始,请继续阅读 从 Spring Initializr 开始

跳过基础知识,请执行以下操作

完成后,您可以对照 gs-gateway/complete 中的代码检查您的结果。

从 Spring Initializr 开始

您可以使用这个预配置的项目,然后点击 Generate 下载 ZIP 文件。该项目已配置好以适用于本教程中的示例。

手动初始化项目

  1. 访问 https://start.spring.io。该服务会自动拉取应用程序所需的所有依赖项,并为您完成大部分设置。

  2. 选择 Gradle 或 Maven,以及您想使用的语言。本指南假设您选择了 Java。

  3. 点击 Dependencies(依赖),然后选择 Reactive GatewayResilience4JContract Stub Runner

  4. 点击 Generate(生成)。

  5. 下载生成的 ZIP 文件,这是一个根据您的选择配置好的 Web 应用程序压缩包。

如果您的 IDE 集成了 Spring Initializr,您可以在 IDE 中完成此过程。
您也可以从 Github Fork 该项目,并在您的 IDE 或其他编辑器中打开它。

创建简单路由

Spring Cloud Gateway 使用路由来处理发送到下游服务的请求。在本指南中,我们将所有请求路由到 HTTPBin。路由可以通过多种方式配置,但本指南中,我们使用 Gateway 提供的 Java API。

首先,在 Application.java 中创建一个 RouteLocator 类型的新的 Bean

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes().build();
}

myRoutes 方法接收一个 RouteLocatorBuilder,该构建器可用于创建路由。除了创建路由外,RouteLocatorBuilder 还允许您向路由添加谓词和过滤器,以便您可以根据特定条件处理路由,并根据需要修改请求/响应。

现在我们可以创建一个路由,当向 Gateway 发送对 /get 的请求时,该路由会将请求转发到 https://httpbin.org/get。在此路由的配置中,我们添加了一个过滤器,在转发请求之前向请求添加一个值为 WorldHello 请求头

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .build();
}

要测试我们的简单 Gateway,可以在端口 8080 上运行 Application.java。应用程序运行后,向 http://localhost:8080/get 发送请求。您可以在终端中使用以下 cURL 命令来完成此操作

$ curl http://localhost:8080/get

您应该会收到一个类似于以下输出的响应

{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Connection": "close",
    "Forwarded": "proto=http;host=\"localhost:8080\";for=\"0:0:0:0:0:0:0:1:56207\"",
    "Hello": "World",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.54.0",
    "X-Forwarded-Host": "localhost:8080"
  },
  "origin": "0:0:0:0:0:0:0:1, 73.68.251.70",
  "url": "http://localhost:8080/get"
}

请注意,HTTPBin 显示请求中发送了值为 WorldHello 头。

使用 Spring Cloud CircuitBreaker

现在我们可以做一些更有趣的事情。由于 Gateway 后面的服务可能会出现不良行为并影响我们的客户端,我们可能希望将创建的路由包装在断路器中。您可以通过在 Spring Cloud Gateway 中使用 Resilience4J Spring Cloud CircuitBreaker 实现来做到这一点。这是通过一个简单的过滤器实现的,您可以将其添加到您的请求中。我们可以创建另一个路由来演示这一点。

在下一个示例中,我们使用 HTTPBin 的 delay API,该 API 会等待一定秒数后再发送响应。由于此 API 可能会花费很长时间发送响应,我们可以将使用此 API 的路由包装在断路器中。以下列表向我们的 RouteLocator 对象添加了一个新路由

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .route(p -> p
            .host("*.circuitbreaker.com")
            .filters(f -> f.circuitBreaker(config -> config.setName("mycmd")))
            .uri("http://httpbin.org:80")).
        build();
}

这个新的路由配置与我们之前创建的配置之间有一些区别。首先,我们使用 host 谓词而不是 path 谓词。这意味着,只要主机是 circuitbreaker.com,我们就将请求路由到 HTTPBin 并将该请求包装在断路器中。我们通过向路由应用过滤器来实现这一点。我们可以使用配置对象来配置断路器过滤器。在此示例中,我们将断路器命名为 mycmd

现在我们可以测试这个新路由了。为此,我们需要启动应用程序,但这一次,我们将向 /delay/3 发送请求。同样重要的是,我们需要包含一个主机为 circuitbreaker.comHost 头。否则,请求将不会被路由。我们可以使用以下 cURL 命令

$ curl --dump-header - --header 'Host: www.circuitbreaker.com' http://localhost:8080/delay/3
我们使用 --dump-header 来查看响应头。--dump-header 后面的 - 告诉 cURL 将头信息打印到标准输出。

使用此命令后,您应该在终端中看到以下内容

HTTP/1.1 504 Gateway Timeout
content-length: 0

如您所见,断路器在等待 HTTPBin 的响应时超时了。当断路器超时时,我们可以选择提供一个回退(fallback)以便客户端不会收到 504 错误,而是收到更有意义的内容。在生产环境中,您可能会返回缓存中的一些数据,例如,但在我们的简单示例中,我们返回一个响应体为 fallback 的响应。

为此,我们可以修改我们的断路器过滤器,使其在超时时提供一个要调用的 URL

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .route(p -> p
            .host("*.circuitbreaker.com")
            .filters(f -> f.circuitBreaker(config -> config
                .setName("mycmd")
                .setFallbackUri("forward:/fallback")))
            .uri("http://httpbin.org:80"))
        .build();
}

现在,当被断路器包装的路由超时时,它将调用 Gateway 应用程序中的 /fallback。现在我们可以将 /fallback 端点添加到我们的应用程序中。

Application.java 中,我们添加类级别的 @RestController 注解,然后向该类添加以下 @RequestMapping

src/main/java/gateway/Application.java

@RequestMapping("/fallback")
public Mono<String> fallback() {
  return Mono.just("fallback");
}

要测试这个新的回退功能,请重新启动应用程序,然后再次发出以下 cURL 命令

$ curl --dump-header - --header 'Host: www.circuitbreaker.com' http://localhost:8080/delay/3

有了回退功能,我们现在可以看到从 Gateway 收到了一个 200 响应,响应体为 fallback

HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: text/plain;charset=UTF-8

fallback

编写测试

作为一个优秀的开发者,我们应该编写一些测试来确保我们的 Gateway 按照预期工作。在大多数情况下,我们希望限制对外部资源的依赖,尤其是在单元测试中,因此不应依赖 HTTPBin。解决这个问题的一个方法是使我们路由中的 URI 可配置,这样如果需要,我们可以更改 URI。

为此,在 Application.java 中,我们可以创建一个名为 UriConfiguration 的新类

@ConfigurationProperties
class UriConfiguration {
  
  private String httpbin = "http://httpbin.org:80";

  public String getHttpbin() {
    return httpbin;
  }

  public void setHttpbin(String httpbin) {
    this.httpbin = httpbin;
  }
}

要启用 ConfigurationProperties,我们还需要向 Application.java 添加一个类级别的注解。

@EnableConfigurationProperties(UriConfiguration.class)

有了新的配置类,我们就可以在 myRoutes 方法中使用它了

src/main/java/gateway/Application.java

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder, UriConfiguration uriConfiguration) {
  String httpUri = uriConfiguration.getHttpbin();
  return builder.routes()
    .route(p -> p
      .path("/get")
      .filters(f -> f.addRequestHeader("Hello", "World"))
      .uri(httpUri))
    .route(p -> p
      .host("*.circuitbreaker.com")
      .filters(f -> f
        .circuitBreaker(config -> config
          .setName("mycmd")
          .setFallbackUri("forward:/fallback")))
      .uri(httpUri))
    .build();
}

我们不再硬编码 HTTPBin 的 URL,而是从新的配置类中获取 URL。

以下列表显示了 Application.java 的完整内容

src/main/java/gateway/Application.java

@SpringBootApplication
@EnableConfigurationProperties(UriConfiguration.class)
@RestController
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

  @Bean
  public RouteLocator myRoutes(RouteLocatorBuilder builder, UriConfiguration uriConfiguration) {
    String httpUri = uriConfiguration.getHttpbin();
    return builder.routes()
      .route(p -> p
        .path("/get")
        .filters(f -> f.addRequestHeader("Hello", "World"))
        .uri(httpUri))
      .route(p -> p
        .host("*.circuitbreaker.com")
        .filters(f -> f
          .circuitBreaker(config -> config
            .setName("mycmd")
            .setFallbackUri("forward:/fallback")))
        .uri(httpUri))
      .build();
  }

  @RequestMapping("/fallback")
  public Mono<String> fallback() {
    return Mono.just("fallback");
  }
}

@ConfigurationProperties
class UriConfiguration {
  
  private String httpbin = "http://httpbin.org:80";

  public String getHttpbin() {
    return httpbin;
  }

  public void setHttpbin(String httpbin) {
    this.httpbin = httpbin;
  }
}

现在我们可以在 src/test/java/gateway 中创建一个名为 ApplicationTest 的新类。在新类中,我们添加以下内容

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = {"httpbin=http://localhost:${wiremock.server.port}"})
@AutoConfigureWireMock(port = 0)
public class ApplicationTest {

  @Autowired
  private WebTestClient webClient;

  @Test
  public void contextLoads() throws Exception {
    //Stubs
    stubFor(get(urlEqualTo("/get"))
        .willReturn(aResponse()
          .withBody("{\"headers\":{\"Hello\":\"World\"}}")
          .withHeader("Content-Type", "application/json")));
    stubFor(get(urlEqualTo("/delay/3"))
      .willReturn(aResponse()
        .withBody("no fallback")
        .withFixedDelay(3000)));

    webClient
      .get().uri("/get")
      .exchange()
      .expectStatus().isOk()
      .expectBody()
      .jsonPath("$.headers.Hello").isEqualTo("World");

    webClient
      .get().uri("/delay/3")
      .header("Host", "www.circuitbreaker.com")
      .exchange()
      .expectStatus().isOk()
      .expectBody()
      .consumeWith(
        response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
  }
}

我们的测试利用 Spring Cloud Contract 的 WireMock 搭建了一个可以模拟 HTTPBin API 的服务器。首先要注意的是使用了 @AutoConfigureWireMock(port = 0)。这个注解会为我们在一个随机端口上启动 WireMock。

接下来,请注意我们利用了 UriConfiguration 类,并在 @SpringBootTest 注解中将 httpbin 属性设置为本地运行的 WireMock 服务器。在测试中,我们为通过 Gateway 调用的 HTTPBin API 设置“桩(stubs)”并模拟预期的行为。最后,我们使用 WebTestClient 向 Gateway 发送请求并验证响应。

总结

恭喜!您刚刚构建了您的第一个 Spring Cloud Gateway 应用程序!

想编写新的指南或贡献现有指南?请查看我们的贡献指南

所有指南的代码均以 ASLv2 许可证发布,文字内容以署名-禁止演绎知识共享许可证发布。