Spring Cloud Gateway 新一代 API 网关服务

彭楷淳发布于 2021-02-12
预计阅读时间 14 分钟
总计 3.2k
浏览

Spring Cloud Gateway 为 SpringBoot 应用提供了 API 网关支持,具有强大的智能路由与过滤器功能,本文将对其用法进行详细介绍。

Gateway 简介


Gateway 是在 Spring 生态系统之上构建的 API 网关服务,基于 Spring 5,Spring Boot 2 和 Project Reactor 等技术。Gateway 旨在提供一种简单而有效的方式来对 API 进行路由,以及提供一些强大的过滤器功能, 例如:熔断、限流、重试等。

Spring Cloud Gateway 具有如下特性:

  • 基于 Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建;
  • 动态路由:能够匹配任何请求属性;
  • 可以对路由指定 Predicate(断言)和 Filter(过滤器);
  • 集成 Hystrix 的断路器功能;
  • 集成 Spring Cloud 服务发现功能;
  • 易于编写的 Predicate(断言)和 Filter(过滤器);
  • 请求限流功能;
  • 支持路径重写。

相关概念


  • Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由;
  • Predicate(断言):指的是 Java 8 的 Function Predicate。 输入类型是 Spring 框架中的 ServerWebExchange。 这使开发人员可以匹配 HTTP 请求中的所有内容,例如请求头或请求参数。如果请求与断言相匹配,则进行路由;
  • Filter(过滤器):指的是 Spring 框架中 GatewayFilter 的实例,使用过滤器,可以在请求被路由前后对请求进行修改。

创建项目模块


这里我们创建一个 api-gateway 模块来演示 Gateway 的常用功能。

引入依赖

在 pom.xml 中添加相关依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

相关配置

Gateway 提供了两种不同的方式用于配置路由,一种是通过 yml 文件来配置,另一种是通过 Java Bean 来配置,下面我们分别介绍下。

方式一:使用 yml 配置

在 application.yml 中进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 9201
service-url:
user-service: http://localhost:8201
spring:
cloud:
gateway:
routes:
- id: path_route #路由的ID
uri: ${service-url.user-service}/user/{id} #匹配后路由地址
predicates: # 断言,路径相匹配的进行路由
- Path=/user/{id}

启动 eureka-server,user-service 和 api-gateway 服务,并调用该地址测试:http://localhost:9201/user/1,我们发现该请求被路由到了 user-service 的该路径上:http://localhost:8201/user/1

方式二:使用 Java Bean 配置

添加相关配置类,并配置一个 RouteLocator 对象:

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

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route2", r -> r.path("/user/getByUsername")
.uri("http://localhost:8201/user/getByUsername"))
.build();
}
}

重新启动 api-gateway 服务,并调用该地址测试:http://localhost:9201/user/getByUsername?username=antonio,我们发现该请求被路由到了 user-service 的该路径上:http://localhost:8201/user/getByUsername?username=antonio

Route Predicate 的使用


Spring Cloud Gateway 将路由匹配作为 Spring WebFlux HandlerMapping 基础架构的一部分。 Spring Cloud Gateway 包括许多内置的 Route Predicate 工厂。 所有这些 Predicate 都与 HTTP 请求的不同属性匹配。 多个 Route Predicate 工厂可以进行组合,下面我们来介绍下一些常用的 Route Predicate。

注意:Predicate 中提到的配置都在 application-predicate.yml 文件中进行修改,并用该配置启动 api-gateway 服务。

After Route Predicate

在指定时间之后的请求会匹配该路由。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: ${service-url.user-service}
predicates:
- After=2019-09-24T16:30:00+08:00[Asia/Shanghai]

Before Route Predicate

在指定时间之前的请求会匹配该路由。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: before_route
uri: ${service-url.user-service}
predicates:
- Before=2019-09-24T16:30:00+08:00[Asia/Shanghai]

Between Route Predicate

在指定时间区间内的请求会匹配该路由。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: before_route
uri: ${service-url.user-service}
predicates:
- Between=2019-09-24T16:30:00+08:00[Asia/Shanghai], 2019-09-25T16:30:00+08:00[Asia/Shanghai]

带有指定 Cookie 的请求会匹配该路由。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: ${service-url.user-service}
predicates:
- Cookie=username,antonio

使用 curl 工具发送带有 Cookie 为 username=antonio 的请求可以匹配该路由。

1
$ curl http://localhost:9201/user/1 --cookie "username=antonio"

Header Route Predicate

带有指定请求头的请求会匹配该路由。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: header_route
uri: ${service-url.user-service}
predicates:
- Header=X-Request-Id, \d+

使用 curl 工具发送带有请求头为 X-Request-Id:123 的请求可以匹配该路由。

1
$ curl http://localhost:9201/user/1 -H "X-Request-Id:123"

Host Route Predicate

带有指定 Host 的请求会匹配该路由。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: host_route
uri: ${service-url.user-service}
predicates:
- Host=**.antoniopeng.com

使用 curl 工具发送带有请求头为 Host:www.antoniopeng.com 的请求可以匹配该路由。

1
$ curl http://localhost:9201/user/1 -H "Host:www.antoniopeng.com"

Method Route Predicate

发送指定方法的请求会匹配该路由。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: method_route
uri: ${service-url.user-service}
predicates:
- Method=GET

使用 curl 工具发送 GET 请求可以匹配该路由。

1
$ curl http://localhost:9201/user/1

使用 curl 工具发送 POST 请求无法匹配该路由。

1
$ curl -X POST http://localhost:9201/user/1

Path Route Predicate

发送指定路径的请求会匹配该路由。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: path_route
uri: ${service-url.user-service}/user/{id}
predicates:
- Path=/user/{id}

使用 curl 工具发送 /user/1 路径请求可以匹配该路由。

1
$ curl http://localhost:9201/user/1

使用 curl 工具发送 /abc/1 路径请求无法匹配该路由。

1
$ curl http://localhost:9201/abc/1

Query Route Predicate

带指定查询参数的请求可以匹配该路由。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: query_route
uri: ${service-url.user-service}/user/getByUsername
predicates:
- Query=username

使用 curl 工具发送带 username=antonio 查询参数的请求可以匹配该路由。

1
$ curl http://localhost:9201/user/getByUsername?username=antonio

使用 curl 工具发送带不带查询参数的请求无法匹配该路由。

1
$ curl http://localhost:9201/user/getByUsername

RemoteAddr Route Predicate

从指定远程地址发起的请求可以匹配该路由。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: ${service-url.user-service}
predicates:
- RemoteAddr=192.168.1.1/24

使用 curl 工具从 192.168.1.1 发起请求可以匹配该路由。

1
$ curl http://localhost:9201/user/1

Weight Route Predicate

使用权重来路由相应请求,以下表示有 80% 的请求会被路由到 localhost:8201,20% 会被路由到 localhost:8202。

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: weight_high
uri: http://localhost:8201
predicates:
- Weight=group1, 8
- id: weight_low
uri: http://localhost:8202
predicates:
- Weight=group1, 2

Route Filter 的使用


路由过滤器可用于修改进入的 HTTP 请求和返回的 HTTP 响应,路由过滤器只能指定路由进行使用。Spring Cloud Gateway 内置了多种路由过滤器,他们都由GatewayFilter 的工厂类来产生,下面我们介绍下常用路由过滤器的用法。

AddRequestParameter GatewayFilter

给请求添加参数的过滤器。

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: http://localhost:8201
filters:
- AddRequestParameter=username, antonio
predicates:
- Method=GET

以上配置会对 GET 请求添加 username=antonio 的请求参数,通过 curl 工具使用以下命令进行测试。

1
$ curl http://localhost:9201/user/getByUsername

相当于发起该请求:

1
$ curl http://localhost:8201/user/getByUsername?username=antonio

StripPrefix GatewayFilter

对指定数量的路径前缀进行去除的过滤器。

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: strip_prefix_route
uri: http://localhost:8201
predicates:
- Path=/user-service/**
filters:
- StripPrefix=2

以上配置会把以 /user-service/ 开头的请求的路径去除两位,通过 curl 工具使用以下命令进行测试。

1
$ curl http://localhost:9201/user-service/a/user/1

相当于发起该请求:

1
$ curl http://localhost:8201/user/1

PrefixPath GatewayFilter

与 StripPrefix 过滤器恰好相反,会对原有路径进行增加操作的过滤器。

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: prefix_path_route
uri: http://localhost:8201
predicates:
- Method=GET
filters:
- PrefixPath=/user

以上配置会对所有 GET 请求添加 /user 路径前缀,通过 curl 工具使用以下命令进行测试。

1
$ curl http://localhost:9201/1

相当于发起该请求:

1
$ curl http://localhost:8201/user/1

Hystrix GatewayFilter

Hystrix 过滤器允许你将断路器功能添加到网关路由中,使你的服务免受级联故障的影响,并提供服务降级处理。

要开启断路器功能,我们需要在 pom.xml 中添加 Hystrix 的相关依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

然后添加相关服务降级的处理类:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class FallbackController {

@GetMapping("/fallback")
public Object fallback() {
Map<String,Object> result = new HashMap<>();
result.put("data",null);
result.put("message","Get request fallback!");
result.put("code",500);
return result;
}
}

在 application-filter.yml 中添加相关配置,当路由出错时会转发到服务降级处理的控制器上:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
gateway:
routes:
- id: hystrix_route
uri: http://localhost:8201
predicates:
- Method=GET
filters:
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/fallback

关闭 user-service,调用该地址进行测试:http://localhost:9201/user/1,发现已经返回了服务降级的处理信息。

RequestRateLimiter GatewayFilter

RequestRateLimiter 过滤器可以用于限流,使用 RateLimiter 实现来确定是否允许当前请求继续进行,如果请求太大默认会返回 HTTP 429-太多请求状态。

在 pom.xml 中添加相关依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

添加限流策略的配置类,这里有两种策略一种是根据请求参数中的 username 进行限流,另一种是根据访问IP进行限流:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class RedisRateLimiterConfig {
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("username"));
}

@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}

我们使用 Redis 来进行限流,所以需要添加 Redis 和 RequestRateLimiter 的配置,这里对所有的 GET 请求都进行了按 IP 来限流的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server:
port: 9201
spring:
redis:
host: localhost
password: 123456
port: 6379
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: http://localhost:8201
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 #每秒允许处理的请求数量
redis-rate-limiter.burstCapacity: 2 #每秒最大处理的请求数量
key-resolver: "#{@ipKeyResolver}" #限流策略,对应策略的Bean
predicates:
- Method=GET
logging:
level:
org.springframework.cloud.gateway: debug

多次请求该地址:http://localhost:9201/user/1,会返回状态码为 429 的错误:

Retry GatewayFilter

对路由请求进行重试的过滤器,可以根据路由请求返回的HTTP状态码来确定是否进行重试。

修改 application.yaml 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
cloud:
gateway:
routes:
- id: retry_route
uri: http://localhost:8201
predicates:
- Method=GET
filters:
- name: Retry
args:
retries: 1 #需要进行重试的次数
statuses: BAD_GATEWAY #返回哪个状态码需要进行重试,返回状态码为5XX进行重试
backoff:
firstBackoff: 10ms
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false

当调用返回 500 时会进行重试,访问测试地址:http://localhost:9201/user/111,可以发现 user-service 控制台报错 2 次,说明进行了一次重试。

1
2
3
4
2019-10-27 14:08:53.435 ERROR 2280 --- [nio-8201-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException] with root cause

java.lang.NullPointerException: null
at com.antonio.cloud.controller.UserController.getUser(UserController.java:34) ~[classes/:na]

结合注册中心使用


我们上次讲到使用 Zuul 作为网关结合注册中心进行使用时,默认情况下 Zuul 会根据注册中心注册的服务列表,以服务名为路径创建动态路由,Gateway 同样也实现了该功能。下面我们演示下 Gateway 结合注册中心如何使用默认的动态路由和过滤器。

使用动态路由

在 pom.xml 中添加相关依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

添加 application-eureka.yml 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server:
port: 9201
spring:
application:
name: api-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能
lower-case-service-id: true #使用小写服务名,默认是大写
eureka:
client:
service-url:
defaultZone: http://localhost:8001/eureka/
logging:
level:
org.springframework.cloud.gateway: debug

使用 application-eureka.yml 配置文件启动 api-gateway 服务,访问 http://localhost:9201/user-service/user/1,可以路由到 user-service 的 http://localhost:8201/user/1 处。

使用过滤器

在结合注册中心使用过滤器的时候,我们需要注意的是 uri 的协议为 lb,这样才能启用 Gateway 的负载均衡功能。

修改 application-eureka.yml 文件,使用了 PrefixPath 过滤器,会为所有 GET 请求路径添加 /user 路径并路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server:
port: 9201
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: prefixpath_route
uri: lb://user-service #此处需要使用lb协议
predicates:
- Method=GET
filters:
- PrefixPath=/user
discovery:
locator:
enabled: true
eureka:
client:
service-url:
defaultZone: http://localhost:8001/eureka/
logging:
level:
org.springframework.cloud.gateway: debug

使用 application-eureka.yml 配置文件启动 api-gateway 服务,访问 http://localhost:9201/1,可以路由到 user-service 的 http://localhost:8201/user/1 处。

更多干货请移步:https://antoniopeng.com


如果你喜欢这个博客或发现它对你有用,欢迎你点击右下角 “OPEN CHAT” 进行评论。也欢迎你分享这个博客,让更多的人参与进来。如果在博客中使用的图片侵犯了您的版权,请联系博主删除它们。谢谢你!