Gateway 入门教程 - 基础篇

大纲

Reactor 与 WebFlux 介绍

Reactor 是什么

为了应对高并发的服务器端开发,在 2009 年 的时候,微软提出了一个更优雅地实现异步编程的方式 - Reactive Programming,中文名是响应式编程或者叫反应式编程。随后,其它技术也迅速地跟上了脚步,像 ES6 通过 Promise 引入了类似的异步编程方式。Netflix 和 TypeSafe 公司也提供了 RxJava、Scala、Akka 技术,让 Java 平台也有了能够实现响应式编程的框架,现在比较熟知的 Hystrix 就是以 RxJava 为基础开发的。到了 2017 年,虽然已经有不少公司在实践响应式编程,但整体来说应用范围依旧不大,主要原因在于缺少简单易用的技术将响应式编程推广普及,诸如 MVC 框架、HTTP 客户端、数据库技术等整合。终于,在 2017 年 9 月 28 日,Spring 5 正式发布,而 Spring 5 其最大的意义就是将响应式编程技术的普及向前推进一大步。在背后支持 Spring 5 响应式编程的框架正是 Reactor,它是由 Pivotal 公司(开发 Spring 等技术的公司)开发的,实现了 Reactive Programming 思想,符合 Reactive Streams 规范(Reactive Streams 是由 Netflix、TypeSafe、Pivotal 等公司发起的)的一项技术。Reactive 与 Servlet 的技术栈对比图如下:

spring-webflux-view

WebFlux 是什么

Spring WebFlux 是 Spring 5.0 引入的新的响应式框架,区别于 Spring MVC,它不需要依赖 Servlet API,采用异步非阻塞的架构,底层基于 Reactor 来实现响应式流规范。因此,Spring WebFlux 特别适合应用在 I/O 密集型的服务中,比如微服务网关这样的应用中。在传统的 Web 架构中,比如 Struts2、Spring MVC 等都是基于 Servlet API 与 Servlet 容器基础之上运行的,使用的是同步阻塞式 I/O 模型。但是在 Servlet 3.1 之后有了异步非阻塞的支持,而 Spring WebFlux 采用的就是典型的异步非阻塞架构,它的核心是基于 Reactor 的相关 API 实现。相对于传统的 Web 架构来说,Spring WebFlux 可以运行在诸如 Netty 及支持 Servlet 3.1+ 的容器(Tomcat、Jetty、Undertow)之上,支持异步非阻塞式 I/O 模型 + 函数式编程(依赖 JDK 8)。根据官方的说明,Spring WebFlux 并不能使接口的响应时间缩短,它仅仅能够提升吞吐量和伸缩性。Spring WebFlux 与 传统 Web 架构的对比图如下:

webflux-vs-servlet.png

首先需要明确的一点就是,Spring WebFlux 不是 Spring MVC 的替代方案!虽然 Spring WebFlux 也可以运行在 Servlet 容器之上(Servlet 3.1+),但是 Spring WebFlux 主要还是应用在适合使用异步非阻塞模型的业务场景。而 Spring MVC 是同步阻塞的,如果项目在 Spring MVC 框架中大量使用了非同步方案,那么 Spring WebFlux 才是适用,否则使用 Spring MVC 才是首选。在微服务架构中,Spring MVC 和 Spring WebFlux 可以混合使用(不是指在同一个应用内),比如上面已经提到的,对于那些 I/O 密集型服务(如网关)就可以使用 Spring WebFlux 来实现。Spring MVC 和 Spring WebFlux 的对比图如下:

webflux-vs-springmvc

  • Spring WebFlux 默认情况下使用 Netty 作为服务器
  • Spring WebFlux 暂时不支持 MySQL,支持 Redis、MongoDB、PostgreSQL
  • Spring WebFlux 使用的响应式流并不是用 JDK 9 提供的,而是基于 Reactor 响应式流库
  • Spring WebFlux 也可以使用 Spring MVC 注解,如 @Controller,方便在两个 Web 框架中自由转换
  • Spring WebFlux 与 Spring MVC 都可以使用 Tomcat、Jetty、Undertow 等 Servlet 容器(Servlet 3.1+)
  • Spring MVC 因为是使用的同步阻塞式 I/O 模型,更方便开发人员开发和测试代码;一般来说,如果 Spring MVC 能够满足的场景,就尽量不要用 Spring WebFlux

Gateway 介绍

Spring Cloud Gateway 是 Spring 官方基于 Spring 5.x、Spring Boot 2.x、Spring WebFlux 和 Reactor 等技术开发的网关,旨在为微服务架构提供简单、有效且统一的 API 路由管理方式。Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul 1.x。在 Spring Boot 2.0 以上版本中,并没有对 Zuul 2.0 以上最新高性能版本进行集成,仍然使用 Zuul 1.x 非 Reactor 模式(基于 Servlet 2.5 阻塞架构)的旧版本。Spring Cloud Gateway 其不仅提供统一的路由方式,并且还基于 Filter 链的方式提供了网关基本的功能,例如:熔断、重试、安全、监控 / 指标、限流等,更多资料可参考:Gateway 官方英文文档

Zuul 与 Gateway 的关系

在 Spring Cloud 全家桶中,有个很重要的组件就是网关。在 Spring Boot 1.x 版本中都是使用 Zuul 网关,但是在 Spring Boot 2.x 版本中,Zuul 新版本(2.0)的发布一直跳票,Spring Cloud 最后自己研发了一个网关 Gateway 来替代 Zuul。简而言之,Gateway 是原 Zuul 1.x 版本的替代方案。

Gateway 的核心作用

Gateway 的核心作用如下:

  • 鉴权
  • 熔断
  • 限流
  • 反向代理
  • 日志监控

在微服务架构中,Gateway 通常架设在 Nginx 等负载均衡服务的后面,具体所处的位置如下图所示:

Gateway 的核心概念

网关提供 API 全托管服务,丰富的 API 管理功能,辅助企业管理大规模的 API,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略(WAF)、防刷、流量、监控日志等功能。一般来说,网关对外暴露的 URL 或者接口信息,统称为路由信息。如果研发过网关中间件,或者使用或了解过 Zuul 的开发者,会知道网关的核心肯定是 Filter 以及 Filter Chain(Filter 责任链)。Spring Cloud Gateway 也具有路由和 Filter 的概念,其中最重要的几个概念如下:

  • 路由(route):路由是网关最基础的部分,路由信息由一个 ID、一个目的 URL、一组断言工厂和一组 Filter 组成;如果路由断言为真,则说明请求的 URL 和配置的路由匹配。
  • 断言(predicate):Java 8 中的断言函数,Spring Cloud Gateway 中的断言函数输入类型是 Spring 5.0 框架中的 ServerWebExchange。在 Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自于 HTTP Request 中的任何信息,比如请求头和参数等。
  • 过滤器(filter):一个标准的 Spring Web Filter,在 Spring Cloud Gateway 中的 Filter 分为两种类型,分别是 Gateway Filter 和 Global Filter,过滤器 Filter 将会对请求和响应进行修改处理,包括在请求被路由之前或者之后对其进行修改。

Gateway 的工作流程

  • Spring Cloud Gateway 组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。
  • Spring Cloud Gateway 是加在整个微服务最前沿的防火墙和代理器,隐藏了微服务结点的 IP 和端口信息,从而可以加强安全保护。
  • Spring Cloud Gateway 本身也是一个微服务,需要注册进服务注册中心。

Gateway 的核心原理

Spring Cloud Gateway 的核心处理流程如下图所示,Gateway 的客户端会向 Spring Cloud Gateway 发起请求,请求首先会被 HttpWebHandlerAdapter 进行提取组装成网关的上下文,然后网关的上下文会传递给 DispatcherHandler。这里的 DispatcherHandler 是所有请求的分发处理器,DispatcherHandler 主要负责分发请求到对应的处理器,比如将请求分发到对应 RoutePredicateHandlerMapping(路由断言处理映射器)。路由断言处理映射器主要用于路由的查找,以及找到路由后返回对应的 FilteringWebHandler。FilteringWebHandler 主要负责组装 Filter 链表并调用 Filter 执行一系列的 Filter 处理,然后把请求转到后端对应的代理服务处理,处理完毕之后,将 Response 返回到 Gateway 客户端。在 Filter 链中,通过虚线分割 Filter 的原因是,过滤器可以在转发请求之前处理或者接收到被代理服务的返回结果之后处理。所有的 Pre 类型的 Filter 执行完毕之后,才会转发请求到被代理的服务处理。被代理的服务把所有请求处理完毕之后,才会执行 Post 类型的过滤器。另外,Pre 类型的过滤器可以实现参数校验、权限校验、流量监控、日志监控、协议转换等功能,Post 类型的过滤器可以实现 HTTP 响应内容与响应头的修改、日志监控、流量监控等功能。值得一提的是,在配置路由的时候,如果不指定端口的话,HTTP 默认设置端口为 80,HTTPS 默认设置端口为 443,Spring Cloud Gateway 的启动容器目前只支持 Netty。

gateway-process

Gateway 入门案例

1. 版本说明

在本文中,使用的 Spring Cloud 版本是 Finchley.RELEASE,对应的 Spring Boot 版本是 2.0.3,特别声明除外,点击下载完整的案例代码。

2. 创建 Maven 父级 Pom 工程

在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下。特别注意,Spring Cloud Gateway 是基于 WebFlux 的,它与 Spring MVC 是不兼容的,如果引用了 spring-boot-starter-web,则需要把 spring-webmvc 排除掉;由于 Spring Cloud Gateway 的启动容器目前只支持 Netty,因此还需要将 spring-boot-starter-tomcat 排除掉。这里也可以引入 spring-boot-starter-webflux 来替代 Spring MVC 的功能,关于 Gateway 的使用,原则上只需要引入 spring-cloud-starter-gateway 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

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

<!-- 利用传递依赖,公共部分 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<!-- 管理依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<!--注意:这里需要添加以下配置,否则可能会有各种依赖问题 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>

3. 创建 Gateway 工程

  • 创建 Gateway 的 Maven 工程,配置工程里的 pom.xml 文件
1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  • 创建 Gateway 的配置类,使用 Java 流式 API 自定义 RouteLocator
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
// 当访问到 http://127.0.0.1:9090/jd 直接跳转到京东商城的首页
return builder.routes()
.route(r -> r.path("/jd")
.uri("http://jd.com:80/").id("jd_route")
).build();
}
}
  • 创建 Gateway 的主控制类
1
2
3
4
5
6
7
@SpringBootApplication
public class GatewayApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
  • 创建 Gateway 的 application.yml 配置文件,添加开启端点的配置信息,Spring Cloud Gateway 提供了一个 Gateway Actuator,该 EndPiont 提供了关于 Filter 及 Routes 的信息查询以及指定 Route 信息更新的 Rest API 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 9090

spring:
application:
name: gateway-server

logging:
level:
org.springframework.cloud.gateway: TRACE
org.springframework.http.server.reactive: DEBUG
org.springframework.web.reactive: DEBUG
reactor.ipc.netty: DEBUG

management:
endpoints:
web:
exposure:
include: '*'
security:
enabled: false

4. 创建 Gateway YML 工程

Spring Cloud Gateway 支持两种方式去配置路由信息,上述代码通过 Java 流式 API 自定义 RouteLocator 的方式定义 Spring Cloud Gateway 的路由信息,也可以通过如下 YML 文件的方式配置路由。

  • 创建 Gateway 的 Maven 工程,配置工程里的 pom.xml 文件
1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  • 创建 Gateway 的主控制类
1
2
3
4
5
6
7
@SpringBootApplication
public class GatewayApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
  • 创建 Gateway 的 application.yml 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
server:
port: 9091

spring:
application:
name: gateway-server
cloud:
gateway:
routes: #当访问到 http://127.0.0.1:9090/baidu 直接跳转到百度的首页
- id: baidu_route
uri: http://baidu.com:80
predicates:
- Path=/baidu

logging:
level:
org.springframework.cloud.gateway: TRACE
org.springframework.http.server.reactive: DEBUG
org.springframework.web.reactive: DEBUG
reactor.ipc.netty: DEBUG

management:
endpoints:
web:
exposure:
include: '*'
security:
enabled: false

5. 测试案例代码

  • (1) 分别启动 gateway、gateway-yml 应用
  • (2) 访问 http://127.0.0.1:9090/actuator/gateway/routes,查看返回的所有路由信息,如下图所示:
    gateway-demo-actuator-routes
  • (3) 访问 http://127.0.0.1:9090/jd,查看是否成功跳转到京东商城的首页
  • (4) 访问 http://127.0.0.1:9091/baidu,查看是否成功跳转到百度的首页

Gateway 的路由断言

Spring Cloud Gateway 的路由匹配的功能是以 Spring WebFlux 中的 Handler Mapping 为基础实现的。Spring Cloud Gateway 也是由许多的路由断言工厂组成的,当 HTTP Request 请求进入 Spring Cloud Gateway 的时候,网关中的路由断言工厂会根据配置的路由规则,对 HTTP Request 请求进行断言匹配;匹配成功则进行下一步处理,否则断言失败直接返回错误信息。值得一提的是,Spring Cloud Gateway 大多数的路由断言工厂是支持正则表达式匹配的。下面将给出各种路由断言工厂的使用示例,点击下载完整的案例代码。

路由断言的两种配置方式

  • 第一种(快捷方式)
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://baidu.com:80
predicates:
- Cookie=mycookie,mycookievalue
  • 第二种(完全展开方式)
1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://baidu.com:80
predicates:
- name: Cookie
args:
name: mycookie
regexp: mycookievalue

提示

更多路由断言的配置说明请参考 官方文档

After 路由断言工厂

After 路由断言工厂中会取一个 UTC 时间格式的时间参数,当请求进来的当前时间在配置的 UTC 时间之后,则会成功匹配,否则不能成功匹配。

  1. 通过 Java 代码的方式,将 After 路由断言的配置信息配置到路由里去:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
// 生成比当前时间早一个小时的UTC时间
ZonedDateTime minusTime = LocalDateTime.now().minusHours(1).atZone(ZoneId.systemDefault());
return builder.routes()
.route(
"after_route", r -> r.after(minusTime).uri("http://baidu.com")
)
.build();
}
}
  1. 也可以在 YML 文件里配置 After 路由断言信息:其中的 UTC 时间可以使用 Java 代码生成,例如 ZonedDateTime.now().minusHours(1).format(DateTimeFormatter.ISO_ ZONED_DATE_TIME);
1
2
3
4
5
6
7
8
9
10
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: after_route
uri: http://baidu.com
predicates:
- After=2019-10-01T21:55:18.146+08:00[Asia/Shanghai]
  1. 测试结果:
  • 启动应用,访问 http://127.0.0.1,查看是否成功跳转到百度的首页
  • 更改 UTC 时间为当前时间一个小时后的 UTC 时间,然后再启动应用,访问 http://127.0.0.1,页面会返回 404 错误信息

Before 路由断言工厂

Before 路由断言工厂会取一个 UTC 时间格式的时间参数,当请求进来的当前时间在配置的 UTC 时间之前,则会成功匹配,否则不能成功匹配。

  1. 通过 Java 代码的方式,将 Before 路由断言的配置信息配置到路由里去:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
// 生成比当前时间晚一个小时的UTC时间
ZonedDateTime plusTime = LocalDateTime.now().plusHours(1).atZone(ZoneId.systemDefault());
return builder.routes()
.route(
"before_route", r -> r.before(plusTime).uri("http://baidu.com")
)
.build();
}
}
  1. 也可以在 YML 文件里配置 Before 路由断言信息:
1
2
3
4
5
6
7
8
9
10
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: before_route
uri: http://baidu.com
predicates:
- Before=2019-10-01T21:55:18.146+08:00[Asia/Shanghai]

Between 路由断言工厂

Between 路由断言工厂会取一个 UTC 时间格式的时间参数,当请求进来的当前时间在配置的 UTC 时间之间,则会成功匹配,否则不能成功匹配。

  1. 通过 Java 代码的方式,将 Between 路由断言的配置信息配置到路由里去:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
ZonedDateTime minusTime = LocalDateTime.now().minusHours(1).atZone(ZoneId.systemDefault());
ZonedDateTime plusTime = LocalDateTime.now().plusHours(1).atZone(ZoneId.systemDefault());
return builder.routes()
.route(
"between_route", r -> r.between(minusTime, plusTime).uri("http://baidu.com")
)
.build();
}
}
  1. 也可以在 YML 文件里配置 Between 路由断言信息:
1
2
3
4
5
6
7
8
9
10
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: between_route
uri: http://baidu.com
predicates:
- Between=2019-11-10T11:11:11.111 08:00[Asia/Shanghai], 2019-11-12T11:11:11.111 08:00[Asia/Shanghai]

Cookie 路由断言工厂会取两个参数,分别是 cookie 名称对应的 key 和 value。当请求中携带的 cookie 和 Cookie 断言工厂中配置的 cookie 一致,则路由匹配成功,否则匹配不成功。

  1. 通过 Java 代码的方式,将 Cookie 路由断言的配置信息配置到路由里去:
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(
"cookie_route", r -> r.cookie("book", "java").uri("http://baidu.com")
)
.build();
}
}
  1. 也可以在 YML 文件里配置 Cookie 路由断言信息:
1
2
3
4
5
6
7
8
9
10
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: cookie_route
uri: http://baidu.com
predicates:
- Cookie=book, java
  1. 测试结果
  • 启动 gateway-cookie 应用
  • 在 Postman 中将 book=java 添加到 Cookie,然后访问 http://127.0.0.1,查看是否成功跳转到百度的首页

Header 路由断言工厂

Header 路由断言工厂用于根据配置的路由 header 信息进行断言匹配路由,匹配成功进行转发,否则不进行转发。

  1. 通过 Java 代码的方式,将 Header 路由断言的配置信息配置到路由里去:
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(
"header_route", r -> r.header("X-Request-Id", "Peter").uri("http://baidu.com")
)
.build();
}
}
  1. 也可以在 YML 文件里配置 Header 路由断言信息:
1
2
3
4
5
6
7
8
9
10
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: header_route
uri: http://baidu.com
predicates:
- Header=X-Request-Id, Peter
  1. 测试结果
  • 启动 gateway-header 应用
  • 在 Postman 中将 X-Request-Id=Peter 添加到 Header,然后访问 http://127.0.0.1,查看是否成功跳转到百度的首页

Host 路由断言工厂

Host 路由断言工厂根据配置的 Host(可以简单理解为域名),对请求中的 Host 进行断言处理,断言成功则进行路由转发,否则不转发。

  1. 通过 Java 代码的方式,将 Host 路由断言的配置信息配置到路由里去:
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(
"host_route", r -> r.host("**.study.com").uri("http://baidu.com")
)
.build();
}
}
  1. 也可以在 YML 文件里配置 Host 路由断言信息:
1
2
3
4
5
6
7
8
9
10
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: host_route
uri: http://baidu.com
predicates:
- Host=**.study.com
  1. 测试结果
  • 编辑系统的 hosts 配置文件,添加域名映射:127.0.0.1 www.study.com
  • 启动 gateway-host 应用
  • 访问 http://www.study.com,查看是否成功跳转到百度的首页

Method 路由断言工厂

Method 路由断言工厂会根据路由信息配置的 method 对请求方法是 Get 或者 Post 等进行断言匹配,匹配成功则进行转发,否则处理失败。

  1. 通过 Java 代码的方式,将 Method 路由断言的配置信息配置到路由里去:
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(
"method_route", r -> r.method("GET").uri("http://baidu.com")
)
.build();
}
}
  1. 也可以在 YML 文件里配置 Method 路由断言信息:
1
2
3
4
5
6
7
8
9
10
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: method_route
uri: http://baidu.com
predicates:
- Method=GET

Query 路由断言工厂

Query 路由断言工厂会从请求中获取参数,并将请求中的参数和 Query 断言路由中的配置进行匹配,比如 http://127.0.0.1?book=java 中的 book=java 和下面的 r.query("book","java") 配置一致,则转发成功,否则转发失败。

  1. 通过 Java 代码的方式,将 Query 路由断言的配置信息配置到路由里去:
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(
"query_route", r -> r.query("book", "java").uri("http://baidu.com")
)
.build();
}
}
  1. 也可以在 YML 文件里配置 Query 路由断言信息:
1
2
3
4
5
6
7
8
9
10
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: query_route
uri: http://baidu.com
predicates:
- Query=book, java

Path 路由断言工厂

Path 路由断言工厂接收一个参数,根据 Path 定义好的规则来判断访问的 URI 是否匹配。在下述配置中,如果请求路径为 /blog/detail/,则此路由将匹配;也可以使用表达式,例如 /blog/detail/** 表示匹配 /blog/detail/ 开头的多级 URI。特别注意,下述的 URI 如果不以 / 结尾,那么转发后的 URI 为 http://baidu.com/blog/detail/;若以 / 结尾,转发后的 URI 则为 http://baidu.com/

  1. 通过 Java 代码的方式,将 Path 路由断言的配置信息配置到路由里去:
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(
"path_route", r -> r.path("/blog/detail/").uri("http://baidu.com/")
)
.build();
}
}
  1. 也可以在 YML 文件里配置 Path 路由断言信息:
1
2
3
4
5
6
7
8
9
10
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: path_route
uri: http://baidu.com/
predicates:
- Path=/blog/detail/

Weight 路由断言工厂

Weight 路由断言工厂,在 Spring Cloud Gateway 中可以使用它对 URL 进行权重路由,只需在配置时指定分组和权重值即可。下述配置中,添加了两个针对 /test 路径转发的路由定义配置,这两个路由属于同一个权重分组,权重的分组名称为 group。最终的效果是把 /test 接口的 95% 的请求流量分发给服务的 V1 版本,把剩余 5% 的流量分发给服务的 V2 版本,具体的实战案例可参考这里的教程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: provider-service-v1
uri: http://127.0.0.1:9091/v1/
predicates:
- Path=/test
- Weight=group, 95

- id: provider-service-v2
uri: http://127.0.0.1:9091/v2/
predicates:
- Path=/test
- Weight=group, 5

RemoteAddr 路由断言工厂

RemoteAddr 路由断言工厂配置一个 IPv4 或 IPv6 网段的字符串或者 IP。当客户端的 IP 地址在网段之内或者和配置的 IP 相同,则成功转发,否则不能转发。例如 192.168.2.1/24 表示一个网段,其中 192.168.2.1 是 IP 地址,24 是子网掩码,当然也可以直接配置一个 IP,比如 127.0.0.1

  1. 通过 Java 代码的方式,将 RemoteAddr 路由断言的配置信息配置到路由里去:
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(
"remoteaddr_route", r -> r.remoteAddr("127.0.0.1").uri("http://baidu.com")
)
.build();
}
}
  1. 也可以在 YML 文件里配置 RemoteAddr 路由断言信息:
1
2
3
4
5
6
7
8
9
10
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: http://baidu.com
predicates:
- RemoteAddr=127.0.0.1

IP 地址表示法(CIDR)

  • 在很多项目中,如果涉及到 IP 的拦截和访问控制都会有这样的写法,比如 192.168.31.1/24,这种写法是一种网络标准规范,被称为无类别域间路由(CIDR)。
  • 以 IPv4 为例解释 CIDR 标记 ipv4/number,其中的 number 用于标记网络标识中的连续 1 的位数,计算方式:ipv4 & number = dest & number
  • 比如 192.168.31.1/24 可以标识 192.168.31.0 - 192.168.31.255
  • 比如 192.168.31.2/30 可以标识 192.168.31.0 - 192.168.31.3

Gateway 的单一内置过滤器

Spring Cloud Gateway 中内置了很多的过滤器工厂(GatewayFilter Factories),当然也可以根据实际应用场景的需要定制自己的过滤器工厂。过滤器允许以某种方式修改进来的 HTTP 请求或返回的 HTTP 响应。过滤器主要作用于需要处理的特定路由,Spring Cloud Gateway 提供了很多种的过滤器工厂,过滤器的实现类将近三十多个。总得来说,可以分为七类:Header、Parameter、Path、Status、Redirect 跳转、Hytrix 熔断和 RateLimiter 限流。下面将介绍 Spring Cloud Gateway 中常用的 Filter 工厂,点击下载完整的案例代码。

提示

Gateway 默认提供了三十多种类型的过滤器工厂,更多的使用介绍请参考 官方文档

请求头相关的过滤器

AddRequestHeader

AddRequestHeader 过滤器工厂用于对匹配上的请求添加 HTTP Header:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("add_request_header_route", r ->
r.path("/addRequestHeader")
.filters(f -> f.addRequestHeader("X-Request-Id", "peter"))
.uri("http://127.0.0.1:8080/addRequestHeader/")
).build();
}
}

也可以在 YML 文件里配置 AddRequestHeader 过滤器:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://baidu.com:80
predicates:
- Path=/id/**
filters:
- AddRequestHeader=X-Request-Id, peter

还可以使用占位符:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://baidu.com:80
predicates:
- Path=/id/{segment}
filters:
- AddRequestHeader=X-Request-Id, id-{segment}

RemoveRequestHeader

RemoveRequestHeader 过滤器工厂用于对匹配上的请求删除 HTTP Header:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: remove_request_header_route
uri: http://baidu.com:80
predicates:
- Path=/id/**
filters:
- RemoveRequestHeader=X-Request-Id

SetRequestHeader

SetRequestHeader 过滤器工厂用于对匹配上的请求替换 HTTP Header 的值:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: set_request_header_route
uri: http://baidu.com:80
predicates:
- Path=/id/**
filters:
- SetRequestHeader=X-Response-Id, david

还可以使用占位符:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: set_request_header_route
uri: http://baidu.com:80
predicates:
- Host: {segment}.myhost.org
filters:
- SetRequestHeader=X-Response-Id, id-{segment}

请求参数相关的过滤器

AddRequestParameter

AddRequestParameter 过滤器作用是对匹配上的请求添加请求参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("add_request_parameter_route", r ->
r.path("/addRequestParameter")
.filters(f -> f.addRequestParameter("book", "java"))
.uri("http://127.0.0.1:8080/addRequestParameter/")
).build();
}
}

也可以在 YML 文件里配置 AddRequestParameter 过滤器:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: http://baidu.com:80
predicates:
- Path=/id/**
filters:
- AddRequestParameter=color, blue

RemoveRequestParameter

RemoveRequestParameter 过滤器作用是对匹配上的请求删除请求参数:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: remove_request_parameter_route
uri: http://baidu.com:80
predicates:
- Path=/id/**
filters:
- RemoveRequestParameter=color

响应头相关的过滤器

AddResponseHeader

AddResponseHeader 过滤器工厂的作用是对从网关返回的请求响应添加 HTTP Header:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("add_response_header_route", r ->
r.path("/addResponseHeader")
.filters(f -> f.addResponseHeader("X-Request-Id", "Peter"))
.uri("http://baidu.com:80/")
).build();
}
}

也可以在 YML 文件里配置 AddResponseHeader 过滤器:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: add_response_header_route
uri: http://baidu.com:80
predicates:
- Path=/id/**
filters:
- AddResponseHeader=X-Response-Color, blue

RemoveResponseHeader

RemoveResponseHeader 过滤器工厂的作用是对从网关返回的请求响应删除 HTTP Header:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: remove_response_header_route
uri: http://baidu.com:80
predicates:
- Path=/id/**
filters:
- RemoveResponseHeader=X-Response-Foo

SetResponseHeader

SetResponseHeader 过滤器工厂的作用是对从网关返回的请求响应替换 HTTP Header 的值:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: set_response_header_route
uri: http://baidu.com:80
predicates:
- Path=/id/**
filters:
- SetResponseHeader=X-Response-Color, blue

还可以使用占位符:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: set_response_header_route
uri: http://baidu.com:80
predicates:
- Host: {segment}.myhost.org
filters:
- SetResponseHeader=X-Response-Foo, bar-{segment}

前缀和路径相关的过滤器

SetPath

SetPath 过滤器工厂的作用是对匹配上的请求重新设置 URL 地址:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: set_path_route
uri: http://baidu.com:80
predicates:
- Path=/id/{segment}
filters:
- SetPath=/{segment}
接口的调用说明接口的调用地址
外部(如浏览器)调用的地址http://locahost/id/hello
微服务应用内部实际调用的地址http://baidu.com:80/hello

PrefixPath

PrefixPath 过滤器工厂的作用是对匹配上的请求添加 URL 前缀:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: prefix_path_route
uri: http://baidu.com:80
predicates:
- Path=/id/**
filters:
- PrefixPath=/hello
接口的调用说明接口的调用地址
外部(如浏览器)调用的地址http://locahost/id
微服务应用内部实际调用的地址http://baidu.com:80/hello/id

StripPrefix

StripPrefix 过滤器工厂的作用是对匹配上的请求去除 URL 前缀:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: strip_prefix_route
uri: http://baidu.com:80
predicates:
- Path=/pay/id/**
filters:
- StripPrefix=2
接口的调用说明接口的调用地址
外部(如浏览器)调用的地址http://locahost/pay/id
微服务应用内部实际调用的地址http://baidu.com:80

提示

StripPrefix 过滤器工厂接受一个参数,该参数表示去掉的路径段数量(前缀数量),每个 / 之间的部分算作一个路径段。比如 /api/v1/pay 有三个路径段,包括 apiv1pay。路径去除的部分不会传递给后端服务,后端只会收到被裁剪后的路径。

RedirectTo

RedirectTo 过滤器工厂的作用是将匹配上的请求重定向到指定的 URL 地址:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: redirect_to_route
uri: http://baidu.com:80
predicates:
- Path=/id/**
filters:
- RedirectTo=302, http://jd.com:80
接口的调用说明接口的调用地址
外部(如浏览器)调用的地址http://locahost/id/
微服务应用内部实际重定向的地址http://jd.com:80

提示

RedirectTo 过滤器工厂接受两个参数,分别是状态码和 URL。状态码参数通常是 300 系列的重定向 HTTP 代码,比如 301。URL 参数需要是有效的 URL 地址。

RewritePath

Spring Cloud Gateway 可以使用 RewritePath 替换 Zuul 的 StripPrefix 功能,而且功能更强大。在 Zuul 中使用如下配置后,所有 /example/xxxx 的请求会转发给 http://example.com/xxxx,同时去除掉了 example 前缀。

1
2
3
4
5
6
zuul:
routes:
example:
path: /example/**
stripPrefix: true
uri: http://example.com

Spring Cloud Gateway 实现了类似的功能,使用的是 RewritePath 过滤器工厂。特别注意,下述的 URI 是不以 / 结尾的,否则仅仅会直接跳转到 http://baidu.com

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("rewrite_path_route", r ->
r.path("/foo/**")
.filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/$\\{segment}"))
.uri("http://baidu.com:80")
).build();
}
}

也可以在 YML 文件里配置 RewritePath 过滤器:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: rewrite_path_route
uri: http://baidu.com:80
predicates:
- Path=/foo/**
filters:
- RewritePath=/foo(?<segment>/?.*), $\{segment}
接口的调用说明接口的调用地址
外部(如浏览器)调用的地址http://locahost/foo/cache/sethelp/help.html
微服务应用内部实际调用的地址http://baidu.com:80/cache/sethelp/help.html

熔断和重试相关的过滤器

Retry 过滤器

网关作为所有请求流量的入口,网关对路由进行协议适配和协议转发处理的过程中,如果出现异常或网络抖动,为了保证后端服务请求的高可用,一般处理方式会对网络请求进行重试,接口必须需要做幂等处理。config.setRetries(2).setStatuses(HttpStatus.INTERNAL_SERVER_ERROR) 表示设置重试次数为两次,当服务调用失败时设置返回的状态码为 500,即服务器内部错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class CommonConfiguration {

@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("retry_route", r -> r.path("/test/retry")
.filters(f -> f.retry(config -> config.setRetries(2)
.setStatuses(HttpStatus.INTERNAL_SERVER_ERROR)))
.uri("http://127.0.0.1:8080/retry?key=abc&count=2"))
.build();
}
}

特别注意

  • Retry 过滤器的使用必须小心谨慎,因为它有很多需要注意的使用细节,详细使用说明请看 官方文档
  • 值得一提的是,在 Spring Cloud 的生态技术中,除了 Gateway 之外,失败重试的技术实现还有:OpenFeign、Hystrix、Resilience4j、Sentinel、Spring Retry 等。

Hystrix 过滤器

Hystrix 可以提供熔断、服务降级和快速失败等功能。Spring Cloud Gateway 对 Hystrix 进行集成提供路由层面的服务熔断和降级,最简单的使用场景是当通过 Spring Cloud Gateway 调用后端服务,后端服务一直出现异常、服务不可用的状态。此时为了提高用户体验,就需要对服务降级,返回友好的提示信息给服务消费者,在保护网关自身可用的同时保护后端服务高可用。下面将给出配置示例,由于篇幅有限,只列出核心的配置内容和代码。

特别注意

  • 截止 2024 年 1 月,在较新版本中的 Gateway(如 4.1.0)中,Hystrix 过滤器已被移除。
  • 移除原因是 Hystrix 项目已经停更,Spring Cloud Gateway 官方推荐使用 Resilience4J 来替代 Hystrix。
  • 若需要在 Gateway 中实现熔断、降级等功能,可以使用 Spring Cloud Gateway 官方提供 Spring Cloud CircuitBreaker 过滤器,详细使用说明请看 官方文档
  • 工程里的 pom.xml 配置文件,引入 spring-cloud-starter-gatewayspring-cloud-starter-netflix-hystrix
1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
  • 工程里的启动主类
1
2
3
4
5
6
7
@SpringBootApplication
public class GatewayApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
  • 工程里的 Fallback 控制器
1
2
3
4
5
6
7
8
@RestController
public class FallbackController {

@GetMapping("/fallback")
public String fallback() {
return "Spring Cloud Gateway Fallback!";
}
}
  • 工程里的 application.xml 配置文件,添加 Hystrix 过滤器相关的配置,并设置 Hystrix 的 fallbackcmd 的超时时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: hystrix_route
predicates:
- Path=/test/hystrix
filters:
- name: Hystrix # Hystrix Filter 的名称
args: # Hystrix 配置参数
name: fallbackcmd # HystrixCommand 的名字
fallbackUri: forward:/fallback # fallback 对应的 uri
uri: http://127.0.0.1:8080/hystrix?isSleep=false

hystrix:
command:
fallbackcmd:
execution:
isolation:
thread:
timeoutInMilliseconds: 5000 # Hystrix 的 fallbackcmd 的超时时间

默认的过滤器(全局生效)

Gateway 提供了 Default Filters,当过滤器配置在 spring.cloud.gateway.default-filters 后,效果相当于该过滤器全局生效(即作用于所有路由)。YML 的配置示例如下:

1
2
3
4
5
6
spring:
cloud:
gateway:
default-filters:
- AddResponseHeader=X-Response-Default-Red, Default-Blue
- PrefixPath=/pay