参考:
Spring Cloud Gateway 介绍
6000 字 | 16 图 | 深入理解 Spring Cloud Gateway 的原理
Spring Cloud Gateway 官方文档
SpringCloud 网关组件 Gateway 原理深度解析
1.什么是 API 网关?有什么作用?
(1)API 网关是一个在分布式系统中作为入口的服务器,用于接收所有外部请求,并将这些请求路由到相应的服务端节点上。它是构建微服务架构中的关键组件之一。
(2)API 网关的作用如下:
- 聚合和转发请求:API 网关接收来自客户端的请求,并根据路由规则将请求转发到正确的后端服务。它可以聚合多个微服务的接口调用,从而简化客户端的请求流程,提高性能和可靠性。
- 负载均衡:通过在网关层实现负载均衡算法,将请求分发到后端的多个服务实例上,实现请求的平衡和分散,提高系统的并发处理能力和容错能力。
- 认证和授权:API 网关可以作为认证和授权的入口,对请求进行身份验证和权限校验。它可以通过集成身份验证机制(如 OAuth、JWT 等)实现用户认证,以及通过配置访问控制规则来授权访问。
- 缓存和性能优化:API 网关可以缓存部分经常请求的结果,从而减轻服务器端的负载,提高系统的响应速度和性能。
- 安全防护:API 网关可以对请求进行安全检查和过滤,防止恶意请求和攻击,保护后端服务的安全和稳定。
- 监控和日志:API 网关可以对请求进行统一的监控和日志记录,帮助开发者了解系统的运行情况,进行故障排查和性能优化。
(3)总之,API 网关充当了微服务架构中客户端和后端服务之间的中间层,集中管理和解耦了微服务的接入和治理。它提供了路由转发、负载均衡、认证授权、缓存和性能优化等功能,简化了系统的复杂性,提供了安全性、可靠性和可扩展性,是构建高效、可靠的分布式系统的重要组件。
2.有哪些常见的网关?
(1)以下是几个常见的网关系统:
-
Spring Cloud Gateway
:Spring Cloud Gateway 是基于 Spring Framework 5、Spring Boot 2 和 Project Reactor 等技术构建的网关系统。它提供了路由、过滤、负载均衡、熔断、限流等功能,可以作为微服务架构中的统一入口,实现请求的转发和管理。Spring Cloud Gateway 是一个轻量级的网关解决方案,易于集成和扩展。 -
***flix Zuul
:Zuul 是由 ***flix 开发的网关系统,属于 ***flix OSS(Open Source Software)家族的一员。它提供了路由、过滤、负载均衡等功能,可以与 Eureka、Ribbon、Hystrix 等 ***flix 开源组件集成使用。Zuul 1.x 版本基于 Servlet 技术栈实现,而 Zuul 2.x 版本采用了 ***ty 作为底层实现。 -
Kong
:Kong 是一个快速、可扩展和分布式的开源 API 网关,它基于 Nginx 构建,并提供了管理界面和插件系统。Kong 支持动态路由、认证授权、限流、监控等功能,可以与多种后端服务(如微服务、数据库等)进行集成,使用方便简单。
(2)以上是一些常见的网关系统,它们都提供了类似的基本功能,包括路由转发、负载均衡、认证授权、限流、监控等。选择适合自身需求的网关系统时,可以根据具体的技术栈、性能要求、可扩展性和社区支持等因素进行评估和选择。
3.什么是 Spring Cloud Gateway?有什么作用?有什么优缺点?
(1)Spring Cloud Gateway 属于 Spring Cloud 生态中的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代 ***flix Zuul
,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流。
(2)Spring Cloud Gateway的主要作用如下:
- 路由转发:Spring Cloud Gateway 可以根据配置的路由规则将请求转发到相应的后端服务。它支持根据请求的 URI、请求头、请求参数等信息进行路由匹配和转发,实现动态的服务路由。
- 过滤器功能:Spring Cloud Gateway 提供了一套强大的过滤器机制,可以灵活地对请求进行预处理、后处理和过滤。开发者可以通过自定义过滤器来实现请求的验证、参数校验、请求日志、安全处理等功能。
-
负载均衡:Spring Cloud Gateway集成了多种负载均衡器,如
***flix Ribbon
和Spring Cloud LoadBalancer
。它可以根据负载均衡算法将请求均匀地分发到后端的多个服务实例上,提高系统的并发处理能力和容错能力。 - 动态路由配置:Spring Cloud Gateway 支持动态的路由配置,可以通过微服务注册中心(如 Eureka、Consul 等)或统一的配置中心(如Spring Cloud Config)实现动态路由的更新和发布,无需重启网关服务。
- 集成 Spring Cloud 生态系统:Spring Cloud Gateway 可以与其他 Spring Cloud 组件无缝集成,如 Spring Cloud Config、Spring Cloud Discovery(如 Eureka、Consul)和 Spring Security 等,实现统一的配置管理、服务注册与发现、安全认证等。
(3)Spring Cloud Gateway 的优缺点如下:
-
优点:
- 性能强劲:是第一代网关 Zuul 的 1.6 倍;
- 功能强大:内置了很多实用的功能,例如转发、监控、限流等;
- 设计优雅,容易扩展;
-
缺点:
- 其实现依赖 ***ty 与 WebFlux,不是传统的 Servlet 编程模型,学习成本高;
- 不能将其部署在 Tomcat、Jetty 等 Servlet 容器里,只能打成 jar 包执行;
- Spring Boot 2.0 及以上的版本才支持 Spring Cloud Gateway;
4.✨Spring Cloud Gateway 的工作流程是什么样的?
Spring Cloud Gateway 的工作流程如下图所示,具体步骤为:
-
请求发送:客户端向
Spring Cloud Gateway
(下面称为网关)发送请求; -
路由判断;当请求到达网关后,先由
Gateway Handler Mapping
进行处理,确定与该请求匹配的路由,该路由会映射到后端的某个服务,此外还会根据路由断言来判断路由是否可用; -
请求过滤:断言成功后请求会到达
Gateway Web Handler
,这里面有很多过滤器,并组成过滤器链 (Filter Chain),这些过滤器可以对请求进行拦截和修改,例如修改请求和响应、做权限验证、添加请求日志等操作,然后将请求转发到实际的后端服务。 - 服务处理:后端服务接收到请求,并进行具体的业务处理;
- 响应过滤:后端将处理得到的结果返回给网关,网关再次通过一系列的过滤器对响应进行各种过滤操作;
-
响应返回:响应经过过滤处理后,最终被网关返回给客户端。
上述图片来源于 Spring 官网
5.✨Spring Cloud Gateway 中的路由 (Route) 是指什么?它由哪几部分组成?
在 Spring Cloud Gateway 中,路由 (Route) 是定义请求转发规则的对象。它指定了接收到的请求应该如何进行路由,并将请求转发到指定的目标服务。一个路由由以下几个要素组成:
-
id
:作为路由的唯一标识符,可以用于配置和管理路由。 -
uri
:指定请求要转发的目标服务的地址。可以是一个完整的 URL,也可以是一个注册在服务注册中心的服务名。 -
predicates
:用于匹配请求的一系列条件,如果请求满足所有的匹配条件,它将会被路由到该路由所指定的目标 URI。 -
filters
:用于在请求被路由之前或之后对请求进行处理和修改的组件。过滤器可以对请求进行增强、校验、修改和校验等操作。 -
order
:用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
(2)以下是一个路由的示例配置:
spring:
cloud:
gateway:
routes:
- id: example_route
uri: http://example.***
predicates:
- Path=/example/**
filters:
- AddRequestHeader=X-Request-Id, 1234567890
order: 1
在上述示例中,我们定义了一个 id 为 example_route
的路由,将匹配到路径为 /example/**
的请求转发到 http://example.***
。同时,还使用了一个过滤器,将请求头 X-Request-Id
添加到请求中,值为 1234567890
。
(3)通过路由的配置,Spring Cloud Gateway 能够根据请求的路径、请求头、请求方法等条件进行请求的转发和过滤。你可以根据具体的业务需求配置不同的路由来实现灵活的请求路由和转发策略。
6.Spring Cloud Gateway 中的断言 (Predicate) 是指什么?有哪些内置断言?
(1)在 Spring Cloud Gateway 中,断言 (Predicate) 用于定义路由规则的匹配条件。它是一个逻辑函数,可以根据请求的特性进行条件判断,以确定是否匹配该路由规则。
(2)断言的主要作用在于路由匹配,即通过断言对请求的特征进行匹配,确定是否满足该路由规则的条件。常用的断言条件包括请求的 URI、请求的方法、请求的头信息、请求的参数等。只有当请求满足断言的条件时,才会进一步考虑该路由规则的转发。
(3)在配置断言时,可以使用多个断言组合,通过逻辑运算符(如AND、OR、NOT)来定义更复杂的路由规则。常见的内置断言如下:
上述图片来源于网络。
下面是一个示例:
(4)需要注意的是,断言的选择和配置需要结合具体的业务需求和场景来进行,合理定义条件判断能够提高路由的准确性和效率。同时,过多或过于复杂的断言条件可能会导致路由规则的维护和管理复杂化,因此需要根据实际情况进行权衡和选择。
7.在 Spring Cloud Gateway 中如何自定义断言?
要实现自定义断言,需要创建一个继承了 AbstractRoutePredicateFactory
类的路由断言工厂类,并完成相关配置和逻辑处理。然后,将该自定义断言添加到路由配置中。具体细节如下:
- 创建自定义的路由断言工厂类
AgeRoutePredicateFactory
:
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.***mons.lang3.StringUtils;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.***ponent;
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/*
这是一个自定义的路由断言工厂类,要求有两个:
1.名字必须是 配置+RoutePredicateFactory
2.必须继承 AbstractRoutePredicateFactory<配置类>
*/
@***ponent
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
//构造函数
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
//读取配置文件的中参数值,并将其赋值到配置类中的属性上
public List<String> shortcutFieldOrder() {
//这个位置的顺序必须跟配置文件中的值的顺序对应
return Arrays.asList("minAge", "maxAge");
}
//断言逻辑
public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//1.接收前台传入的 age 参数
String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age");
//2.先判断是否为空
if (StringUtils.isNotEmpty(ageStr)) {
//3.如果不为空,再进行路由逻辑判断
int age = Integer.parseInt(ageStr);
return age <= config.getMaxAge() && age >= config.getMinAge();
}
return false;
}
};
}
//配置类,用于接收配置文件中的对应参数
@Data
@NoArgsConstructor
public static class Config {
private int minAge; // 18
private int maxAge; // 60
}
}
- 在配置文件中,添加一个 Age 的断言配置,要求请求中的 age 在 18 ~ 60 之间:
8.Spring Cloud Gateway 中的路由和断言是什么关系?
路由和断言之间的关系如下:
- 一对多:一个路由规则可以包含多个断言;
- 同时满足:如果一个路由规则中有多个断言,则需要同时满足才能匹配;
- 第一个匹配成功:如果一个请求可以匹配多个路由,则映射第一个匹配成功的路由;
9.Spring Cloud Gateway 中的过滤器 (Filter) 是指什么?有哪些过滤器?
9.1.概述
(1)在Spring Cloud Gateway中,过滤器 (Filter) 是用于在请求路由过程中对请求和响应进行处理和修改的组件。过滤器可以在请求被路由到目标服务之前或之后执行一些操作,如校验请求、修改请求头、记录日志、添加认证等。
9.2.分类
9.2.1.请求和响应
从请求和响应的角度来划分,过滤器可以以下两种:
- Pre 类型:在请求被转发到微服务之前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。
- Post 类型:微服务处理完请求后,返回响应给网关,网关可以再次进行处理,例如修改响应内容或响应头、日志输出、流量监控等。
9.2.2.作用范围
从作用范围的角度来划分,过滤器可以以下两种:
(1)全局过滤器 (Global Filter):全局过滤器对所有的路由都生效,可以用于实现全局的请求处理逻辑。常见的全局过滤器如下所示:
全局过滤器最常见的用法是进行负载均衡。配置如下所示:
spring:
cloud:
gateway:
routes:
- id: product_route # 第三方微服务路由规则
uri: lb://service-product # 负载均衡,将请求转发到注册中心注册的 service-product 服务
predicates:
- Path=/product-serv/**
filters: #过滤器
- StripPrefix=1 # 转发之前去掉 1 层路径(这里是去掉 /product-serv)
这里有个关键字 lb
,用到了全局过滤器 LoadBalancerClientFilter
,当匹配到这个路由后,会将请求转发到 service-product
服务,且支持负载均衡转发,也就是先将 service-product
解析成实际的微服务的 host 和 port,然后再转发给实际的微服务。
(2)局部过滤器 (Gateway Filter):路由过滤器只对指定的路由生效,可以用于实现对特定路由的请求处理逻辑。常见的局部过滤器如下所示:
10.在 Spring Cloud Gateway 中如何自定义过滤器?
10.1.自定义全局过滤器
(1)内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。开发中的鉴权逻辑如下:
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录);
- 认证通过,将用户信息进行加密形成 token,返回给客户端,作为登录凭证;
- 以后每次请求,客户端都携带认证的 token;
- 服务端对 token 进行解密,判断是否有效;
(2)如上图,对于验证用户是否已经登录鉴权的过程可以在网关统一检验。检验的标准就是请求中是否携带 token 凭证以及 token 的正确性。下面的我们自定义一个 GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑。
package ***.itheima.filters;
import org.apache.***mons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.***ponent;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
//自定义全局过滤器,作用是统一鉴权,需要实现 GlobalFilter 和 Ordered 接口
@***ponent
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//完成判断逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (!StringUtils.equals(token, "admin")) {
System.out.println("鉴权失败");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().set***plete();
}
//调用 chain.filter 继续向下游执行
return chain.filter(exchange);
}
//顺序,数值越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
10.2.自定义局部过滤器
要实现自定义局部过滤器,需要创建一个继承了 AbstractGatewayFilterFactory
类的过滤器工厂类,并完成相关配置和逻辑处理。然后,将该自定义自定义局部过滤器添加到路由配置中。具体细节如下:
- 创建自定义的过滤器工厂类
LogGatewayFilterFactory
:
package ***.itheima.filters;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.***ponent;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
//自定义局部过滤器
@***ponent
public class LogGatewayFilterFactory
extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
//构造函数
public LogGatewayFilterFactory() {
super(LogGatewayFilterFactory.Config.class);
}
//读取配置文件中的参数,并将其赋值到配置类中
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("consoleLog", "cacheLog");
}
//过滤器逻辑
@Override
public GatewayFilter apply(LogGatewayFilterFactory.Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (config.isCacheLog()) {
System.out.println("cacheLog 已经开启了....");
}
if (config.isConsoleLog()) {
System.out.println("consoleLog 已经开启了....");
}
return chain.filter(exchange);
}
};
}
//配置类,用于接收配置参数
@Data
@NoArgsConstructor
public static class Config {
private boolean consoleLog;
private boolean cacheLog;
}
}
- 在配置文件中,添加一个 Log 的过滤器配置:
11.在 Spring Cloud Gateway 如何自定义全局异常处理?
(1)在 SpringBoot 项目中,我们捕获全局异常只需要在项目中配置 @RestControllerAdvice
和 @ExceptionHandler
就可以了。不过,这种方式在 Spring Cloud Gateway 下不适用。Spring Cloud Gateway 中实现自定义全局异常常用的一种方式是实现 ErrorWebExceptionHandler
接口并重写其中的 handle
方法。下面是一个示例:
@***ponent
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
// 自定义异常处理逻辑
// ...
}
}
(2)需要注意的是,Spring Boot 中的全局异常处理器可以返回 ResponseEntity
或直接通过 @ResponseBody
返回响应体,而 Spring Cloud Gateway 的全局异常处理则需要返回 Mono<Void>
作为响应结果。这么设计是因为 Spring Cloud Gateway 是基于响应式编程模型构建的,响应是通过异步的方式处理的,因此需要返回 Mono 或 Flux 作为响应。另外,Spring Cloud Gateway 的异常处理还与 ServerWebExchange
对象有关,你可以通过该对象获取到请求和响应的相关信息。
(3)需要注意的是,Spring Cloud Gateway 使用 ErrorWebExceptionHandler 来处理异常,而不是 @ExceptionHandler
注解。这是因为 Spring Cloud Gateway 处理请求的过程是一个管道的形式,异常可能会在不同的过滤器中抛出,需要一个统一的异常处理器来处理这些异常。
(4)总结起来,Spring Boot 和 Spring Cloud Gateway 的全局异常处理方式有所不同,主要是基于各自的Web框架特性和响应式编程模型的需求而设计。
相关知识点:
Spring Boot 面试题——全局异常处理
12.如何使用 Spring Cloud Gateway 进行限流?
12.1.使用 RequestRateLimiter 过滤器
Spring Cloud Gateway 内置了一个 RequestRateLimiter
过滤器,可以基于令牌桶算法实现请求的限流。你可以在路由配置中使用该过滤器来实现限流功能。示例如下:
spring:
cloud:
gateway:
routes:
- id: example_route
uri: http://example.***
predicates:
- Path=/example/**
filters:
- RequestRateLimiter=10
以上示例中,example_route 路由使用了 RequestRateLimiter
过滤器,每秒最多处理 10 个请求。
12.2.结合 Sentinel 进行限流
(1)Sentinel
支持对 Spring Cloud Gateway、Zuul 等主流网关进行限流,并且从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:
- Route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 Route Id;
- 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组
(2)具体步骤如下:
- 导入依赖
<dependency>
<groupId>***.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
-
编写配置类:基于 Sentinel 的 Spring Cloud Gateway 限流是通过其提供的过滤器来完成的,使用时只需注入对应的
SentinelGatewayFilter
实例以及SentinelGatewayBlockExceptionHandler
实例即可。
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCode***onfigurer serverCode***onfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>>
viewResolversProvider,
ServerCode***onfigurer serverCode***onfigurer) {
this.viewResolvers =
viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCode***onfigurer = serverCode***onfigurer;
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
// 配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(
new GatewayFlowRule("product_route") //资源名称,对应路由id
.setCount(1) // 限流阈值
.setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
);
GatewayRuleManager.loadRules(rules);
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler
sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers,
serverCode***onfigurer);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange
serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
- 自定义 API 分组:自定义 API 分组是一种更细粒度的限流规则定义。
/**
* 配置初始化的限流参数
*/
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new
GatewayFlowRule("product_api1").setCount(1).setIntervalSec(1));
rules.add(new
GatewayFlowRule("product_api2").setCount(1).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
//自定义 API 分组
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("product_api1")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
//以 /product-serv/product/api1 开头的请求
add(new ApiPathPredicateItem().setPattern("/product_serv/product/api1/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("product_api2")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
//以 /product-serv/product/api2/demo1 完成的 url 路径匹配
add(new ApiPathPredicateItem().setPattern("/product_serv/product/api2/demo1"));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}