Spring Cloud Gateway 15 分钟极速入门

Spring cloud gateway 是 spring 官方基于 Spring 5.0、Spring Boot2.0 和 Project Reactor 等技术开发的网关,Spring Cloud Gateway 旨在为微服务架构提供简单、有效和统一的API路由管理方式,Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且还基于Filer链的方式提供了网关基本的功能,例如:安全、监控/埋点、限流等。

下面就以一篇简单的 demo 程序快速入门 Spring Cloud Gateway。准备工作如下:

  • 大约花费 15 分钟
  • 一个趁手的文本编辑器或者 IDE
  • JDK >= 1.8
  • Gradle 4+ 或者 Maven 3.2+

1、如何完全这个入门

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

从头开始,继续使用Gradle进行构建。

要跳过这些基本步骤,请执行以下步骤:

  • 下载并解压缩本指南的源存储库,或者使用 Git Clone git clone https://github.com/spring-guides/gs-gateway.git
  • gs-gateway/initial 目录
  • 现在开始创建一个 简单的路由

完成后,您可以根据 gs-gateway/complete 目录中的代码检查结果。

2、通过 Gradle 进行编译

首先设置一个基本的构建脚本。在使用Spring构建应用程序时,您可以使用任何您喜欢的构建系统,但是这里包含了使用Gradle和Maven所需的代码。如果您不熟悉这两种方法,请参考使用 Gradle 构建 Java 项目或使用 Maven 构建 Java项目

2.1 创建一个文件结构

在您选择的项目目录中,创建以下子目录结构,例如在 *nix 系统上通过 mkdir -p src/main/java/hello 创建以下目录结果。

└── src
    └── main
        └── java
            └── hello

2.2 创建一个 gradle 构建文件

下面就是 gradle 的构建项目

build.gradle

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.1.7.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

bootJar {
    baseName = 'gs-gateway'
    version =  '0.1.0'
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:Greenwich.SR2"
    }
}

dependencies {
    compile("org.springframework.cloud:spring-cloud-starter-gateway")
    compile("org.springframework.cloud:spring-cloud-starter-netflix-hystrix")
    compile("org.springframework.cloud:spring-cloud-starter-contract-stub-runner"){
        exclude group: "org.springframework.boot", module: "spring-boot-starter-web"
    }
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

Spring Boot gradle 插件提供了很多方便的特性:

  • 它收集类路径上的所有jar,并构建一个单独的、可运行的uber-jar,这使它更方便地执行和传输您的服务。
  • 它搜索 public static void main() 方法,将其标记为可运行类。
  • 它提供了一个内置的依赖项解析器,用于设置版本号以匹配 Spring Boot 的依赖项。您可以覆盖您希望覆盖的任何版本,否则它将使用默认 Spring Boot 选择的版本集。

3、使用 Maven 构建项目

首先设置一个基本的构建脚本。在使用 Spring 构建应用程序时,您可以使用您喜欢的任何构建系统,但是这里包含了使用 Maven 所需的代码。如果您不熟悉 Maven,请参考 使用 Maven 构建 Java 项目

3.1 创建文件目录

在您选择的项目目录中,创建以下子目录结构,例如在 *nix 系统上通过 mkdir -p src/main/java/hello 创建以下目录结果。

└── src
    └── main
        └── java
            └── hello

以下就是 maven 构建项目需要的 pom.xml 文件

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-gateway</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-web</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Spring Boot gradle 插件提供了很多方便的特性:

  • 它收集类路径上的所有jar,并构建一个单独的、可运行的uber-jar,这使它更方便地执行和传输您的服务。
  • 它搜索 public static void main() 方法,将其标记为可运行类。
  • 它提供了一个内置的依赖项解析器,用于设置版本号以匹配 Spring Boot 的依赖项。您可以覆盖您希望覆盖的任何版本,否则它将使用默认 Spring Boot 选择的版本集。

4、创建一个简单的路由

Spring Cloud Gateway 使用路由来处理对下游服务的请求。在本指南中,我们将把所有请求路由到 HTTPBin。路由可以通过多种方式配置,但在本指南中,我们将使用网关提供的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。在此路由的配置中,我们将添加一个过滤器,该过滤器将在路由之前向请求头 key 需要为 Hello,请求头 value 需要为 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 上运行。应用程序运行后,向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 显示在请求中发送了带值 World 的报文头 Hello

5、 使用 Hystrix

现在让我们做一些更有趣的事情。由于网关背后的服务可能会对我们的客户产生不良影响,所以我们可能希望将我们创建的路由封装在断路器中。您可以在 Spring Cloud Gatewa y中使用 Hystrix 来实现这一点。这是通过一个简单的过滤器实现的,您可以将该过滤器添加到您的请求中。让我们创建另一个路由来演示这一点。

在本例中,我们将利用 HTTPBin 的延迟API,该API在发送响应之前等待一定的时间。因为这个API可能会花费很长时间来发送它的响应,所以我们可以在一个 HystrixCommand 中包装使用这个 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("*.hystrix.com")
            .filters(f -> f.hystrix(config -> config.setName("mycmd")))
            .uri("http://httpbin.org:80")).
        build();
}

这个新路由配置与我们之前创建的路由配置之间存在一些差异。首先,我们使用的是主机谓词而不是路径谓词。这意味着只要主机是 hystrix.com,我们就会将请求路由到HTTPBin,并将该请求包装在 HystrixCommand 中。我们通过对路由应用一个过滤器来实现这一点。可以使用配置对象配置 Hystrix 过滤器。在本例中,我们只是将 HystrixCommand命名为 mycmd

让我们测试一下这条新路由。启动应用程序,但是这次我们将向/delay/3发出请求。同样重要的是,我们应该包含一个主机报文头,其中包含 hystrix.com 的主机,否则请求将不会被路由。在 cURL 中是这样的:

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

我们使用 ——dump-header 来查看响应标头,后面的 ——dump-header 告诉cURL将标头打印到 stdout。

执行此命令后,您应该在终端中看到以下内容:

HTTP/1.1 504 Gateway Timeout
content-length: 0

可以看到,Hystrix 超时等待来自 HTTPBin 的响应。当 Hystrix 超时时,我们可以选择提供一个后备方案,这样客户不仅可以收到 504,还可以收到更有意义的东西。在生产场景中,您可能会从缓存中返回一些数据,但是在我们的简单示例中,我们将返回一个 fallback 响应,而不是返回正文。

为此,让我们修改 Hystrix 过滤器,以提供一个 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("*.hystrix.com")
            .filters(f -> f.hystrix(config -> config
                .setName("mycmd")
                .setFallbackUri("forward:/fallback")))
            .uri("http://httpbin.org:80"))
        .build();
}

现在,当Hystrix 包装路由超时时,它将在网关应用程序中调用 /fallback。让我们将 /fallback 端点添加到我们的应用程序中。

Application.java 中添加类级注释@RestController,然后将下面的 @RequestMapping 添加到类中。

src/main/java/gateway/Application.java

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

要测试这个新的 fallback 功能,请重新启动应用程序,并再次发出以下cURL命令:

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

有了适当的 fallback,我们现在看到我们从的网关获得了状态为 200 的 fallback 的响应体。

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

fallback

6、测试用例

作为一名优秀的开发人员,我们应该编写一些测试来确保我们的网关正在做我们期望它应该做的事情。在大多数情况下,我们希望限制对外部资源的依赖,特别是在单元测试中,因此我们不应该依赖 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("*.hystrix.com")
      .filters(f -> f
        .hystrix(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("*.hystrix.com")
        .filters(f -> f
          .hystrix(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 ,这个类的内部如下所示:

@RunWith(SpringRunner.class)
@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.hystrix.com")
      .exchange()
      .expectStatus().isOk()
      .expectBody()
      .consumeWith(
        response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
  }
}

我们的测试实际上利用了来自Spring Cloud 契约的 WireMock,以便让服务器能够模拟来自 HTTPBin 的 api 。首先要注意的是 @AutoConfigureWireMock(port = 0) 的使用,该注释将在一个随机端口上为我们启动WireMock。

接下来请注意,我们正在利用我们的 UriConfiguration 类,并将@SpringBootTest 注释中的 httpbin 属性设置为本地运行的 WireMock 服务器。在测试中,我们为通过网关调用的 HTTPBin api 设置“stubs”,并模拟我们期望的行为。最后,我们使用 WebTestClient 实际向网关发出请求并验证响应。

7、总结

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

想写一本新的指南还是对现有的指南做些贡献?请查看我们的 投稿指南

参考地址:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章