1 什么是 Spring Cloud Gateway?
Spring Cloud Gateway 是 Spring Cloud 生态系统中的网关,用于替代 zuul。 zuul 基于 Servlet,使用阻塞 API 且不支持长连接(WebSockets),而 Spring Cloud Gateway 使用非阻塞 API,支持 WebSockets。
Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,不仅旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式,而且基于 Filter 链的方式提供一系列网关基本功能,如安全,监控,限流等。
2 Spring Cloud Gateway 的网关路由
Spring Cloud Gateway 的网关路由可以通过配置文件或者代码(自定义 RouteLocator)来进行配置,我们一般使用配置文件进行网关路由的配置。
2.1 一个最简单的网关路由
我们新建一个项目,命名为 Gateway ,其 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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>Gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Gateway</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件如下:
server:
port: 8080
spring:
cloud:
gateway:
routes:
#自定义的路由ID,是唯一的
- id: gateway-test
#目标服务地址
uri: https://blog.csdn.net
#路由条件
predicates:
- Path=/nav/java
我们在配置文件中配置了一个 id 为 gateway-test 的路由规则,当我们访问 http://localhost:8080/nav/java 时会自动转发至 https://blog.csdn.net/nav/java。打开项目,在浏览器进行测试,结果如下,证明转发成功。
2.2 Predicate
Predicate 是 Java 8 引入的一个函数,它接受一个输入参数,返回一个布尔值结果,可以用来接口请求参数校验等操作。
Spring Cloud Gateway 利用了 Predicate 的特性实现了各种路由匹配规则,比如通过请求参数等,用来作为条件匹配到对应的路由。下面的图片总结了 Spring Cloud 内置的几种 Predicate 的实现。
Predicate 实现了匹配规则,在接收到请求之后,会找到相应的路由进行处理。我们接下来将介绍几种 Spring Cloud GateWay 内置的 Predicate 的使用。
2.2.1 通过时间匹配
server:
port: 8080
spring:
cloud:
gateway:
routes:
#自定义的路由ID,是唯一的
- id: gateway-test
#目标服务地址
uri: https://blog.csdn.net
#路由条件
predicates:
- Between=2019-01-01T00:00:00+08:00[Asia/Shanghai], 2020-01-01T00:00:00+08:00[Asia/Shanghai]
Predicate 可以支持我们设置一个时间段,在请求转发时,可以判断现在是否处于这个时间段,只有处于这个时间段才能匹配到路由。
我们把时间段设置为 2019 年全年,然后在浏览器输入 http://localhost:8080/,发现确实转发到了 csdn 首页。
2.2.2 通过请求方式匹配
我们可以通过 POST、GET 等不同的请求方式来进行路由。
server:
port: 8080
spring:
cloud:
gateway:
routes:
#自定义的路由ID,是唯一的
- id: gateway-test
#目标服务地址
uri: https://blog.csdn.net
#路由条件
predicates:
- Method=GET
我们设置只有使用 get 方式才能进行路由。在浏览器输入 http://localhost:8080/,由于浏览器使用 get 方式去请求,发现确实可以返回网页,证明匹配到路由,我们再使用 post 方式进行测试,发现无法匹配。
2.2.3 通过请求路径匹配
通过接收一个匹配路径的参数来判断是否进行路由。
server:
port: 8080
spring:
cloud:
gateway:
routes:
#自定义的路由ID,是唯一的
- id: gateway-test
#目标服务地址
uri: https://blog.csdn.net
#路由条件
predicates:
- Path=/nav/{segment}
如果请求路径符合要求,那么路由将匹配,例如 /nav/java,/nav/python。 我们测试一下:
输入 http://localhost:8080/nav/java 可以正确访问页面
输入 http://localhost:8080/Geffin 时返回404
2.2.4 将各种 Predicate 组合使用
上面对于 Predicate 的使用我们都是进行单个测试,但事实上我们可以将各种 Predicate 组合使用。
server:
port: 8080
spring:
cloud:
gateway:
routes:
#自定义的路由ID,是唯一的
- id: gateway-test
#目标服务地址
uri: https://blog.csdn.net
#路由条件
predicates:
- Path=/nav/{segment}
- Method=GET
- Between=2019-01-01T00:00:00+08:00[Asia/Shanghai], 2020-01-01T00:00:00+08:00[Asia/Shanghai]
当多个 Predicate 存在于同一个路由时,请求需要满足全部条件才能被路由匹配。需要注意的是,当一个请求满足多个路由的条件时,请求只会被首个成功匹配的路由转发。
3 Spring Cloud Gateway 的服务化
我们上面介绍了 Spring Cloud Gateway 作为 API 网关如何代理单个服务,但在实际开发中,我们不可能为每个服务单独配置。服务的相互调用都依赖于服务中心,事实上,只要将 Spring Cloud Gateway 注册到服务中心,它就会默认代理服务中心的所有服务。
继续修改 Gateway 项目,增加 eureka 的依赖,同时在启动类上添加 eureka 的注解,将其注册到服务中心。
配置文件修改如下:
server:
port: 8089
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
# 通过服务中心根据serviceId创建路由
enabled: true
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
我们同时启动 eureka ,eurekaProducer,Gateway 三个项目,先打开 http://localhost:8761/ 查看服务注册情况。
eurekaProducer,Gateway 已经被注册至服务中心。
我们先测试 eurekaProducer 的功能,在浏览器输入 http://localhost:8080/helloWorld?id=3&saying=s,发现其返回如下,证明该服务一切正常。
来自8080端口:序号为3的用户发布了一条新消息:s
我们接下来通过网关来访问 eurekaProducer ,浏览器输入 http://localhost:8089/PRODUCER/helloWorld?id=3&saying=OO,发现其返回如下,证明网关功能实现正常。
来自8080端口:序号为3的用户发布了一条新消息:OO
4 Spring Cloud Gateway 中的过滤器
在我之前讲解 Zuul 的博客中已经介绍过 Zuul 的过滤器,与 Zuul 相似,Spring Cloud Gateway 也包含过滤器,不过只有两种:
- PRE:在请求被路由之前调用,可以用于实现权限验证等功能。
- POST:在路由到微服务以后执行,可以用于实现统计信息等功能。
Spring Cloud Gateway 也可以根据过滤器是否会应用到所有的路由上分为两类,GlobalFilter 会应用到所有的路由上,而 GatewayFilter 只会应用到单个路由或者一个分组的路由上。
有时候我们可以使用 Spring Cloud Gateway 的过滤器完成一些具体的路由配置。下面我们使用 AddRequestParameter GatewayFilter 进行演示,该过滤器的功能为在请求中添加指定参数。
修改我们的配置文件如下:
server:
port: 8089
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: test-filter
uri: lb://PRODUCER
filters:
- AddRequestParameter=id,11
predicates:
- Method=GET
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
这样会给匹配的每个请求添加上 id=11 的参数和值。我们启动 eureka ,eurekaProducer,Gateway 三个项目,访问 http://localhost:8089/helloWorld?saying=a,发现其返回:
来自8080端口:序号为11的用户发布了一条新消息:a
我们并没有传入 id 的值,但服务却接收到 id = 11,这说明网关在转发的过程中已经通过过滤器添加了设置的参数和值。
注意,当路由配置中 uri 所用的协议为 lb 时,Spring Cloud Gateway 会将项目名通过 eureka 解析为实际的主机和端口。
5 Spring Cloud Gateway 中的限流功能
在开发高并发系统时,我们有三把利器用来保护系统,分别是缓存、降级和限流。API 网关作为所有请求的入口,请求量大,我们可以通过对并发访问的请求进行限速来保护系统的可用性。
Spring Cloud Gateway 默认集成了Redis 限流,可以对不同服务做不同维度的限流,例如接口限流,IP 限流等。首先我们添加 Redis 依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
配置文件修改如下,这里注意,filter 名称必须是 RequestRateLimiter
server:
port: 8089
spring:
application:
name: gateway
redis:
host: localhost
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: test-redis
uri: lb://PRODUCER
filters:
- name: RequestRateLimiter
args:
# 允许用户每秒处理多少个请求
redis-rate-limiter.replenishRate: 10
# 令牌桶的容量,允许在一秒钟内完成的最大请求数
redis-rate-limiter.burstCapacity: 20
# 使用 SpEL 按名称引用 bean
key-resolver: "#{@userKeyResolver}"
predicates:
- Method=GET
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
然后我们需要创建一个配置类
package com.example.Gateway;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* 路由限流配置
* @author 30309
*
*/
@Configuration
public class Config {
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
}
我们目前的限流方案为根据请求参数中的 user 字段来限流,现在已实现完毕。
网关可以根据不同策略对请求进行限流,当然我们也可以使用别的方案限流,比如 IP 限流
package com.example.Gateway;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* 路由限流配置
* @author 30309
*
*/
@Configuration
public class Config {
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
使用接口限流,我们需要获取请求地址的 uri 作为限流 key
package com.example.Gateway;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* 路由限流配置
* @author 30309
*
*/
@Configuration
public class Config {
@Bean
KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
}
6 Spring Cloud Gateway 中的熔断功能
Spring Cloud Gateway 可以利用 Hystrix 的熔断特性,在流量过大时进行服务降级。首先我们添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
配置文件修改如下:
server:
port: 8089
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: test-Hystrix
uri: lb://PRODUCER
predicates:
- Method=GET
filters:
- name: Hystrix
args:
name: fallbackcmd
# fallback的路径
fallbackUri: forward:/fallbackTest
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
在发生熔断时,Hystrix 的 fallback 会被调用,请求将转发到 /fallbackTest。
我们再写一个控制器。
package com.example.Gateway;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@RequestMapping("/fallbackTest")
public String fallbackTest(int id,String saying) {
return "fallback:id=" + id + " saying=" + saying;
}
}
我们测试一下 Spring Cloud Gateway 中的熔断功能,启动 eureka ,eurekaProducer,Gateway 三个项目,在浏览器输入 http://localhost:8089/helloWorld?id=3&saying=a,发现可以正常返回。
来自8080端口:序号为3的用户发布了一条新消息:a
然后我们关闭 eurekaProducer 项目,再次访问 http://localhost:8089/helloWorld?id=3&saying=a,发现其返回
fallback:id=3 saying=a
说明 Spring Cloud Gateway 中的熔断功能实现成功。
7 Spring Cloud Gateway 中的重试功能
配置文件修改如下,要实现重试功能,我们需要使用 RetryGatewayFilter
server:
port: 8089
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: test-retry
uri: lb://PRODUCER
predicates:
- Method=GET
filters:
- name: Retry
args:
# 重试次数
retries: 3
# HTTP 的状态返回码
statuses: BAD_GATEWAY
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
参考:springcloud(十五):服务网关 Spring Cloud GateWay 入门
springcloud(十六):服务网关 Spring Cloud GateWay 服务化和过滤器
springcloud(十七):服务网关 Spring Cloud GateWay 熔断、限流、重试