一、API网关的核心定位与架构价值
在微服务架构体系中,API网关是位于客户端与后端服务集群之间的流量中间层,是整个分布式系统的七层流量调度中枢,承担着所有南北向流量的入口管控职责。
很多开发者会混淆API网关与反向代理的边界:反向代理(如基础Nginx)的核心能力是四层/七层流量转发与负载均衡,聚焦于网络层的流量分发;而API网关是反向代理的能力超集,在基础转发能力之上,叠加了业务维度的流量治理、安全防护、协议适配、可观测性等全链路能力,是业务架构的核心基础设施。
从架构演进的视角来看,API网关解决了微服务架构下的核心痛点:
- 避免客户端与多服务直接耦合,统一接入入口,降低客户端接入成本
- 抽离各服务重复的横切逻辑(鉴权、限流、日志、跨域等),实现一次开发全服务复用
- 统一管控集群流量入口,实现故障隔离、风险兜底,保障集群稳定性
- 适配多端、多协议的接入需求,实现异构系统的无缝打通
二、API网关的核心能力与底层逻辑
API网关的能力体系可分为基础核心能力、生产级增强能力两大维度,每个能力的落地都有明确的底层逻辑支撑。
2.1 核心基础能力
2.1.1 智能路由与请求转发
这是API网关最核心的基础能力,本质是请求的精准分发与流量调度。 底层逻辑:网关接收客户端所有请求,基于预设的匹配规则,完成请求与后端服务实例的映射,再通过负载均衡策略将请求转发至对应实例。 主流匹配规则的优先级与实现逻辑:
- 精确匹配:请求路径与规则完全一致,基于HashMap实现O(1)时间复杂度匹配,优先级最高
- 前缀匹配:请求路径匹配规则前缀,基于Trie前缀树实现O(k)时间复杂度匹配(k为路径长度),优先级次之
- 正则匹配:基于正则表达式完成路径匹配,时间复杂度较高,优先级最低
- 多维度匹配:支持基于Host、请求头、请求参数、Cookie、请求方法等多维度组合匹配
2.1.2 全链路安全防护
网关作为集群流量的唯一入口,是整个系统的第一道安全防线,可实现全集群的统一安全管控,避免各服务重复建设安全能力。 核心安全能力的底层逻辑:
- 身份认证:统一完成JWT、OAuth2.0、AK/SK等认证逻辑,校验通过后将用户身份信息透传给后端服务,避免各服务重复解析token
- 权限校验:基于RBAC模型统一完成接口级别的权限管控,拦截非法权限请求
- 攻击防护:内置WAF基础能力,完成SQL注入、XSS跨站脚本、CSRF跨站请求伪造、路径遍历等常见攻击的拦截
- 访问管控:基于IP黑白名单、地域封禁、接口访问频次管控,实现恶意请求的前置拦截
2.1.3 流量治理与稳定性保障
网关是集群流量的总阀门,可实现全集群的流量统一管控,避免流量洪峰冲垮后端服务,是分布式系统高可用架构的核心组件。 核心流量治理能力:
- 流量限流:基于预设规则限制接口的请求频次,避免过载。主流算法包括固定窗口、滑动窗口、漏桶、令牌桶四种,其中令牌桶算法可应对突发流量,是网关场景的首选方案
- 熔断降级:基于服务调用的失败率、响应时长等指标,自动熔断故障服务,返回兜底响应,避免故障级联传播
- 流量整形:将突发的不规则流量整形成平稳的流量,平滑转发至后端服务,避免后端服务被突发流量打垮
- 负载均衡:内置轮询、加权轮询、一致性哈希、最小连接数等多种负载均衡策略,实现请求的均匀分发
2.1.4 协议转换与异构适配
网关可实现多协议的接入与转换,解决异构系统的通信问题,降低客户端与后端服务的适配成本。 核心适配能力:
- 协议转换:支持客户端HTTP/HTTPS/HTTP3/WebSocket接入,后端服务可适配REST、Dubbo、gRPC等协议,网关完成协议的双向转换
- 格式转换:支持JSON、XML等数据格式的双向转换,适配不同客户端的格式需求
- 兼容适配:支持接口版本管理、请求参数转换、响应体格式化,实现后端接口迭代与客户端的解耦
2.2 生产级增强能力
2.2.1 全链路可观测性
网关作为全链路请求的起点,是分布式追踪的核心节点,可实现全集群请求的统一可观测能力建设。 核心能力:
- 访问日志:统一记录所有请求的访问日志,包括请求路径、请求参数、响应状态、响应时长、客户端IP、服务实例等信息,支持敏感字段脱敏
- 指标采集:统一采集QPS、响应时长P99/P95、错误率、限流次数、熔断次数等核心指标,对接Prometheus等监控系统
- 分布式追踪:自动生成TraceId、SpanId,对接SkyWalking、Jaeger等追踪系统,实现全链路请求的追踪排查
2.2.2 灰度发布与流量调度
网关可实现精细化的流量调度,支撑业务的灰度发布、蓝绿发布、金丝雀发布等发布策略,降低版本迭代的风险。 底层逻辑:基于请求头、用户ID、IP段、设备类型等维度,将符合规则的流量转发至新版本服务实例,通过流量权重的逐步调整,完成版本的平滑迭代,出现问题可快速切回,无业务感知。
2.2.3 请求与响应的统一处理
网关可实现全集群请求与响应的统一处理,抽离各服务的重复处理逻辑。 核心能力:
- 统一请求校验:基于预设规则完成请求参数的合法性校验,拦截非法请求
- 统一跨域处理:全集群的CORS跨域配置统一管理,避免各服务重复配置
- 统一响应处理:统一的响应体格式化、异常响应处理、响应内容压缩
- 统一请求改写:支持请求头、请求参数、请求路径的统一改写,适配后端服务的接口要求
三、主流API网关架构选型与对比
当前主流的API网关可分为Java生态网关、云原生高性能网关、服务网格Sidecar网关三大类,不同网关的架构设计、性能表现、适用场景有明确差异。
3.1 主流网关核心特性对比
| 网关产品 | 技术栈 | 底层IO模型 | 核心优势 | 核心劣势 | 适用场景 |
| Spring Cloud Gateway | Java/Reactor/Netty | 主从Reactor多线程、非阻塞IO | Java生态无缝整合、扩展开发成本低、Spring Cloud全体系适配、动态路由支持完善 | 性能低于C/Rust/Lua实现的网关,超大规模流量场景有瓶颈 | Java技术栈微服务架构、中小规模流量、业务定制化需求多的场景 |
| Apache APISIX | OpenResty/LuaJIT | 事件驱动、IO多路复用 | 极致性能、毫秒级动态配置生效、插件生态丰富、云原生友好、国产开源社区活跃 | 业务定制化需基于Lua开发,Java技术栈有学习成本 | 超大规模流量场景、云原生K8s架构、对动态配置和性能要求高的场景 |
| Kong Gateway | OpenResty/LuaJIT | 事件驱动、IO多路复用 | 企业级能力成熟、全球社区活跃、商业支持完善 | 配置生效延迟高于APISIX、开源版高级特性受限、性能略低于APISIX | 跨国企业、需要成熟企业级商业支持的场景 |
| Envoy | C++ | 事件驱动、非阻塞IO | 极致性能、低延迟、云原生原生支持、服务网格适配最优、可观测性能力极强 | 配置复杂、学习曲线陡峭、业务定制化需基于C++/WASM开发,成本极高 | 云原生服务网格架构、超大规模流量、对性能和延迟要求极致的场景 |
3.2 核心选型原则
- 技术栈匹配优先:Java技术栈团队,业务定制化需求多的场景,优先选择Spring Cloud Gateway;云原生架构团队,通用流量管控为主的场景,优先选择APISIX/Envoy。
- 流量规模匹配:日均请求亿级以下、单实例QPS万级以下的场景,Spring Cloud Gateway完全可满足需求;单实例QPS十万级以上、超大规模流量场景,优先选择APISIX/Envoy。
- 动态能力需求:需要频繁调整路由规则、限流策略、插件配置,要求配置毫秒级生效的场景,优先选择APISIX;配置变更频率低的场景,可根据技术栈选择。
- 云原生适配度:基于K8s部署的云原生架构,优先选择APISIX/Envoy/Higress;虚拟机部署的传统架构,可选择Spring Cloud Gateway/Kong。
3.3 易混淆技术点明确区分
- 南北向网关 vs 东西向网关:南北向网关管控客户端到服务端的外部流量,是集群的统一接入入口;东西向网关管控服务之间的内部流量,一般由服务网格的Sidecar实现,二者的使用场景、能力侧重点完全不同,不可混淆。
- Zuul vs Spring Cloud Gateway:Zuul1基于Servlet同步阻塞IO模型,一个请求对应一个线程,高并发下线程资源极易耗尽,性能极差;Zuul2虽实现了异步非阻塞,但Spring Cloud官方未完成整合,国内生产环境使用极少;Spring Cloud Gateway基于WebFlux响应式编程模型,底层采用Netty非阻塞IO,性能是Zuul1的10倍以上,是Spring Cloud生态的唯一官方网关方案,完全替代Zuul。
- 网关限流 vs 服务限流:网关限流是集群入口的总阀门限流,核心目标是防止流量洪峰冲垮整个集群,保护整体架构;服务限流是单服务的精细化限流,核心目标是保护单个服务不被过载请求打垮,二者是互补关系,而非替代关系,生产环境需同时配置。
四、API网关高性能设计核心原理
API网关作为IO密集型应用,高性能设计的核心本质是:用最少的CPU、内存资源处理最多的请求,同时保持最低的延迟,核心是减少阻塞、减少上下文切换、减少数据拷贝、减少CPU计算开销。
4.1 IO模型与Reactor架构设计
IO模型是网关性能的基础,不同IO模型的吞吐量、延迟表现有数量级的差异。
- BIO同步阻塞IO:一个请求对应一个线程,线程在等待IO操作时全程阻塞,无法处理其他请求,高并发下线程数爆炸,CPU上下文切换开销极大,仅适用于低并发场景,Zuul1即采用此模型。
- NIO同步非阻塞IO:线程发起IO请求后不会阻塞,可继续处理其他请求,IO就绪后再回调处理,一个线程可处理数千个连接,大大减少线程数量,降低上下文切换开销。
- IO多路复用:NIO的核心实现,基于Linux epoll/kqueue机制,用一个Selector线程监听数千个Socket连接的IO事件,仅当事件就绪时才分发给工作线程处理,是当前高性能网关的标准底层模型。
主从Reactor多线程模型
主从Reactor多线程模型是当前所有高性能网关的标准架构,Nginx、Netty、APISIX、Envoy均基于此模型实现。
模型核心逻辑:
- 主Reactor(BossGroup):仅负责监听客户端的连接请求,完成TCP三次握手后,将创建的连接注册到从Reactor,不处理任何业务逻辑,线程数一般等于CPU核心数。
- 从Reactor(WorkerGroup):负责监听已建立连接的读写IO事件,事件就绪后,将事件分发给对应的Handler处理,线程数一般为CPU核心数的2倍。
- 工作线程池:负责处理业务逻辑(鉴权、限流、路由匹配等),避免业务逻辑阻塞IO线程,导致吞吐量下降。
此模型的核心优势是:分工明确,无单点瓶颈,充分利用多核CPU性能,全程无阻塞,可支撑数十万级的并发连接,百万级的QPS。
4.2 零拷贝技术
传统IO操作中,数据从网卡到用户态再到网卡发送,需要经过4次数据拷贝、2次内核态与用户态的上下文切换,其中2次CPU拷贝是极大的性能开销。零拷贝技术的核心是减少数据拷贝次数,消除CPU在用户态与内核态之间的数据拷贝开销,大幅提升IO处理效率。
网关场景主流的零拷贝实现:
- sendfile系统调用:Linux内核原生支持,数据直接在内核缓冲区之间完成拷贝,无需经过用户态,将4次拷贝减少为2次DMA拷贝,0次CPU拷贝,是真正意义上的零拷贝。网关转发请求时,可直接将后端服务的响应数据通过sendfile转发给客户端,无需经过网关应用程序的用户态内存,性能提升可达数倍。
- mmap内存映射:将内核缓冲区与用户态缓冲区映射到同一块物理内存地址,消除内核态到用户态的CPU拷贝,将4次拷贝减少为3次,适用于需要对数据进行少量修改的场景。
- 用户态零拷贝:Netty通过CompositeByteBuf实现多个ByteBuf的逻辑合并,无需实际拷贝数据;通过ByteBuf的切片实现数据的共享,无需拷贝,大幅减少内存拷贝开销。
4.3 内存池化技术
网关作为IO密集型应用,会频繁创建和销毁网络传输的缓冲区对象,若每次都通过JVM堆内存分配,会导致频繁的GC,甚至STW停顿,严重影响吞吐量和延迟。
内存池化的核心逻辑:提前申请一块连续的大内存,划分为不同规格的内存块,应用需要缓冲区时,直接从内存池中获取,使用完毕后归还到内存池,无需频繁向JVM申请和释放内存,大幅减少GC压力,提升内存分配效率。
Netty的PooledByteBufAllocator是内存池化的经典实现,采用Arena-Chunk-Page-Slab的四层内存管理模型:
- Arena:内存分配的顶层区域,每个线程对应一个Arena,避免多线程竞争,实现无锁分配
- Chunk:大内存块,默认16MB,是内存分配的基本单位
- Page:Chunk划分为多个Page,默认8KB,是内存分配的最小单位
- Slab:Page划分为多个不同规格的Slab,适配小内存的分配需求
Spring Cloud Gateway底层基于Netty实现,默认开启池化内存分配,是其高性能的核心支撑。
4.4 无锁化设计
高并发场景下,锁竞争是CPU开销的核心来源之一,多线程同时修改共享变量会导致频繁的锁竞争、上下文切换,吞吐量急剧下降。高性能网关的设计中,需尽可能采用无锁化设计,消除锁竞争开销。
网关场景主流的无锁化实现:
- 单线程串行处理:Netty将每个Channel连接绑定到固定的EventLoop线程,该连接的所有IO操作、事件处理均由该线程串行执行,无需多线程竞争,完全消除锁开销。
- 线程本地变量:采用ThreadLocal(Netty FastThreadLocal)存储线程私有变量,每个线程拥有独立的变量副本,无需共享,无锁竞争。
- CAS无锁操作:对于必须共享的变量,采用CAS原子操作替代锁,基于CPU的CAS指令实现无锁的变量修改,避免锁竞争带来的开销。
- 无锁队列:内部消息传递采用Disruptor无锁环形队列,基于内存屏障和CAS实现,高并发下性能远超JDK自带的并发队列。
4.5 高效路由匹配设计
网关的每个请求都需要经过路由匹配,高并发场景下,路由匹配的效率直接决定网关的吞吐量。若每次请求都遍历所有路由规则、执行正则匹配,会带来极大的CPU开销。
高性能路由匹配的核心设计:
- 路由规则预编译:网关启动时,将所有路由规则预编译为可快速匹配的对象,将匹配条件解析为结构化数据,避免每次请求都重新解析规则。
- 多级索引设计:为路由规则建立多级索引,基于Host建立一级索引,基于路径匹配类型建立二级索引,精确匹配路由存入HashMap,前缀匹配路由构建Trie前缀树,正则匹配路由单独分组。请求到来时,按优先级依次匹配,匹配成功立即终止,无需遍历所有规则,时间复杂度从O(n)降至O(1)或O(k)。
- 懒加载与缓存:热点路由规则的匹配结果存入本地缓存,下次相同请求直接命中缓存,无需重复匹配。
4.6 全异步化流程设计
高性能网关的整个请求处理流程,必须实现全链路异步非阻塞,无任何阻塞操作,才能充分利用CPU资源,实现吞吐量的最大化。
全异步化的核心要求:
- 整个请求处理流程基于响应式编程模型实现,从请求接收、路由匹配、鉴权、限流、转发请求、接收响应、返回客户端,全流程无阻塞。
- 所有远程调用(配置中心、认证中心、缓存、后端服务)均采用异步非阻塞方式实现,禁止使用同步调用阻塞IO线程。
- 禁止在IO线程中执行任何阻塞操作,包括同步数据库查询、Thread.sleep、同步锁等待等,否则会导致EventLoop线程阻塞,整个网关的吞吐量急剧下降。
五、生产级网关实战:基于Spring Cloud Gateway实现
5.1 项目基础环境配置
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>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>gateway-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway-demo</name>
<description>API Gateway Demo</description>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.1</spring-cloud.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<fastjson2.version>2.0.52</fastjson2.version>
<jjwt.version>0.12.5</jjwt.version>
<redisson.version>3.27.0</redisson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2023.0.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.1.0-jre</version>
</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>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
MySQL表结构
CREATE TABLE `t_route_config` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`route_id` varchar(64) NOT NULL COMMENT '路由ID',
`uri` varchar(256) NOT NULL COMMENT '转发地址',
`predicates` text NOT NULL COMMENT '断言规则JSON',
`filters` text COMMENT '过滤器规则JSON',
`order_num` int NOT NULL DEFAULT '0' COMMENT '排序优先级',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0-禁用 1-启用',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_route_id` (`route_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='路由配置表';
CREATE TABLE `t_black_ip` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`ip_address` varchar(64) NOT NULL COMMENT 'IP地址',
`reason` varchar(256) DEFAULT NULL COMMENT '封禁原因',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ip_address` (`ip_address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IP黑名单表';
application.yml配置
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: public
gateway:
enabled: true
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
globalcors:
cors-configurations:
'[/**]':
allowed-origin-patterns: "*"
allowed-methods: "*"
allowed-headers: "*"
allow-credentials: true
max-age: 3600
data:
redis:
host: 127.0.0.1
port: 6379
database: 0
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/gateway_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
type-aliases-package: com.jam.demo.pojo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
springdoc:
api-docs:
enabled: true
swagger-ui:
enabled: true
path: /swagger-ui.html
logging:
level:
com.jam.demo: INFO
org.springframework.cloud.gateway: INFO
项目启动类
package com.jam.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
@SpringBootApplication
@MapperScan("com.jam.demo.mapper")
@OpenAPIDefinition(info = @Info(title = "API网关服务", version = "1.0", description = "API网关服务接口文档"))
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
5.2 核心实体类定义
RouteConfig.java
package com.jam.demo.pojo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_route_config")
public class RouteConfig {
@TableId(type = IdType.AUTO)
private Long id;
private String routeId;
private String uri;
private String predicates;
private String filters;
private Integer orderNum;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
BlackIp.java
package com.jam.demo.pojo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_black_ip")
public class BlackIp {
@TableId(type = IdType.AUTO)
private Long id;
private String ipAddress;
private String reason;
private LocalDateTime expireTime;
private LocalDateTime createTime;
}
TokenInfo.java
package com.jam.demo.pojo.dto;
import lombok.Data;
@Data
public class TokenInfo {
private Long userId;
private String username;
private String role;
private Long expireTime;
}
5.3 核心工具类
JwtUtils.java
package com.jam.demo.utils;
import com.jam.demo.pojo.dto.TokenInfo;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.util.StringUtils;
import com.alibaba.fastjson2.JSON;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
/**
* JWT工具类
* @author ken
*/
public class JwtUtils {
private static final String SECRET_KEY = "jam_gateway_2024_secure_key_1234567890123456";
private static final long EXPIRATION = 7200000L;
private static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
private JwtUtils() {
}
/**
* 生成JWT令牌
* @param tokenInfo 令牌信息
* @return 生成的JWT令牌
*/
public static String generateToken(TokenInfo tokenInfo) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + EXPIRATION);
tokenInfo.setExpireTime(expireDate.getTime());
return Jwts.builder()
.subject(tokenInfo.getUserId().toString())
.claim("userInfo", JSON.toJSONString(tokenInfo))
.issuedAt(now)
.expiration(expireDate)
.signWith(KEY, Jwts.SIG.HS256)
.compact();
}
/**
* 校验并解析JWT令牌
* @param token JWT令牌
* @return 解析后的令牌信息,解析失败返回null
*/
public static TokenInfo parseToken(String token) {
if (!StringUtils.hasText(token)) {
return null;
}
try {
Jws<Claims> claimsJws = Jwts.parser()
.verifyWith(KEY)
.build()
.parseSignedClaims(token);
Claims payload = claimsJws.getPayload();
String userInfoJson = payload.get("userInfo", String.class);
return JSON.parseObject(userInfoJson, TokenInfo.class);
} catch (JwtException | IllegalArgumentException e) {
return null;
}
}
/**
* 校验令牌是否有效
* @param token JWT令牌
* @return 有效返回true,无效返回false
*/
public static boolean validateToken(String token) {
if (!StringUtils.hasText(token)) {
return false;
}
try {
Jwts.parser()
.verifyWith(KEY)
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
5.4 核心过滤器实现
RequestWrapperGlobalFilter.java
package com.jam.demo.filter.global;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 请求体包装全局过滤器,解决请求体只能读取一次的问题
* @author ken
*/
@Component
public class RequestWrapperGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (exchange.getRequest().getHeaders().getContentLength() <= 0) {
return chain.filter(exchange);
}
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
DataBufferUtils.retain(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
return chain.filter(exchange.mutate().request(decorator).build());
});
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
AuthGlobalFilter.java
package com.jam.demo.filter.global;
import com.jam.demo.pojo.dto.TokenInfo;
import com.jam.demo.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
/**
* 认证全局过滤器,统一处理JWT认证
* @author ken
*/
@Slf4j
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private static final List<String> WHITE_LIST = Arrays.asList(
"/api/user/login",
"/api/user/register",
"/swagger-ui.html",
"/v3/api-docs"
);
private static final String AUTH_HEADER = "Authorization";
private static final String TOKEN_PREFIX = "Bearer ";
private static final String USER_ID_HEADER = "X-User-Id";
private static final String USER_ROLE_HEADER = "X-User-Role";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
if (isWhitePath(path)) {
return chain.filter(exchange);
}
String authHeader = request.getHeaders().getFirst(AUTH_HEADER);
if (!StringUtils.hasText(authHeader) || !authHeader.startsWith(TOKEN_PREFIX)) {
return buildUnauthorizedResponse(exchange);
}
String token = authHeader.substring(TOKEN_PREFIX.length());
TokenInfo tokenInfo = JwtUtils.parseToken(token);
if (tokenInfo == null) {
return buildUnauthorizedResponse(exchange);
}
ServerHttpRequest mutatedRequest = request.mutate()
.header(USER_ID_HEADER, tokenInfo.getUserId().toString())
.header(USER_ROLE_HEADER, tokenInfo.getRole())
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
private boolean isWhitePath(String path) {
return WHITE_LIST.stream().anyMatch(path::startsWith);
}
private Mono<Void> buildUnauthorizedResponse(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
String body = "{\"code\":401,\"message\":\"未授权,请先登录\",\"data\":null}";
return response.writeWith(Mono.just(response.bufferFactory().wrap(body.getBytes())));
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1;
}
}
AccessLogGlobalFilter.java
package com.jam.demo.filter.global;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.InetSocketAddress;
import java.util.Optional;
/**
* 访问日志全局过滤器,统一记录请求访问日志
* @author ken
*/
@Slf4j
@Component
public class AccessLogGlobalFilter implements GlobalFilter, Ordered {
private static final String START_TIME_ATTR = "startTime";
private static final String MASK_PATTERN = "(1[3-9]\\d)\\d{4}(\\d{4})";
private static final String MASK_REPLACEMENT = "$1****$2";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(START_TIME_ATTR, System.currentTimeMillis());
return chain.filter(exchange)
.doFinally(signalType -> writeAccessLog(exchange));
}
private void writeAccessLog(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
Long startTime = exchange.getAttribute(START_TIME_ATTR);
long duration = startTime == null ? 0 : System.currentTimeMillis() - startTime;
String clientIp = getClientIp(request);
String method = request.getMethod().name();
String path = request.getPath().value();
int status = Optional.ofNullable(response.getStatusCode()).map(code -> code.value()).orElse(0);
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
String routeId = route == null ? "unknown" : route.getId();
String userAgent = request.getHeaders().getFirst(HttpHeaders.USER_AGENT);
String maskedPath = path.replaceAll(MASK_PATTERN, MASK_REPLACEMENT);
log.info("AccessLog|ip:{}|method:{}|path:{}|status:{}|routeId:{}|duration:{}ms|userAgent:{}",
clientIp, method, maskedPath, status, routeId, duration, userAgent);
}
private String getClientIp(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
String ip = headers.getFirst("X-Forwarded-For");
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
int index = ip.indexOf(',');
return index > 0 ? ip.substring(0, index).trim() : ip.trim();
}
ip = headers.getFirst("X-Real-IP");
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip.trim();
}
InetSocketAddress remoteAddress = request.getRemoteAddress();
return remoteAddress == null ? "unknown" : remoteAddress.getAddress().getHostAddress();
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
GrayReleaseGatewayFilterFactory.java
package com.jam.demo.filter.gateway;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.List;
/**
* 灰度发布网关过滤器工厂
* @author ken
*/
@Slf4j
@Component
public class GrayReleaseGatewayFilterFactory extends AbstractGatewayFilterFactory<GrayReleaseGatewayFilterFactory.Config> {
private static final String USER_ID_HEADER = "X-User-Id";
private static final String VERSION_HEADER = "X-Service-Version";
public GrayReleaseGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String userId = request.getHeaders().getFirst(USER_ID_HEADER);
String targetVersion = config.getBaseVersion();
if (StringUtils.hasText(userId)) {
int hash = Math.abs(userId.hashCode()) % 100;
if (hash < config.getGrayRatio()) {
targetVersion = config.getGrayVersion();
}
}
ServerHttpRequest mutatedRequest = request.mutate()
.header(VERSION_HEADER, targetVersion)
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("baseVersion", "grayVersion", "grayRatio");
}
@Data
public static class Config {
private String baseVersion;
private String grayVersion;
private int grayRatio;
}
}
RateLimitGatewayFilterFactory.java
package com.jam.demo.filter.gateway;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
/**
* 令牌桶限流网关过滤器工厂
* @author ken
*/
@Slf4j
@Component
public class RateLimitGatewayFilterFactory extends AbstractGatewayFilterFactory<RateLimitGatewayFilterFactory.Config> {
private final RedissonClient redissonClient;
public RateLimitGatewayFilterFactory(RedissonClient redissonClient) {
super(Config.class);
this.redissonClient = redissonClient;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
String limitKey = "gateway:rate_limit:" + path;
RRateLimiter rateLimiter = redissonClient.getRateLimiter(limitKey);
rateLimiter.trySetRate(RateType.OVERALL, config.getPermits(), config.getInterval(), RateIntervalUnit.SECONDS);
if (rateLimiter.tryAcquire()) {
return chain.filter(exchange);
}
log.warn("Request limited, path:{}, limit:{}/{}s", path, config.getPermits(), config.getInterval());
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
String body = "{\"code\":429,\"message\":\"请求过于频繁,请稍后再试\",\"data\":null}";
return response.writeWith(Mono.just(response.bufferFactory().wrap(body.getBytes())));
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("permits", "interval");
}
@Data
public static class Config {
private int permits;
private int interval;
}
}
5.5 动态路由实现
RouteConfigMapper.java
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.pojo.entity.RouteConfig;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RouteConfigMapper extends BaseMapper<RouteConfig> {
}
RouteConfigService.java
package com.jam.demo.service;
import org.springframework.cloud.gateway.route.RouteDefinition;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface RouteConfigService {
Flux<RouteDefinition> getRouteDefinitions();
Mono<Void> refreshRoutes();
}
RouteConfigServiceImpl.java
package com.jam.demo.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.mapper.RouteConfigMapper;
import com.jam.demo.pojo.entity.RouteConfig;
import com.jam.demo.service.RouteConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 动态路由服务实现类
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RouteConfigServiceImpl implements RouteConfigService, RouteDefinitionRepository {
private final RouteConfigMapper routeConfigMapper;
private final ApplicationEventPublisher eventPublisher;
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
List<RouteConfig> routeConfigs = routeConfigMapper.selectList(
new LambdaQueryWrapper<RouteConfig>().eq(RouteConfig::getStatus, 1)
);
if (CollectionUtils.isEmpty(routeConfigs)) {
return Flux.empty();
}
List<RouteDefinition> definitions = routeConfigs.stream()
.map(this::convertToRouteDefinition)
.filter(java.util.Objects::nonNull)
.toList();
return Flux.fromIterable(definitions);
}
@Override
public Mono<Void> refreshRoutes() {
eventPublisher.publishEvent(new RefreshRoutesEvent(this));
log.info("Route definitions refreshed successfully");
return Mono.empty();
}
private RouteDefinition convertToRouteDefinition(RouteConfig config) {
try {
RouteDefinition definition = new RouteDefinition();
definition.setId(config.getRouteId());
definition.setUri(java.net.URI.create(config.getUri()));
definition.setOrder(config.getOrderNum());
definition.setPredicates(JSON.parseArray(config.getPredicates(), org.springframework.cloud.gateway.handler.predicate.PredicateDefinition.class));
if (org.springframework.util.StringUtils.hasText(config.getFilters())) {
definition.setFilters(JSON.parseArray(config.getFilters(), org.springframework.cloud.gateway.filter.FilterDefinition.class));
}
return definition;
} catch (Exception e) {
log.error("Convert route config to definition failed, routeId:{}", config.getRouteId(), e);
return null;
}
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return Mono.empty();
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return Mono.empty();
}
}
5.6 全局异常处理
GlobalExceptionHandler.java
package com.jam.demo.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.*;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
* @author ken
*/
@Slf4j
@Component
@Order(-2)
public class GlobalExceptionHandler extends AbstractErrorWebExceptionHandler {
public GlobalExceptionHandler(ErrorAttributes errorAttributes,
WebProperties webProperties,
ApplicationContext applicationContext,
ServerCodecConfigurer codecConfigurer) {
super(errorAttributes, webProperties.getResources(), applicationContext);
this.setMessageWriters(codecConfigurer.getWriters());
this.setMessageReaders(codecConfigurer.getReaders());
}
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
Throwable error = getError(request);
log.error("Gateway request error, path:{}", request.path(), error);
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
result.put("message", "网关内部错误,请稍后再试");
result.put("data", null);
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(result));
}
}
六、生产环境最佳实践
6.1 部署与容灾
- 网关必须采用集群部署,避免单点故障,基于K8s实现Pod的多可用区部署,单节点故障不影响整体服务可用性。
- 采用四层负载均衡(LVS/HAProxy)作为网关集群的前置接入,实现网关实例的流量均匀分发与健康检查,故障实例自动摘除。
- 配置合理的弹性扩缩容策略,基于CPU使用率、QPS、连接数等指标实现自动扩缩容,应对流量洪峰。
6.2 监控与告警
- 核心监控指标:QPS、平均响应时长、P99/P95响应时长、错误率、限流次数、熔断次数、连接数、CPU使用率、内存使用率、GC频率与时长。
- 全链路追踪:接入SkyWalking/Jaeger等分布式追踪系统,实现请求的全链路追踪,快速定位故障节点。
- 告警配置:针对错误率突增、响应时长飙升、限流次数异常、实例宕机等核心异常场景配置实时告警,及时发现并处理故障。
6.3 安全加固
- 强制HTTPS接入,禁用TLS 1.0/1.1等低版本协议,禁用弱加密套件,配置合理的证书过期告警。
- 配置请求体大小限制,避免大请求攻击;配置单IP最大连接数与请求频次限制,防DDoS攻击。
- 定期更新网关组件版本,修复安全漏洞;对网关的管理接口配置严格的IP访问限制,避免未授权访问。
6.4 性能调优
- JVM调优:JDK17推荐使用ZGC低延迟垃圾回收器,配置合理的堆内存大小,避免频繁GC与STW停顿,禁用堆外内存限制,适配Netty池化内存。
- Netty参数调优:配置合理的BossGroup/WorkerGroup线程数,开启TCP_NODELAY禁用Nagle算法,调整SO_BACKLOG连接队列大小,适配高并发场景。
- 业务优化:减少自定义过滤器中的阻塞操作,避免在过滤器中执行同步数据库查询、远程调用等操作;热点配置本地缓存,减少远程调用开销。
七、总结
API网关作为微服务架构的流量入口与管控中枢,是分布式系统不可或缺的核心基础设施。其核心价值不仅是请求转发,更是通过统一的流量治理、安全防护、协议适配、可观测性能力,实现架构的解耦与标准化,降低微服务架构的落地与维护成本。
在架构选型时,需结合团队技术栈、流量规模、业务定制化需求,选择最适配的网关产品;在高性能设计中,需牢牢抓住异步非阻塞、零拷贝、内存池化、无锁设计、高效匹配这几个核心点,实现网关性能的最大化;在生产落地时,需做好容灾部署、监控告警、安全加固、性能调优,保障网关的高可用与稳定运行。