1. 概述
Zuul 是由 Netflix 开源的微服务网关,提供都动态路由、监控、熔断、安全等等功能。
Zuul is a gateway service that provides dynamic routing, monitoring, resiliency, security, and more.
Spring Cloud Netflix Zuul 将 Zuul 融入 Spring Cloud 生态体系,作为 Spring Cloud 微服务架构中的 API 网关。如下图所示:
拓展小知识:
Zuul 有 1.X 和 2.X 两个版本,前者基于同步阻塞模式的编程模型实现,后者基于异步非阻塞模式的编程模型实现。两者的对比,可以看看[《Zuul1 和 Zuul2 该如何选择?》](https://blog.csdn.net/yang75108/article/details/86991401 TODO)文章。
目前,Spring Cloud Netflix Zuul 采用的是 Zuul 1.X 版本。并且,Zuul 2.X 不会被集成到 Spring Cloud 中,具体可见 ISSUE#86。
可能因为 Spring Cloud 团队已经开源了 Spring Cloud Gateway 网关,而它和 Zuul 2.X 一样,也是基于异步非阻塞模式的编程模型实现。
所以胖友如果是新学 Spring Cloud 网关相关的内容,建议优先选择《芋道 Spring Cloud 网关 Spring Cloud Gateway 入门》。
2. 为什么使用网关?
胖友可以后续阅读如下文章:
🙂 先继续 Zuul 的入门。
3. 快速入门
示例代码对应仓库:
labx-21-sc-zuul-demo01
本小节我们来对 Zuul 进行快速入门。创建一个 labx-21-sc-zuul-demo01
项目,最终项目结构如下图:
3.1 引入依赖
创建 pom.xml
文件中,主要引入 Spring Cloud Gateway 相关依赖。代码如下:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>labx-21</artifactId>
<groupId>cn.iocoder.springboot.labs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>labx-21-sc-zuul-demo01</artifactId>
<properties>
<spring.boot.version>2.2.4.RELEASE</spring.boot.version>
<spring.cloud.version>Hoxton.SR1</spring.cloud.version>
<spring.cloud.alibaba.version>2.2.0.RELEASE</spring.cloud.alibaba.version>
</properties>
<!--
引入 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 引入 Spring Cloud Zuul 相关依赖,使用它作为网关,并实现对其的自动配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
</project>
在 spring-cloud-starter-netflix-zuul
依赖中,会引入 Zuul 1.X 等等依赖,如下图所示:
3.2 配置文件
创建 application.yaml
配置文件,添加 Spring Cloud Zuul 相关配置。配置如下:
server:
port: 8888
spring:
application:
name: zuul-application
# Zuul 配置项,对应 ZuulProperties 配置类
zuul:
servlet-path: / # ZuulServlet 匹配的路径,默认为 /zuul
# 路由配置项,对应 ZuulRoute 数组
routes:
route_yudaoyuanma:
path: /blog/**
url: http://www.iocoder.cn
route_oschina:
path: /oschina/**
url: https://www.oschina.net
① server.port
配置项,设置网关的服务器端口。
② zuul
配置项,Spring Cloud Zuul 配置项,对应 ZuulProperties 类。
servlet-path
配置项,设置 ZuulServlet 匹配的路径,默认为 /zuul
。Zuul 和 SpringMVC 一样,都是通过实现自定义的 Servlet,从而进行请求的转发。如下图所示:
routes
配置项,对应 ZuulRoute Map。其中 key 为路由编号,value 为路由具体配置:
path
:匹配的 URL 地址。url
:转发的 URL 地址。
这里,我们将以 /blog/**
开头的 URL 转发到 http://www.iocoder.cn,以 /oschina/**
开头的 URL 转发到 https://www.oschina.net。
3.3 ZuulApplication
创建 ZuulApplication 类,网关的启动类。代码如下:
@SpringBootApplication
@EnableZuulProxy // 开启 Zuul 网关
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
在类上,添加 @EnableZuulProxy
注解,声明开启 Zuul 网关功能。
3.4 简单测试
① 使用 ZuulApplication 启动网关。可以看到控制台打印 Spring Cloud Zuul 相关日志如下:
2020-05-03 18:16:26.568 INFO 44488 --- [ main] o.s.c.n.zuul.ZuulFilterInitializer : Starting filter initializer
② 使用浏览器,访问 http://127.0.0.1:8888/blog,成功转发到目标 URI http://www.iocoder.cn
,如下图所示:
③ 使用浏览器,访问 http://127.0.0.1:8888/oschina,成功转发到目标 URI http://www.oschina.net
,如下图所示:
4. 基于注册中心实现动态路由
示例代码对应仓库:
在「3. 快速入门」小节,我们在配置文件中,通过 path
+ url
的组合,添加了两个路由信息,这种我们一般称之为“静态路由”或者“传统路由”。
本小节,我们将通过 path
+ service-id
的组合,添加路由信息,这种我们一般称之为“动态路由”。如此,每个路由转发的 URL 地址,将从 Spring Cloud 注册中心获取对应 service-id
服务名对应的服务实例列表,并通过 Ribbon 等等进行负载均衡。
我们直接从「3. 快速入门」小节的 labx-21-sc-zuul-demo01
项目来复制,搭建 Zuul 基于注册中心实现动态路由的示例。最终项目结构如下图:
分割线:先进行网关项目的改造。
4.1 引入依赖
修改 pom.xml
文件,引入注册中心 Nacos 相关的依赖如下:
<!-- 引入 Spring Cloud Alibaba Nacos Discovery 相关依赖,将 Nacos 作为注册中心,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
考虑到 Nacos 作为 Spring Cloud 架构中的注册中心,已经越来越流行了,所以本小节我们使用它。感兴趣的胖友,可以后面看看艿艿写的《芋道 Spring Cloud Alibaba 注册中心 Nacos 入门》文章。
友情提示:如果胖友想用使用 Eureka 作为注册中心,可以引入
spring-cloud-starter-netflix-eureka-client
依赖。
4.2 配置文件
修改 application.yaml
配置文件,增加注册中心相关的配置项。完整配置如下:
server:
port: 8888
spring:
application:
name: zuul-application
cloud:
nacos:
# Nacos 作为注册中心的配置项
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
# Zuul 配置项,对应 ZuulProperties 配置类
zuul:
servlet-path: / # ZuulServlet 匹配的路径,默认为 /zuul
# 路由配置项,对应 ZuulRoute Map
routes:
route_yudaoyuanma:
path: /blog/**
url: http://www.iocoder.cn
route_oschina:
path: /oschina/**
url: https://www.oschina.net
route_users:
path: /users/**
service-id: user-service
① spring.cloud.nacos.discovery
配置项,使用 Nacos 作为 Spring Cloud 注册中心的配置项。这里就不详细解释,毕竟 Nacos 不是主角。
② 在 zuul.routes
配置项中,添加“动态路由” route_users
,通过 service-id
配置路由对应 user-service
用户服务。
4.3 搭建用户服务
创建 labx-21-sc-user-service
项目,作为 user-service
用户服务。代码比较简单,艿艿就不瞎哔哔了。最终项目如下图所示:
4.4 简单测试
① 执行 UserServiceApplication 两次,启动两个 user-service
服务。启动完成后,在 Nacos 注册中心可以看到该服务的两个实例,如下图所示:
② 执行 ZuulApplication 启动网关。
③ 访问 http://127.0.0.1:8888/users/user/get?id=1 地址,返回 JSON 结果如下:
{
"id": 1,
"name": "没有昵称:1",
"gender": 2
}
请求经过网关后,转发到 user-service
服务成功。
另外,Spring Cloud Zuul 集成 Spring Cloud 注册中心时,会给注册中心的每个服务在 Zuul 中自动创建对应的“动态路由”。例如说本小节的示例,对应配置文件的效果如下:
zuul:
routes:
user-service:
path: /user-service/**
service-id: user-service
我们来简单测试下,访问 http://127.0.0.1:8888/user-service/user/get?id=1 地址,返回 JSON 结果如下:
{
"id": 1,
"name": "没有昵称:1",
"gender": 2
}
友情提示:Zuul 通过 RibbonRoutingFilter 过滤器,使用 Ribbon 负载均衡请求后端的服务实例。
更多关于 Ribbon 的内容,胖友可以阅读《芋道 Spring Cloud Netflix 负载均衡 Ribbon 入门》文章。
5. 基于配置中心 Apollo 实现动态路由
示例代码对应仓库:
在「4. 基于注册中心实现动态路由」小节中,我们在配置文件中,添加了 Zuul “动态路由”。但是,如果每次进行路由的变更时,都需要修改配置文件,并重启 Zuul 实例,显然是不合适的。
因此,我们可以引入配置中心 Apollo 来实现动态路由的功能,将 zuul
配置项统一存储在 Apollo 中。同时,通过通过 Apollo 的实时监听器,在 zuul
发生变化时,刷新内存中的路由信息。
当然,Gateway 中我们还是会使用注册中心,目的是为了获取服务的实例列表,只是不再使用 Gateway 基于注册中心来的动态路由功能而已。
我们直接从「4. 基于注册中心实现动态路由」小节的 labx-21-sc-zuul-demo02-registry
项目,复制出本小节的 labx-21-sc-zuul-demo03-config-apollo
项目,搭建 Zuul 基于配置中心 Apollo 实现动态路由的示例。最终项目结构如下图:
分割线:先进行网关项目的改造。
5.1 引入依赖
修改 pom.xml
文件,引入配置中心 Apollo 相关的依赖如下:
<!-- 引入 Apollo 客户端,内置对 Apollo 的自动化配置 -->
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>1.5.1</version>
</dependency>
5.2 配置文件
修改 application.yaml
配置文件,增加 Apollo 相关的配置项。完整配置如下:
server:
port: 8888
spring:
application:
name: zuul-application
cloud:
nacos:
# Nacos 作为注册中心的配置项
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
# Apollo 相关配置项
app:
id: ${spring.application.name} # 使用的 Apollo 的项目(应用)编号
apollo:
meta: http://127.0.0.1:8080 # Apollo Meta Server 地址
bootstrap:
enabled: true # 是否开启 Apollo 配置预加载功能。默认为 false。
eagerLoad:
enable: true # 是否开启 Apollo 支持日志级别的加载时机。默认为 false。
namespaces: application # 使用的 Apollo 的命名空间,默认为 application。
① zuul
配置项,我们都删除了,统一在 Apollo 中进行配置。
为了演示 Gateway 启动时,从 Apollo 加载 spring.cloud.gateway
配置项,作为初始的路由信息,我们在 Apollo 配置如下:
- 默认将请求转发到艿艿的博客 http://www.iocoder.cn,嘿嘿~
配置对应文本内容如下:
zuul.servlet-path = /
zuul.routes.route_yudaoyuanma.path = /**
zuul.routes.route_yudaoyuanma.url = http://www.iocoder.cn
② app.id
和 apollo
配置项,为 Apollo 相关配置项。这里就不详细解释,毕竟 Apollo 不是主角。感兴趣的胖友,可以阅读《Spring Boot 配置中心 Apollo 入门》文章。
5.3 ZuulPropertiesRefresher
创建 ZuulPropertiesRefresher 类,监听 Apollo 中的zuul
发生变化时,刷新内存中的路由信息。代码如下:
@Component
public class ZuulPropertiesRefresher {
private static final Logger logger = LoggerFactory.getLogger(ZuulPropertiesRefresher.class);
@Autowired
private ApplicationContext applicationContext;
@Autowired
private RouteLocator routeLocator;
@ApolloConfigChangeListener(interestedKeyPrefixes = "zuul.") // <1>
public void onChange(ConfigChangeEvent changeEvent) {
refreshZuulProperties(changeEvent);
}
private void refreshZuulProperties(ConfigChangeEvent changeEvent) {
logger.info("Refreshing zuul properties!");
/*
* rebind configuration beans, e.g. ZuulProperties
* @see org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#onApplicationEvent
*/
// <2>
this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
/*
* refresh routes
* @see org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration.ZuulRefreshListener#onApplicationEvent
*/
// <3>
this.applicationContext.publishEvent(new RoutesRefreshedEvent(routeLocator));
logger.info("Zuul properties refreshed!");
}
}
① <1>
处,通过 Apollo 提供的 @ApolloConfigChangeListener
注解,声明监听 zuul.
配置项的刷新。
② <2>
处,发布 EnvironmentChangeEvent 配置变更事件,从而被 Spring Cloud Context ConfigurationPropertiesRebinder 所监听,刷新 ZuulProperties 配置类。
③ <3>
处,发布 RoutesRefreshedEvent 路由刷新事件,从而被 Spring Cloud Zuul ZuulServerAutoConfiguration.ZuulRefreshListener 所监听,刷新内存中的 Zuul 路由信息。
友情提示:这块涉及一定的知识量,胖友可以通过在对应的 Listener 监听器,打上相应的断点进行调试。
5.4 搭建用户服务
创建 labx-21-sc-user-service
项目,作为 user-service
用户服务。代码比较简单,艿艿就不瞎哔哔了。最终项目如下图所示:
5.5 简单测试
① 执行 UserServiceApplication 两次,启动两个 user-service
服务。
② 执行 ZuulApplication 启动网关。
使用浏览器,访问 http://127.0.0.1:8888/ 地址,返回艿艿的博客首页,如下图所示:
③ 修改在 Apollo 的 zuul
配置项,转发请求到用户服务。如下图所示:
配置对应文本内容如下:
zuul.servlet-path = /
zuul.routes.route_yudaoyuanma.path = /**
zuul.routes.route_yudaoyuanma.service-id = user-service
此时 IDEA 控制台看到 GatewayPropertiesRefresher 监听到 zuul
配置项刷新,并打印日志如下:
2020-05-04 00:26:31.763 INFO 52628 --- [Apollo-Config-2] c.i.s.l.z.ZuulPropertiesRefresher : Refreshing zuul properties!
2020-05-04 00:26:31.864 INFO 52628 --- [Apollo-Config-2] c.i.s.l.z.ZuulPropertiesRefresher : Zuul properties refreshed!
④ 访问 http://127.0.0.1:8888/user/get?id=1 地址,返回 JSON 结果如下:
{
"id": 1,
"name": "没有昵称:1",
"gender": 2
}
请求经过网关后,转发到 user-service
服务成功。
6. 基于配置中心 Nacos 实现动态路由
示例代码对应仓库:
在「4. 基于注册中心实现动态路由」小节中,我们在配置文件中,添加了 Zuul “动态路由”。但是,如果每次进行路由的变更时,都需要修改配置文件,并重启 Zuul 实例,显然是不合适的。
因此,我们可以引入配置中心 Config 来实现动态路由的功能,将 zuul
配置项统一存储在 Config 中。同时,通过通过 Config 的实时监听器,在 zuul
发生变化时,刷新内存中的路由信息。
当然,Zuul 中我们还是会使用注册中心,目的是为了获取服务的实例列表,只是不再使用 Zuul 基于注册中心来的动态路由功能而已。
我们直接从「4. 基于注册中心实现动态路由」小节的 labx-21-sc-zuul-demo02-registry
项目,复制出本小节的 labx-21-sc-zuul-demo03-config-nacos
项目,搭建 Zuul 基于配置中心 Nacos 实现动态路由的示例。最终项目结构如下图:
分割线:先进行网关项目的改造。
6.1 引入依赖
修改 pom.xml
文件,引入配置中心 Nacos 相关的依赖如下:
<!-- 引入 Spring Cloud Alibaba Nacos Config 相关依赖,将 Nacos 作为配置中心,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
6.2 配置文件
① 创建 bootstrap.yaml
配置文件,添加配置中心 Nacos 相关的配置。配置如下:
spring:
application:
name: zuul-application
cloud:
nacos:
# Nacos Config 配置项,对应 NacosConfigProperties 配置属性类
config:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
namespace: # 使用的 Nacos 的命名空间,默认为 null
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
name: # 使用的 Nacos 配置集的 dataId,默认为 spring.application.name
file-extension: yaml # 使用的 Nacos 配置集的 dataId 的文件拓展名,同时也是 Nacos 配置集的配置格式,默认为 properties
spring.cloud.nacos.config
配置项,为配置中心 Nacos 相关配置项。这里就不详细解释,毕竟 Nacos 不是主角。感兴趣的胖友,可以阅读《Spring Cloud Alibaba 配置中心 Nacos 入门》文章。
② 修改 application.yaml
配置文件,删除 Zuul 相关的配置。完整配置如下:
server:
port: 8888
spring:
cloud:
nacos:
# Nacos 作为注册中心的配置项
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
zuul
配置项,我们都删除了,统一在配置中心 Nacos 中进行配置。
为了演示 Zuul 启动时,从 Nacos 加载 zuul
配置项,作为初始的路由信息,我们在 Nacos 配置如下:
- 默认将请求转发到艿艿的博客 http://www.iocoder.cn,嘿嘿~
配置对应文本内容如下:
zuul:
servlet-path: /
routes:
route_yudaoyuanma:
path: /**
url: http://www.iocoder.cn
6.3 ZuulRouteRefreshListener
在 Nacos 配置发生变化时,Spring Cloud Alibaba Nacos Config 内置的监听器 会监听到配置刷新,发布 EnvironmentChangeEvent 配置变更事件。因此,我们实现自定义 ZuulRouteRefreshListener 监听器,来监听该事件。代码如下:
@Component
public class ZuulRouteRefreshListener implements ApplicationListener<EnvironmentChangeEvent> {
private static final Logger logger = LoggerFactory.getLogger(ZuulRouteRefreshListener.class);
@Autowired
private ApplicationEventPublisher publisher;
@Autowired
private RouteLocator routeLocator;
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
// <1> 判断是否有 `zuul.` 配置变化
boolean zuulConfigUpdated = false;
for (String key : event.getKeys()) {
if (key.startsWith("zuul.")) {
zuulConfigUpdated = true;
break;
}
}
if (!zuulConfigUpdated) {
return;
}
// <2> 发布 RoutesRefreshedEvent 事件
this.publisher.publishEvent(new RoutesRefreshedEvent(routeLocator));
logger.info("发布 RoutesRefreshedEvent 事件完成,刷新 Zuul 路由");
}
}
① <1>
处,判断 zuul.
配置项,是否发生变化。
② <2>
处,发布 RoutesRefreshedEvent 路由刷新事件,从而被 Spring Cloud Zuul ZuulServerAutoConfiguration.ZuulRefreshListener 所监听,刷新内存中的 Zuul 路由信息。
友情提示:这块涉及一定的知识量,胖友可以通过在对应的 Listener 监听器,打上相应的断点进行调试。
6.4 搭建用户服务
创建 labx-21-sc-user-service
项目,作为 user-service
用户服务。代码比较简单,艿艿就不瞎哔哔了。最终项目如下图所示:
6.5 简单测试
① 执行 UserServiceApplication 两次,启动两个 user-service
服务。
② 执行 ZuulApplication 启动网关。
使用浏览器,访问 http://127.0.0.1:8888/ 地址,返回艿艿的博客首页,如下图所示:
③ 修改在 Nacos 的 zuul
配置项,转发请求到用户服务。如下图所示:
配置对应文本内容如下:
zuul:
servlet-path: /
routes:
route_yudaoyuanma:
path: /**
service-id: user-service
此时 IDEA 控制台看到 ZuulRouteRefreshListener 监听到 zuul.
配置项刷新,并打印日志如下:
④ 访问 http://127.0.0.1:8888/user/get?id=1 地址,返回 JSON 结果如下:
{
"id": 1,
"name": "没有昵称:1",
"gender": 2
}
请求经过网关后,转发到 user-service
服务成功。
7. 灰度发布
Spring Cloud Zuul 并未提供灰度发布功能。
友情提示:感兴趣的胖友,可以阅读《Spring Cloud 网关 Spring Cloud Gateway 入门》的「8. 灰度发布」小节。
8. Zuul 过滤器
Zuul 的两大核心是路由和过滤功能:
- 路由功能:负责匹配请求到指定 Route 路由,从而转发请求到该路由对应的
url
后端 URL 或service-id
服务实例上。 - 过滤功能:负责对请求进行拦截,实现自定义的功能,例如说限流、熔断等等功能。
8.1 过滤器类型
Zuul 定义了 IZuulFilter 接口,定义了 Zuul 过滤器的两个基础方法。代码如下:
public interface IZuulFilter {
/**
* 是否进行过滤。如果是,则返回 true。
*/
boolean shouldFilter();
/**
* 执行过滤器的逻辑。
*/
Object run() throws ZuulException;
}
Zuul 定义了 ZuulFilter 抽象基类,定义了 Zuul 过滤器的类型和执行顺序方法。代码如下:
public abstract class ZuulFilter implements IZuulFilter, Comparable<ZuulFilter> {
/**
* 执行类型。
*/
abstract public String filterType();
/**
* 执行顺序。
*/
abstract public int filterOrder();
}
过滤器一共分成四种类型,分别在不同阶段执行:
// FilterConstants.java
/**
* {@link ZuulFilter#filterType()} pre type.
* 前置类型:在 route 之前调用
* 使用场景:身份认证、记录请求日志、服务容错、服务限流...
*/
public static final String PRE_TYPE = "pre";
/**
* {@link ZuulFilter#filterType()} route type.
* 路由类型:路由请求到后端 URL 或服务实例上
* 使用场景:使用 Apache HttpClient 请求后端 URL、或使用 Netflix Ribbon 请求服务实例...
*/
public static final String ROUTE_TYPE = "route";
/**
* {@link ZuulFilter#filterType()} post type.
* 后置类型:
* 1. 发生异常时,在 error 之后调用
* 2. 正常执行时,最后调用
* 使用场景:收集监控信息、响应结果给客户端...
*/
public static final String POST_TYPE = "post";
/**
* {@link ZuulFilter#filterType()} error type.
* 错误类型:处理请求时发生的错误时被调用。
*/
public static final String ERROR_TYPE = "error";
这么说可能有点抽象,我们再来一起看一幅图和一段代码,即可清楚明白:
8.2 核心过滤器
Zuul 自带了很多 ZuulFilter 实现类,如下图所示:
友情提示:如下的过滤器,胖友先简单看看。后续可以进行调试,进一步深入了解。
① pre 过滤器
名称 | 优先级 | 作用 |
---|---|---|
ServletDetectionFilter | -3 | 检测当前请求是通过 DispatcherServlet 处理运行的还是 ZuulServlet 运行处理的。 |
Servlet30WrapperFilter | -2 | 对原始的HttpServletRequest 进行包装。 |
FormBodyWrapperFilter | -1 | 将 Content-Type为 application/x-www-form-urlencoded 或 multipart/form-data 的请求包装成 FormBodyRequestWrapper对 象。 |
② route 过滤器
名称 | 优先级 | 作用 |
---|---|---|
DebugFilter | 1 | 根据 zuul.debug.request 的配置来决定是否打印 debug 日志。 |
PreDecorationFilter | 5 | 对当前请求进行预处理以便执行后续操作。 |
RibbonRoutingFilter | 10 | 通过 Ribbon 和 Hystrix 来向服务实例发起请求,并将请求结果进行返回。 |
SimpleHostRoutingFilter | 100 | 只对请求上下文中有 routeHost 参数的进行处理,直接使用 HttpClient 向 routeHost 对应的物理地址进行转发。 |
SendForwardFilter | 500 | 只对请求上下文中有 forward.to 参数的进行处理,进行本地跳转。 |
③ post 过滤器
名称 | 优先级 | 作用 |
---|---|---|
SendErrorFilter | 0 | 当其他过滤器内部发生异常时的会由它来进行处理,产生错误响应。 |
SendResponseFilter | 1000 | 利用请求上下文的响应信息来组织请求成功的响应内容。 |
8.3 自定义过滤器
一般情况下,我们在 Zuul 上会去做的拓展,主要集中在 Filter 上,例如说接入认证服务。因此,我们来搭建 Zuul 自定义 Filter 实现的示例,提供“伪劣”的认证功能。还是老样子,从「3. 快速入门」小节的 labx-21-sc-zuul-demo01
项目,复制出本小节的 labx-21-sc-zuul-demo05-custom-zuul-filter
项目,最终项目结构如下图:
8.3.1 AuthZuulFilter
创建 AuthZuulFilter 类,认证过滤器,提供“伪劣”的认证功能。代码如下:
@Component
public class AuthZuulFilter extends ZuulFilter {
/**
* 外部请求 Header - token 认证令牌 <2.1>
*/
private static final String DEFAULT_TOKEN_HEADER_NAME = "token";
/**
* 转发请求 Header - userId 用户编号 <2.2>
*/
private static final String DEFAULT_HEADER_NAME = "user-id";
/**
* token 和 userId 的映射 <2.3>
*/
private static Map<String, Integer> TOKENS = new HashMap<String, Integer>();
static {
TOKENS.put("yunai", 1);
}
public String filterType() {
return FilterConstants.PRE_TYPE; // <3.1> 前置过滤器
}
public int filterOrder() {
return 0;
}
public boolean shouldFilter() {
return true; // <3.2> 需要过滤
}
public Object run() throws ZuulException {
// 获取当前请求上下文
RequestContext ctx = RequestContext.getCurrentContext();
// <4.1> 获得 token
HttpServletRequest request = ctx.getRequest();
String token = request.getHeader(DEFAULT_TOKEN_HEADER_NAME);
// <4.2> 如果没有 token,则不进行认证。因为可能是无需认证的 API 接口
if (!StringUtils.hasText(token)) {
return null;
}
// <4.3> 进行认证
Integer userId = TOKENS.get(token);
// <4.4> 通过 token 获取不到 userId,说明认证不通过
if (userId == null) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401); // 响应 401 状态码
return null;
}
// <4.5> 认证通过,将 userId 添加到 Header 中
ctx.getZuulRequestHeaders().put(DEFAULT_HEADER_NAME, String.valueOf(userId));
return null;
}
}
① 集成 ZuulFilter 抽象类,并在类上添加 @Component
注解,保证 Zuul 能够加载到 AuthZuulFilter 过滤器。
② <2.1>
和 <2.2>
处,定义了认证 Token 的 Header 名字 token
,和认证后的 UserId 的 Header 名字 user-id
。
<2.3>
处,定义了一个存储 token
和 userId
映射的 Map,毕竟咱仅仅是一个提供“伪劣”的认证功能的 Filter。
③ <3.1>
处,设置过滤器的类型为 pre
前置过滤器。
<3.2>
处,设置过滤器需要执行。
④ <4.1>
处,从请求 Header 中获取 token
,作为认证标识。
<4.2>
处,如果没有 token
,则不进行认证。因为可能是无需认证的 API 接口。
<4.3>
处,“伪劣”的认证逻辑,哈哈哈~实际场景下,一般调用远程的认证服务。
<4.4>
处,通过 token
获取不到 userId
,说明认证不通过,设置返回 401 状态码。
<4.5>
处,通过 token
获取到 userId
,说明认证通过,将 userId
添加到请求 Header,从而实现将 userId
传递给目标 URI。
友情提示:在 Spring Cloud Security 项目中,提供了 Zuul 的支持,胖友可以后续去愁一愁噢。
8.3.2 简单测试
① 执行 ZuulApplication 启动网关。
② 使用 Postman 模拟请求 Header token
为 yunai1
,演示认证不通过的情况,结果如下图:
③ 使用 Postman 模拟请求 Header token
为 yunai
,演示认证通过的情况,结果如下图:
8.4 禁用过滤器
在 Spring Cloud Zuul 中,我们可以通过在配置文件中,添加 zuul.<过滤器名>.<过滤器类型>.disable=true
配置项来禁用指定过滤器。
例如说,我们想要禁用 SendResponseFilter 后置过滤器,则可以添加 zuul.SendResponseFilter.post.disable=true
配置项来禁用。
9. 请求限流
Spring Cloud Zuul 并未提供请求限流功能。
友情提示:感兴趣的胖友,可以阅读《芋道 Spring Cloud 网关 Spring Cloud Gateway 入门》的「10. 请求限流」小节。
10. 基于 Hystrix 实现服务容错
示例代码对应仓库:
Zuul 作为服务网关,为了避免被调用的服务拖垮,在使用 Ribbon 调用后端服务时,集成 Hystrix 进行不同 Route 的隔离,进入实现服务的容错。具体的,后续胖友可以看看 AbstractRibbonCommand 的源码,如下图所示:
Hystrix 库,是 Netflix 开源的一个针对分布式系统的延迟和容错库。
Hystrix 供分布式系统使用,提供延迟和容错功能,隔离远程系统、访问和第三方程序库的访问点,防止级联失败,保证复杂的分布系统在面临不可避免的失败时,仍能有其弹性。
下面,我们来搭建 Zuul 基于 Hystrix 实现服务容错的使用示例。还是老样子,从「4. 基于注册中心实现动态路由」小节的 labx-21-sc-zuul-demo02-registry
项目,复制出本小节的 labx-21-sc-zuul-demo07-hystrix
项目,最终项目结构如下图:
分割线:先进行网关项目的改造。
10.1 引入依赖
因为 spring-cloud-starter-netflix-zuul
默认引入了 Hystrix 相关依赖,所以我们无需主动引入。如下图所示:
10.2 FallbackProvider
Spring Cloud Zuul 定义了 FallbackProvider 接口,提供服务调用失败的 Fallback 降级的响应,例如说 Hystrix Fallback。代码如下:
@Component
public class ApiFallbackProvider implements FallbackProvider {
public String getRoute() {
return "*";
}
public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
return new ClientHttpResponse() {
public HttpStatus getStatusCode() {
return HttpStatus.OK;
}
public int getRawStatusCode() {
return HttpStatus.OK.value();
}
public String getStatusText() {
return HttpStatus.OK.getReasonPhrase();
}
public void close() {}
public InputStream getBody() { // 响应内容
String bodyText = String.format("{\"code\": 500,\"message\": \"Service unavailable:%s\"}", cause.getMessage());
return new ByteArrayInputStream(bodyText.getBytes());
}
public HttpHeaders getHeaders() { // 响应头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON); // json 返回
return headers;
}
};
}
}
- 代码比较简单,胖友自己瞅两眼就懂了。
10.3 搭建用户服务
创建 labx-21-sc-user-service
项目,作为 user-service
用户服务。代码比较简单,艿艿就不瞎哔哔了。最终项目如下图所示:
10.4 简单测试
① 执行 UserServiceApplication 启动 user-service
服务,执行 ZuulApplication 启动网关。
② 使用浏览器访问 http://127.0.0.1:8888/users/user/get?id=1 接口,成功调用 user-service
服务,并返回如下:
{
"id": 1,
"name": "没有昵称:1",
"gender": 2
}
③ 停止 UserServiceApplication 关闭 user-service
服务,模拟服务不可用的情况。使用浏览器访问 http://127.0.0.1:8888/users/user/get?id=1 接口,失败调用 user-service
服务,并返回如下:
{
"code": 500,
"message": "Service unavailable:null"
}
此时,我们快速使用浏览器访问 http://127.0.0.1:8888/users/user/get?id=1 接口,多次失败调用 user-service
服务,会触发 Hystrix 熔断,并返回如下:
{
"code": 500,
"message": "Service unavailable:Hystrix circuit short-circuited and is OPEN"
}
11. 基于 Sentinel 实现服务容错
示例代码对应仓库:
labx-21-sc-zuul-demo07-sentinel
。
本小节我们来进行 Zuul 和 Sentinel 的整合,使用 Sentinel 进行 Zuul 的流量保护。
Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级流量控制产品,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。
Sentinel 提供了 sentinel-zuul-adapter
子项目,已经对 Zuul 进行适配,所以我们只要引入它,基本就完成了 Zuul 和 Sentinel 的整合,贼方便。
友情提示:本小节会引用《Sentinel 官方文档 —— 网关限流》的内容。
下面,我们来搭建 Gateway 基于 Sentinel 实现服务容错的使用示例。还是老样子,从「3. 快速入门」小节的 labx-21-sc-zuul-demo01
项目,复制出本小节的 labx-21-sc-zuul-demo07-sentinel
项目,最终项目结构如下图:
11.1 引入依赖
修改 pom.xml
文件,额外引入 Sentinel 相关的依赖如下:
<!-- 引入 Spring Cloud Alibaba Sentinel 相关依赖,使用 Sentinel 提供服务保障,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
11.2 配置文件
修改 application.yaml
配置文件,增加 Sentinel 的配置项。完整配置如下:
server:
port: 8888
spring:
application:
name: zuul-application
cloud:
nacos:
# Nacos 作为注册中心的配置项
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
sentinel:
eager: true # 是否饥饿加载。默认为 false 关闭
transport:
dashboard: localhost:7070 # 是否饥饿加载。
# Sentinel 对 Zuul 的专属配置项,对应 SentinelZuulProperties 类
zuul:
order:
pre: 10000 # 前置过滤器 SentinelZuulPreFilter 的顺序
post: 1000 # 后置过滤器 SentinelZuulPostFilter 的顺序
error: -1 # 错误过滤器 SentinelZuulErrorFilter 的顺序
# Zuul 配置项,对应 ZuulProperties 配置类
zuul:
servlet-path: / # ZuulServlet 匹配的路径,默认为 /zuul
# 路由配置项,对应 ZuulRoute Map
routes:
yudaoyuanma: # 这是一个 Route 编号
path: /**
url: http://www.iocoder.cn
① 为了测试方便,我们修改 zuul.routes
配置项,所有请求都转发到艿艿的博客 http://www.iocoder.cn。
② spring.cloud.sentinel
配置项,是 Spring Cloud Sentinel 的配置项,后续胖友可以看看《芋道 Spring Cloud Alibaba 服务容错 Sentinel 入门》文章。
友情提示:艿艿本机搭建的 Sentinel 控制台启动在 7070 端口。
③ spring.cloud.sentinel.zuul
配置项,是 Sentinel 对 Zuul 的专属配置项,对应 SentinelZuulProperties 类。
order
:配置 Sentinel 拓展出的 Zuul 过滤器 SentinelZuulPreFilter、SentinelZuulPostFilter、SentinelZuulErrorFilter 的顺序。
11.3 ZuulBlockFallbackProvider
Sentinel 定义了 ZuulBlockFallbackProvider 接口,提供转发请求被 block 的 Fallback 降级的响应。代码如下:
public interface ZuulBlockFallbackProvider {
/**
* The route this fallback will be used for.
* @return The route the fallback will be used for.
*/
String getRoute();
/**
* Provides a fallback response based on the cause of the failed execution.
*
* @param route The route the fallback is for
* @param cause cause of the main method failure, may be <code>null</code>
* @return the fallback response
*/
BlockResponse fallbackResponse(String route, Throwable cause);
}
#getRoute()
方法,该 ZuulBlockFallbackProvider 匹配的 Route 编号,例如说route_users
。如果想要匹配所有 Route 则,返回*
。#fallbackResponse()
方法,处理指定route
路由发生的cause
异常,提供 Fallback 时的 BlockResponse 响应。
Sentinel 提供了 ZuulBlockFallbackProvider 默认实现类 DefaultBlockFallbackProvider,提供默认响应,代码如下:
public class DefaultBlockFallbackProvider implements ZuulBlockFallbackProvider {
@Override
public String getRoute() {
return "*"; // 匹配所有 Route
}
@Override
public BlockResponse fallbackResponse(String route, Throwable cause) {
if (cause instanceof BlockException) {
return new BlockResponse(429, "Sentinel block exception", route);
} else {
return new BlockResponse(500, "System Error", route);
}
}
}
这里,我们自定义 ZuulBlockFallbackProvider 实现类 CustomBlockFallbackProvider,提供自定义响应,代码如下:
@Component
public class CustomBlockFallbackProvider implements ZuulBlockFallbackProvider {
@PostConstruct
public void init() {
ZuulBlockFallbackManager.registerProvider(this); // <X> 注册到 ZuulBlockFallbackManager
}
public String getRoute() {
return "*";
}
public BlockResponse fallbackResponse(String route, Throwable cause) {
if (cause instanceof BlockException) {
return new BlockResponse(429, "你被 Block 啦!", route);
} else {
return new BlockResponse(500, "系统异常", route);
}
}
}
- 注意
<X>
处,需要将自己注册到 ZuulBlockFallbackManager 管理器中。
11.4 ZuulApplication
修改 ZuulApplication 类的代码,声明这是一个 Zuul 应用。代码如下:
@SpringBootApplication
@EnableZuulProxy // 开启 Zuul 网关
public class ZuulApplication {
public static void main(String[] args) {
System.setProperty(SentinelConfig.APP_TYPE, ConfigConstants.APP_TYPE_ZUUL_GATEWAY); // 【重点】设置应用类型为 Zuul
SpringApplication.run(ZuulApplication.class, args);
}
}
11.5 简单测试
① 执行 ZuulApplication 启动网关。
访问 Sentinel 控制台,可以看到网关已经成功注册上。如下图所示:
② 点击「流控规则」菜单,我们来给路由 yudaoyuanma
创建一个网关流控规则,如下图所示:
- 创建了针对路由
yudaoyuanma
的统一流控规则,允许每个 URL 的 QPS 上限为 3。
使用浏览器,快速访问 http://www.iocoder.cn 地址 4 次,会发现被 Sentinel 限流,返回 {"code":429, "message":"你被 Block 啦!", "route":"yudaoyuanma"}
结果。该文字提示,就是我们自定义的 CustomBlockFallbackProvider 提供的。
③ 下面,我们来给 /categories/**
路径,配置单独流控规则。
点击「API 管理」菜单,我们先创建一个包含 /categories/**
的 API 分组,如下图所示:
点击「流控规则」菜单,我们再给 API 分组创建一个网关流控规则,如下图所示:
- 创建了针对该 API 分组的单独流控规则,允许每个 URL 的 QPS 上限为 1。
友情提示:目前该功能,在 Zuul 的
servlet-path = /
时存在 BUG,具体可见 ISSUE#1109 的讨论。因此下面的演示,暂时只是艿艿期待的 YY 结果哈~
使用浏览器,快速访问 http://127.0.0.1:8888/categories/Spring-Cloud/ 地址 2 次,会发现被 Sentinel 限流,返回 {"code":429, "message":"你被 Block 啦!", "route":"yudaoyuanma"}
结果。
注意,虽然我们给 /categories/**
配置了单独的流控规则,但是通过路由配置的统一的流控规则也是生效的,也会作用到 /categories/**
上,即叠加的效果。
11.6 网关限流规则
sentinel-zuul-adapter
项目增加了网关限流规则(GatewayFlowRule),针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
GatewayFlowRule 的字段解释如下:
resource
:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。resourceMode
:规则是针对 API Gateway 的 route 还是用户在 Sentinel 中定义的 API 分组,默认是 route。grade
:限流指标维度,同限流规则的grade
字段。count
:限流阈值intervalSec
:统计时间窗口,单位是秒,默认是 1 秒。controlBehavior
:流量整形的控制效果,同限流规则的controlBehavior
字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。burst
:应对突发请求时额外允许的请求数目。maxQueueingTimeoutMs
:匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。paramItem
:参数限流配置。若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。其中的字段:parseStrategy
:从请求中提取参数的策略,目前支持提取来源 IP、Host、任意 Header 和任意 URL 参数四种策略。fieldName
:若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应的 header 名称或 URL 参数名称。pattern
:参数值的匹配模式,只有匹配该模式的请求属性值会纳入统计和流控;若为空则统计该请求属性的所有值。matchStrategy
:参数值的匹配策略,目前支持精确匹配、子串匹配和正则匹配三种策略。
具体的示例,可以看看 sentinel-gw-flow.json
配置文件,内容如下:
[
{
"resource": "yudaoyuanma",
"count": 3
},
{
"resource": "yudaoyuanma_customized_api",
"count": 1
}
]
11.7 API 定义分组
sentinel-zuul-adapter
项目增加了API 定义分组(ApiDefinition),用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api
,请求 path 模式为 /foo/**
和 /baz/**
的都归到 my_api
这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。
ApiDefinition 的字段解释如下:
apiName
:分组名。predicateItems
:匹配规则(ApiPathPredicateItem)数组。
具体的示例,可以看看 sentinel-gw-api-group.json
配置文件,内容如下:
[
{
"apiName": "yudaoyuanma_customized_api",
"predicateItems": [
{
"pattern": "/categories/**",
"matchStrategy": 1
},
{
"items": [
{
"pattern": "/Dubbo/good-collection/",
"matchStrategy": 0
},
{
"pattern": "/SkyWalking/**",
"matchStrategy": 1
}
]
}
]
}
]
12. 监控端点
示例代码对应仓库:
labx-21-sc-zuul-demo09-actuator
。
Spring Cloud Zuul 的 RoutesEndpoint 和 FiltersEndpoint 类,基于 Spring Boot Actuator,提供了自定义监控端点,实现了 Zuul 的监控管理的功能。整理如下表格:
路径 | 用途 |
---|---|
GET /routes | 获得所有路由(简要) |
GET /routes/details | 获得指定路由(明细) |
POST /routes | 发布 RoutesRefreshedEvent 事件 |
GET /filters | 获得所有过滤器 |
下面,我们来搭建 Zuul 的监控端点的使用示例。还是老样子,从「3. 快速入门」小节的 labx-08-sc-gateway-demo01
项目,复制出本小节的 labx-21-sc-zuul-demo09-actuator
项目,最终项目结构如下图:
12.1 引入依赖
因为 spring-cloud-starter-netflix-zuul
默认引入了 Spring Boot Actuator 相关依赖,所以我们无需主动引入。如下图所示:
12.2 配置文件
修改 application.yaml
配置文件,额外增加 Spring Boot Actuator 配置项。完成配置如下:
server:
port: 8888
spring:
application:
name: zuul-application
# Zuul 配置项,对应 ZuulProperties 配置类
zuul:
servlet-path: / # ZuulServlet 匹配的路径,默认为 /zuul
# 路由配置项,对应 ZuulRoute Map
routes:
route_yudaoyuanma:
path: /blog/**
url: http://www.iocoder.cn
route_oschina:
path: /oschina/**
url: https://www.oschina.net
management:
endpoints:
web:
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
endpoint:
# Health 端点配置项,对应 HealthProperties 配置类
health:
enabled: true # 是否开启。默认为 true 开启。
show-details: ALWAYS # 何时显示完整的健康信息。默认为 NEVER 都不展示。可选 WHEN_AUTHORIZED 当经过授权的用户;可选 ALWAYS 总是展示。
server:
port: 18888 # 单独设置端口,因为 8888 端口全部给 Zuul 了
① 增加 management
配置项,设置的 Actuator 配置。每个配置项的作用,胖友看下艿艿添加的注释。如果还不理解的话,后续看下《Spring Boot 监控端点 Actuator 入门》文章。
下面,执行 ZuulApplication 启动网关,测试每一个端点。
12.3 路由端点
RoutesEndpoint 提供了 Zuul 路由相关的端点。
① GET
请求 http://127.0.0.1:18888/actuator/routes/ 地址,获得所有路由(简要)。结果如下:
{
"/blog/**": "http://www.iocoder.cn",
"/oschina/**": "https://www.oschina.net"
}
② 请求 http://127.0.0.1:18888/actuator/routes/details 地址,获得所有路由(明细)。结果如下:
{
"/blog/**": {
"id": "route_yudaoyuanma",
"fullPath": "/blog/**",
"location": "http://www.iocoder.cn",
"path": "/**",
"prefix": "/blog",
"retryable": false,
"customSensitiveHeaders": false,
"prefixStripped": true
},
"/oschina/**": {
"id": "route_oschina",
"fullPath": "/oschina/**",
"location": "https://www.oschina.net",
"path": "/**",
"prefix": "/oschina",
"retryable": false,
"customSensitiveHeaders": false,
"prefixStripped": true
}
}
③ POST
请求 http://127.0.0.1:18888/actuator/routes/ 地址,发布 RoutesRefreshedEvent 并获得所有路由(简要)。结果如下:
{
"/blog/**": "http://www.iocoder.cn",
"/oschina/**": "https://www.oschina.net"
}
12.4 过滤器端点
FiltersEndpoint 提供了 Zuul 过滤器相关的端点。
① GET
请求 http://127.0.0.1:18888/actuator/filters 地址,获取所有过滤器。结果如下图:
至此,我们已经完成 Spring Cloud Zuul 的学习。如下是 Zuul 相关的官方文档:
更多关于 Zuul 的内容,胖友可以访问《Zuul 实现原理与源码解析系统 —— 精品合集》。