开篇词
该指南将引导你逐步使用 Spring Cloud Gateway。
你将创建的应用
我们将使用 Spring Cloud Gateway 构建网关。
你将需要的工具
- 大概 15 分钟左右;
- 你最喜欢的文本编辑器或集成开发环境(IDE)
- JDK 1.8 或更高版本;
- Gradle 4+ 或 Maven 3.2+
- 你还可以将代码直接导入到 IDE 中:
如何完成这个指南
像大多数的 Spring 入门指南一样,你可以从头开始并完成每个步骤,也可以绕过你已经熟悉的基本设置步骤。如论哪种方式,你最终都有可以工作的代码。
- 要从头开始,移步至用 Gradle 来构建;
- 要跳过基础,执行以下操作:
待一切就绪后,可以检查一下 gs-gateway/complete
目录中的代码。
用 Gradle 来构建
首先,我们设置一个基本的构建脚本。在使用 Spring 构建应用时可以使用任何喜欢的构建系统,但此处包含使用 Gradle 和 Maven 所需的代码。如果你都不熟悉,请参阅使用 Gradle 构建 Java 项目或使用 Maven 构建 Java 项目。
创建目录结构
在我们选择的项目目录中,创建以下自目录结构;例如,在 *nix 系统上使用 mkdir -p src/main/java/hello
:
└── src
└── main
└── java
└── hello
创建 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,并构建一个可运行的单个超级 jar,这使执行和传输服务更加方便;
- 它搜索
public static void main()
方法并将其标记为可运行类; - 它提供了一个内置的依赖解析器,用于设置版本号以及匹配 Spring Boot 依赖。我们可以覆盖所需的任何版本,但默认为 Boot 选择的一组版本。
用 Maven 来构建
首先,我们搭建一个基本的构建脚本。使用 Spring 构建应用时,可以使用任何喜欢的构建系统,但是此处包含了使用 Maven 所需的代码。如果你不熟悉 Maven,请参阅使用 Maven 构建 Java 项目。
创建目录结构
在我们选择的项目目录中,创建以下自目录结构;例如,在 *nix 系统上使用 mkdir -p src/main/java/hello
:
└── src
└── main
└── java
└── hello
创建 Maven 构建文件
以下是初始 Maven 构建文件。
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 Maven 插件提供了许多方便的功能:
- 它收集类路径上的所有 jar,并构建一个可运行的单个超级 jar,这使执行和传输服务更加方便;
- 它搜索
public static void main()
方法并将其标记为可运行类; - 它提供了一个内置的依赖解析器,用于设置版本号以及匹配 Spring Boot 依赖。我们可以覆盖所需的任何版本,但默认为 Boot 选择的一组版本。
用 IDE 来构建
- 阅读如何将该指南直接导入 Spring Tool Suite;
- 阅读如何在 IntelliJ IDEA 中使用该指南。
创建简单路由
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
方法采用一个 RouterLocatorBuilder
,可以轻松地使用它来创建路由。除了仅创建路由外,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 显示在请求中发送了具有值 World
的标头 Hello
。
使用 Hystrix
现在,让我们做一些有趣的事情。由于网关后面的服务可能会对客户产生不良影响,因此我们可能希望将在断路器中创建的路由进行包装。我们可以使用 Hystrix 在 Spring Cloud Gateway 中执行该操作。这是通过可添加到请求中的简单过滤器实现的。让我们创建另一条路线来证明这一点。
在该示例中,我们将利用 HTTPBin 的延迟 API,该 API 等待一定秒数后再发送响应。由于该 API 可能会花费很长时间来发送其响应,因此我们可以将使用该 API 的路由包装在 HystrixCommand
中。向我们的 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
发出请求。同样重要的是,我们必须包含 Host
标头,该标头包含 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");
}
要测试该新的后备功能,请重新启动应用,然后再次发出以下 cURL 命令:
$ curl --dump-header - --header 'Host: www.hystrix.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("*.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
属性设置为在本地运行的 WireMock 服务器。然后,在测试中,我们为通过网关调用的 HTTPBin API 设置 “存根”,并模拟我们期望的行为。最后,我们使用 WebTestClient
实际向网关发出请求并验证响应。
概述
恭喜你!我们刚刚构建了第一个 Spring Cloud Gateway 应用!
想看指南的其他内容?请访问该指南的所属专栏:《Spring 官方指南》