Spring Boot 与 Docker
观察 GraphQL 的实际运行

本指南将引导您如何使用 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. 点击 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还允许您为路由添加谓词和过滤器,这样您就可以基于某些条件进行路由处理,并根据需要修改请求/响应。

现在我们可以创建一个路由,当向网关的/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();
}

要测试我们的简单网关,我们可以在端口 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 的延迟 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 发出请求。同样重要的是,我们需要在请求中包含一个 Host 头,其值为 circuitbreaker.com。否则,请求不会被路由。我们可以使用以下 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 的响应时超时了。当断路器超时时,我们可以选择提供一个回退策略,这样客户端就不会收到 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

有了备用方案后,我们现在可以看到从网关返回了一个 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();
}

我们不再硬编码 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 服务器。在测试中,我们为通过网关调用的 HTTPBin API 设置了“桩”(stubs),并模拟了我们期望的行为。最后,我们使用 WebTestClient 向网关发出请求并验证响应。

总结

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

本页目录