网关 Spring Cloud Gateway - API 调用的组织者
作者:行百里er
博客:https://chendapeng.cn (opens new window)
提示
这里是 行百里er 的博客:行百里者半九十,凡事善始善终,吾将上下而求索!
# 引言:网关为何而生?
三皇五帝时期,中原洪水泛滥,大禹率领民众,对洪水进行疏导,使每个水系都有各自的流向,最终完成了治水大业。
我在玩《穹之扉》水坝机关这里的时候,搞了好久才完成,每个机关控制各自水道的流向,最终只要每条水道流通就能完成了。
言归正传,在一个错综复杂的大型微服务系统里,各个服务间的 API 调用将是一个巨大的考验,每个调用者都得在记录每个微服务的地址再分别去调用,还有服务认证问题、跨域问题等等。
如果有一个类似于疏通水系的中间件,每个客户端调用都从它这里走,而它能够统一指挥调度请求的流向,那 API 请求的问题将会变得清晰、简单、高效!
网关 就为此而生了。
网关还可以隐藏服务名称、限流以及许多其他有用的事情。
# Spring Cloud Gateway
Spring Cloud Gateway
是网关的一种,它可精确控制 API 层,集成 Spring Cloud 服务发现和客户端负载均衡解决方案,以简化配置和维护。
Spring Cloud Gateway
不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如: 安全 ,监控 和 限流 。
# 核心概念
# Route
Route 是网关的基础元素,表示一个具体的路由信息。当请求到达网关时,由 Gateway Handler Mapping 通过 断言
进行路由匹配,就是 Mapping , 当断言为真时,匹配的路由。
路由有以下几个部分组成:
id
:路由的标识,唯一,区别于其他路由;uri
:目标 uri ,客户端的请求被最终转发到的目的地址;order
:多个 route 之间的排序,数值越小,匹配优先级越高;predicate
:断言,就是路由的匹配条件,其作用是进行条件判断,当断言为真时,才会执行真正的路由;filter
:过滤器,可以在请求发出的前后进行一些业务上的处理。
# Predicate
前面说过,断言 predicate
是 Route 的组成部分之一。
Predicate 是 Java 8 中提供的一个函数:
Predicate 函数式接口的主要作用就是提供一个 test(T t)
方法,接受一个参数返回一个布尔类型,Predicate 在进行一些判断的时候非常常用。
在 Gateway 中,输入类型是 ServerWebExchange
,它可以让开发人员匹配来自 HTTP 的请求,比如 请求头
或者 请求参数
。简而言之,它就是匹配条件。
# Filter
Filter
是 Gateway 中的过滤器,可以在请求发出的前后做一些业务上的处理。
将以上三个核心点连起来看,当用户发出请求到达 Gateway ,Gateway 会通过一些匹配条件,定位到真正的服务节点,并在这个转发过程前后,进行一些及细化控制。其中 Predicate
就是我们匹配的条件,而 Filter
可以理解为一个拦截器,有了这两个点,再加上目标 uri
,就可以实现一个具体的路由了。
# 工作原理
来看一下, Spring Cloud Gateway 的工作原理图:
客户端向 Spring Cloud Gateway 发出请求,如果请求与网关程序定义的路由匹配,则该请求就会被发送到网管 Web 处理程序,此时处理程序运行特定的请求过滤器链。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求的前后执行逻辑。所有 pre 过滤器的逻辑先执行,然后执行代理请求,代理请求完成后,执行 post 过滤器逻辑。
具体的执行流程:
- Gateway Client 向 Gateway Server 发送请求;
- 请求首先会被
HttpWebHandlerAdapter
进行提取组装成网关上下文; - 然后网关的上下文会传递到
DispatcherHandler
,它负责将请求分发给RoutePredicateHandlerMapping
; RoutePredicateHandlerMapping
负责路由查找,并根据路由断言判断路由是否可用;- 如果过断言成功,由
FilteringWebHandler
创建过滤器链并调用; - 请求会一次经过 PreFilter--微服务--PostFilter 的方法,最终返回响应。
# Spring Cloud Gateway 网关项目演练
# 创建网关服务
创建一个 Module gateway-service
,该 Module 引入 spring-cloud-starter-gateway
的依赖,并加入父级项目:
<!-- 父级模块 -->
<parent>
<groupId>cn.chendapeng</groupId>
<artifactId>SpringCloudAlibabaDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath/>
</parent>
<dependencies>
<!-- Nacos 服务注册与发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 网关 Spring Cloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
网关服务的配置文件 application.yml
:
server:
port: 8000
spring:
application:
name: gateway-service
cloud:
nacos:
discovery:
# Nacos 集群,服务注册与发现
server-addr: 192.168.242.112:81
gateway:
# 路由列表
routes:
- id: user-service
uri: http://localhost:8001
predicates:
- Path=/user/**
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这里我们主要看一下 gateway
的配置,前面已经分析过,gateway
的核心就是 routes
及其组成:id,uri,predicate。
其中断言 Predicates 可以由多个条件组成,比如上面的配置 Path=/user/**
就是匹配条件的一种:根据路径的正则表达式匹配。当然还可以配置多个匹配条件,当同时满足 Predicates 下的匹配条件才会进行路由转发。
如下配置:
其含义是,当访问网关服务 http://localhost:8000/user/**
的时候就会转发到 http://localhost:8001/user/**
。
Tip :这里的 http://localhost:8001
是另外一个微服务项目 user-service
,提供了接口 /user/info/{id}
,代码位于:https://github.com/ChenDapengJava/SpringCloudAlibabaDemo/tree/master/user-service (opens new window) 。
下面我们来验证一下,首先看 user-service
本身的接口是否能正确访问 :
下面,我们不直接访问 user-service
了,改为通过网关访问,我们网关服务的端口是 8000 ,根据配置的路由规则,访问地址改为:http://localhost:8000/user/info/2
,访问结果:
网关生效!
# 负载均衡
其实我的 user-service
有两个服务在运行:
在 Nacos 注册中心也可以看到有2个 user-service
的实例:
从这里可以看出,我们前面配置网关的路由的 uri 并没有对 user-service
服务进行负载均衡的访问,而是固定的访问 8001 这个实例,那么 Spring Cloud Gateway 能做负载均衡吗?肯定能啊,毕竟这一小节的标题就是 负载均衡 。
Gateway
有自带的负载均衡,也可以通过 routes
配置负载均衡。
前排重要提示 :由于本系列使用的 Spring Cloud 版本为 2021.0.1
,其一些组件底层使用的负载均衡默认移除了 Ribbon ,而是默认支持 Spring Cloud LoadBalancer ,但是使用的时候需要引入需要引入依赖:
<!-- 负载均衡 loadbalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2
3
4
5
# Spring Cloud Gateway 自动负载均衡
我们知道,在 OpenFeign
中,只要在 @FeignClient
注解添加 name
属性为服务名,就可根据服务名自动进行负载均衡访问,这样的好处是当一个服务增加实例的时候,不用重新配置和重启。
在 Spring Cloud Gateway 中也有类似的功能,通过配置 spring.cloud.gateway.discovery.loacator.enabled=true
来开启,完整的配置文件如下:
server:
port: 8000
spring:
application:
name: gateway-service
cloud:
nacos:
discovery:
# Nacos 集群
server-addr: 192.168.242.112:81
gateway:
discovery:
locator:
enabled: true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
开启之后,可以看到,这里 没有配置路由 。可以通过地址去访问服务了,地址格式如下:
http://网关地址/服务名称/**
http://localhost:8000/user-service/user/info/1
2
测试网关负载均衡,为了方便观察,给返回的对象加个字段 serverPort
,调用 http://localhost:8000/user-service/user/info/1
进行测试:
这就是网关的负载均衡,通过配置开启,然后在访问的时候加上对应服务的服务名即可。
# 手动配置负载均衡
不知道大家发现没有,通过 Spring Cloud Gateway 的自动负载均衡,访问的时候地址必须加上 服务名称 才可以,这样就暴露了服务名称。
在实际使用的时候,我们一般不配置 spring.cloud.gateway.discovery.loacator.enabled
,该配置项默认就是 false ,由我们自己在路由配置上进行一些设置,也能实现负载均衡。来看一下具体配置。
server:
port: 8000
spring:
application:
name: gateway-service
cloud:
nacos:
discovery:
# Nacos 集群
server-addr: 192.168.242.112:81
# 配置项 spring.cloud.gateway.discovery.locator.enabled 默认为 false
# 如果该配置项设置为 true,则可以根据地址 http://网关地址/服务名称/** 进行访问,且自动负载均衡
# 如果网关配置了 routes,并且配置了 lb 负载均衡,那么可以不加服务名称就可访问,为了不暴露服务名称,可将此选项设置为false(或者不配置,默认就是false)
gateway:
discovery:
locator:
enabled: true
# 配置网关路由
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
配置负载均衡的关键就是路由的 uri
以 lb://
开头,后面接需要转发到的服务名称,这个服务名称必须和注册到 Nacos 中的对应,否则会找不到服务。
测试结果:
同样能实现负载均衡,并且不用暴露服务名称。
# 小结
- 配置项
spring.cloud.gateway.discovery.locator.enabled
默认为 false; - 如果该配置项设置为 true,则可以根据地址
http://网关地址/服务名称/**
进行访问,且自动负载均衡; - 如果网关配置了
routes
,并且uri
配置了lb://
负载均衡,那么可以不加服务名称就可访问,为了不暴露服务名称,可将此选项设置为 false(或者不配置,默认就是false)
# 路由配置
# 两种配置方式
方式1, 就是前面 demo 中的使用方式,在 application.yml
配置文件中设置:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
2
3
4
5
6
7
8
方式2, 使用 @Bean 注入 RouteLocator :
@Configuration
public class GatewayRouteConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/user/**")
// .filter(xxx)
.uri("lb://user-service")
)
.route(r -> r.path("/order/**")
.uri("lb://order-service")
)
.build();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
application.yml 配置文件里不配置路由,通过该配置类配置。
这里我又加了一个服务 order-service
,并向 Nacos 注册了两个实例,现在测试一下:
通过配置类注入 RouteLocator 的方式也能正确访问。
但是通过配置文件不香吗,还要写代码来配置路由?所以我们一般使用配置文件进行路由配置。
# 断言 Predicate
通过前文的操练,现在基本已经可以使用 Gateway 对 API 进行有条理的调用了。但是对于 Gateway 的使用远不止这些,之前我们只配置了 routes
的 Path 断言,他还有很多其他实用的断言工厂:
Spring 官网给出了 12 种断言工厂,下面介绍其中几个比较常用的。
# 1,Path
这种就是前面我们的使用方式:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
2
3
4
5
6
7
8
根据 Path 定义好的规则来判断访问的 uri
是否匹配。这里可以使用正则表达式来匹配多级 URL 。
# 2,Method
Method 路由断言工厂是要匹配 HTTP 的访问方式(GET、POST、DELETE等),配置方法:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
- Method=GET
2
3
4
5
6
7
8
9
predicates
这里为复数,说明可以配置多个断言,比如这里配置的就是 Path 和 Method 断言,他们之间是与的关系,即同时匹配上才会进行路由转发。
说回 Method 断言,这里配置的意思是必须满足 HTTP 请求的方式为 GET 才进行转发,否则直接提示 404:
# 3,Query
Query 断言工厂接收两个参数,一个必须的参数,一个可选的正则表达式,配置方法:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
- Method=GET
- Query=name, zhang.
- Query=age, \d+
2
3
4
5
6
7
8
9
10
11
这样配置的话,请求必须包含一个值与 zhang
匹配的 name
参数,并且包含一个值为任意数字的 age
参数,该路由才会匹配上。也就是说,如果需要匹配多个参数,这里可以写多个 Query 。
不加 name
参数,直接不匹配,404:
加上匹配的 name
和 age
参数:
# 4,Host
匹配当前请求是否来自于设置的主机,设置方法:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
- Host=**.chendapeng.cn
2
3
4
5
6
7
8
9
测试验证:
# 5,Header
匹配请求头,设置请求头名称的正则表达式:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
- Host=**.chendapeng.cn
- Header=X-Request-name, .+
2
3
4
5
6
7
8
9
10
测试:
# 6,After & Before & Between
这三个都是关于时间匹配的:
- After :当请求时间晚于设定的时间,路由才会匹配;
- Before :当请求早于设定的时间,路由才会匹配;
- Between :当请求在设定的时间之间,路由才会匹配。
这个一般用于指定项目功能上线时间,比如有个功能需要提前上线,上线后不想让用户请求到该功能,那么用 After 断言匹配路由就能实现。
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
- After=2022-08-26T23:29:28.831+08:00[Asia/Shanghai]
2
3
4
5
6
7
8
9
值得一提的是,这里的时间是 java ZonedDateTime
类型,可以用 ZonedDateTime time = ZonedDateTime.now();
这种方式获取到时间,填写到这里。
按照我这里的配置,在 2022-08-26 23:29:28.831
这个时间之前都访问不了。
# Spring Cloud Gateway 内置过滤器 Filter
Spring Cloud Gateway 还有内置的过滤器,Spring 官网上介绍了 30 多种过滤器,这里先介绍其中2种,后续再介绍其他比较有用的过滤器。
# 1,AddRequestHeader
看名字就知道,这个过滤器是用来添加请求头的,配置方法:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
filters:
- AddRequestHeader=X-Request-Home, China
2
3
4
5
6
7
8
9
10
这个完整的配置含义是,当断言匹配路由成功后,将通过 AddRequestHeader 过滤器工厂添加 X-Request-Home: China
请求头,将其传递到下游服务,该服务可以直接获取请求头信息。
请求调用:
可以看到后端服务能够获取过滤器添加的请求头信息:
# 2,RedirectTo
该过滤器用于重定向操作,当路由匹配时,将自动转发的配置的地址上,该配置的第一个参数是 300 系列的状态码,比如 302。配置方法:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
filters:
- RedirectTo=302, https://google.com
2
3
4
5
6
7
8
9
10
比如这个,请求将被转发到 google.com:
这里就演示这两个过滤器的使用,还有很多其他过滤器可参考官网:
除了内置的过滤器,我们还可以根据自己的业务自定义过滤器,使用也是很方便的。这里就先不聊了,下次准备聊聊自定义过滤器还有网关限流、熔断、跨域等功能。
首发公众号 行百里er ,欢迎各位关注阅读指正。