本指南将指导你如何用 Spring Cloud Gateway创建一个网关。

目录结构

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

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.6.RELEASE</version>
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR1</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 Cloud Gateway使用路由来处理对下游服务的请求。在本指南中,我们将把所有请求路由到HTTPBin。路由可以配置多种方式,但是对于本指南,我们将使用网关提供的Java API。

要开始,请在Application.java中创建一个新的RouteLocato的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。在这个路由的配置中,我们将添加一个过滤器,它将在路由请求之前向请求添加带有值为World的请求头Hello。
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显示请求中发送的请求头Hello的值为World。

使用Hystrix
现在让我们做一些更有趣的事情。由于网关后面的服务可能对我们的客户造成不良影响,我们可能希望将我们创建的路由封装在断路器中。您可以在使用Hystrix的Spring Cloud Gateway中执行此操作。这是通过一个简单的过滤器实现的,您可以将它添加到您的请求中。让我们创建另一条路线来演示这一点。

在本例中,我们将利用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来查看响应头,而- after --dump-header告诉curl将头打印到stdout。
执行此命令后,您应该在终端中看到以下内容:

HTTP/1.1 504 Gateway Timeout
content-length: 0

如您所见,Hystrix 在等待HTTPBin的响应时超时。当Hystrix超时时,我们可以选择提供回退,这样客户不仅可以收到504,还可以得到更有用的信息。例如,在生产场景中,您可以从缓存返回一些数据,但在我们的简单示例中,我们只返回带有主体回退的响应。
为此,我们修改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");
}

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

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

当回退到位后,我们现在看到从网关返回200。

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("*.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 Contract中的WireMock,以支持一个可以从HTTPBin模拟API的服务器。首先要注意的是使用@AutoConfigureWireMock(port = 0)。这个注释将为我们在一个随机端口上启动WireMock。

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

祝贺你!您刚刚构建了第一个Spring Coud网关应用程序!