构建网关

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

您将构建什么

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

您需要什么

  • 大约 15 分钟

  • 您喜欢的文本编辑器或 IDE

  • Java 17+

如何完成本指南

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

从头开始,请继续转到 使用 Spring Initializr 开始

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

完成后,您可以将您的结果与 gs-gateway/complete 中的代码进行比较。

使用 Spring Initializr 开始

您可以使用此 预初始化项目 并点击生成以下载 ZIP 文件。此项目已配置为适合本教程中的示例。

手动初始化项目

  1. 导航到 https://start.spring.io。此服务会引入应用程序所需的所有依赖项,并为您完成大部分设置工作。

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

  3. 点击依赖项并选择Reactive GatewayResilience4JContract Stub Runner

  4. 点击生成

  5. 下载生成的 ZIP 文件,它是一个使用您的选择配置的 Web 应用程序的存档。

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

创建简单路由

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

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

src/main/java/gateway/Application.java

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

myRoutes 方法接收一个 RouteLocatorBuilder,该构建器可用于创建路由。除了创建路由之外,RouteLocatorBuilder 还允许您向路由添加断言和过滤器,以便您可以根据某些条件处理路由,以及根据需要更改请求/响应。

现在我们可以创建一个路由,当对网关发出 /get 请求时,将请求路由到 https://httpbin.org/get。在我们对该路由的配置中,我们添加了一个过滤器,在路由请求之前,将 Hello 请求标头和值为 World 添加到请求中

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();
}

要测试我们的简单网关,我们可以运行 Application.java,端口为 8080。应用程序运行后,向 https://127.0.0.1:8080/get 发出请求。您可以通过在终端中使用以下 cURL 命令来做到这一点

$ curl https://127.0.0.1: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": "https://127.0.0.1:8080/get"
}

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

使用 Spring Cloud CircuitBreaker

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

在下一个示例中,我们使用 HTTPBin 的延迟 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();
}

此新路由配置与我们之前创建的配置之间存在一些差异。首先,我们使用主机断言而不是路径断言。这意味着,只要主机是 circuitbreaker.com,我们就将请求路由到 HTTPBin 并将其包装在断路器中。我们通过将过滤器应用于路由来做到这一点。我们可以使用配置对象配置断路器过滤器。在此示例中,我们为断路器命名为 mycmd

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

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

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

HTTP/1.1 504 Gateway Timeout
content-length: 0

如您所见,断路器在等待 HTTPBin 的响应时超时了。当断路器超时时,我们可以选择提供一个回退,以便客户端不会收到 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();
}

现在,当包装在断路器中的路由超时时,它会在网关应用程序中调用 /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' https://127.0.0.1:8080/delay/3

有了回退,我们现在看到我们从网关收到了一个 200,响应正文为 fallback

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

fallback

编写测试

作为一个优秀的开发者,我们应该编写一些测试来确保我们的网关按预期工作。在大多数情况下,我们希望限制对外部资源的依赖,尤其是在单元测试中,因此我们不应该依赖 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();
}

我们不再将 URL 硬编码到 HTTPBin,而是从新的配置类中获取 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/main/test/java/gateway 中创建一个名为 ApplicationTest 的新类。在新类中,我们添加以下内容

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = {"httpbin=https://127.0.0.1:${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 服务器。在测试中,我们然后为通过网关调用的 HTTPBin API 设置“存根”,并模拟我们期望的行为。最后,我们使用 WebTestClient 向网关发出请求并验证响应。

总结

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

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

所有指南都使用代码的 ASLv2 许可证和文本的 署名-非商业性-禁止演绎创作共用许可证 发布。