SpringCloud---Gateway vs Netflix Zuul 网关对比深度解析

基于:Spring Cloud Gateway 4.x(Spring Framework 6 / Spring Boot 3.x / Reactor Netty)vs Netflix Zuul 1.x(Servlet 容器)+ Zuul 2.x(Netty,无 Spring Cloud 集成)

本文从原理、设计、实现、使用四个维度对两大网关做深度对比:原理层面对比线程模型与性能表现,设计层面对比路由与过滤器抽象,实现层面走一遍双方请求处理链路源码,使用层面给出两套可运行的完整示例(pom / 配置 / 自定义过滤器 / 调用链路),最后给出总览大表、选型建议与 Zuul → Gateway 迁移路径。解决"新项目选谁、遗留系统怎么迁"两个问题。


目录

  1. 阅读说明与口径
  2. 速览结论
  3. [原理对比:阻塞 Servlet vs 响应式 Netty](#原理对比:阻塞 Servlet vs 响应式 Netty)
    • [3.1 两种线程模型](#3.1 两种线程模型)
    • [3.2 性能表现(第三方基准数据)](#3.2 性能表现(第三方基准数据))
  4. 设计对比:路由与过滤器模型
    • [4.1 核心抽象:三要素 vs 四类过滤器](#4.1 核心抽象:三要素 vs 四类过滤器)
    • [4.2 动态路由与配置方式](#4.2 动态路由与配置方式)
  5. 实现对比:请求处理链路源码解析
    • [5.1 Zuul 1.x:ZuulServlet 过滤器链](#5.1 Zuul 1.x:ZuulServlet 过滤器链)
    • [5.2 Gateway:DispatcherHandler 响应式链路](#5.2 Gateway:DispatcherHandler 响应式链路)
    • [5.3 关键类关系图](#5.3 关键类关系图)
  6. 使用对比:配置与自定义开发
    • [6.1 Zuul 完整示例](#6.1 Zuul 完整示例)
    • [6.2 Gateway 完整示例](#6.2 Gateway 完整示例)
    • [6.3 从 Zuul 迁移到 Gateway](#6.3 从 Zuul 迁移到 Gateway)
  7. 总览大表
  8. 选型建议
  9. 来源

1. 阅读说明与口径

  • 对比对象spring-cloud-starter-netflix-zuul(Zuul 1.x,最后一个可用版本在 Spring Cloud Hoxton / Boot 2.3 线);Netflix Zuul 2.x(仅 Netflix 内部生态使用,Spring 从未集成);spring-cloud-starter-gateway(Gateway 4.x,当前 Spring Cloud 官方网关)。
  • 两个项目的时间线(决定"为什么 Gateway 是默认答案"的前提):
时间 事件
2018-12 Netflix 宣布 Hystrix / Ribbon / Zuul 等组件进入维护模式,不再新增功能
2020-12-22 Spring Cloud 2020.0.0 发布,正式移除 Zuul / Hystrix / Ribbon,仅保留 Eureka
2021-01 Netflix 发布 Zuul 2 GA(Netty 异步模型),但与 Zuul 1 不向后兼容,Spring 官方未集成
2021 至今 Spring Cloud Gateway 成为官方唯一推荐网关,随 Boot 3.x 进入 4.x 时代
  • 版本口径:Gateway 4.x 与 Spring Cloud 发行版的对应关系(以官方兼容表为准):
Spring Cloud 发行版 Gateway 版本 Spring Boot
2023.0.x 4.1.x 3.2 / 3.3.x
2024.0.x 4.2.x 3.4.x
2025.0.x 4.3.x 3.5.x
  • 性能数据口径 :第 3.2 节的数字来自第三方公开基准测试,非官方背书,受机型 / 并发模型 / 版本影响大,仅用于理解数量级差距,不用于容量规划的精确依据。
  • 本文与 \[springcloude全家桶知识大纲] 互补:大纲看全家桶全局,本文聚焦网关这一格的横向对比。涉及 Servlet 容器线程模型的背景可参考 \[tomcat知识点]、\[spring-MVC知识点]。

2. 速览结论

结论 说明
新项目无脑选 Gateway Spring Cloud 2020.0 起 Zuul 已被官方移除;Gateway 是 Boot 3.x 生态里唯一官方网关
性能差距根源是线程模型 Zuul 1 同步阻塞(每请求一线程),Gateway 响应式非阻塞(EventLoop 复用),吞吐差距可达 3~5 倍
⚠️ Zuul 2 是"理论上先进、实际上没人用" 改用了 Netty 异步模型,但 Spring 从未集成,2021 年 GA 后即进入 Netflix 内部维护状态
Zuul 1 遗留系统注意 CVE 2.2.x 早期版本受 Spring4Shell(CVE-2022-22965)影响,须升级到 Hoxton.SR12 线
🔧 迁移核心难点不是配置而是过滤器 Zuul 的同步 ZuulFilter 不能平移,要重写为响应式 GlobalFilter,并排查阻塞调用

关键认知 :Gateway 对 Zuul 1 的胜利,不是"同一套设计优化了一下",而是编程范式代差------阻塞 Servlet 模型 vs 响应式 Netty 模型。理解了这一点,原理 / 设计 / 实现 / 使用四个维度的所有差异都是同一个根因的展开。


3. 原理对比:阻塞 Servlet vs 响应式 Netty ★★★

3.1 两种线程模型

Zuul 1.x:同步阻塞 Servlet 模型

  • 部署在 Servlet 容器(Tomcat)上,本质是一串 javax.servlet.FilterZuulServlet 注册为 Servlet)。
  • 每个请求独占一个 Tomcat 工作线程 :线程发起下游 HTTP 调用后阻塞等待响应返回,期间线程什么都干不了。
  • 默认线程池只有 200 个线程(server.tomcat.threads.max=200):并发请求数一旦超过线程数,请求开始排队,再往上就是拒绝。线程数调大又会带来上下文切换与内存开销。
  • 因为请求---响应对应关系被线程"钉死",不支持 WebSocket、不支持 HTTP/2 多路复用

Gateway:异步非阻塞响应式模型

  • 基于 Spring WebFlux + Project Reactor + Reactor Netty,没有 Servlet 容器 (引入 spring-boot-starter-webflux 而非 starter-web,跑在 Netty 上)。
  • 少量 EventLoop 线程(默认 ≈ CPU 核数 × 2)通过事件驱动 + 回调处理大量连接:发起下游调用后线程立即返回去处理别的请求,I/O 完成时由回调继续后续处理。线程永远不"等"。
  • 因此天然支持 WebSocket、HTTP/2、SSE 等长连接 / 多路复用协议。

#mermaid-svg-va3ytG1g2XJEiGlH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-va3ytG1g2XJEiGlH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-va3ytG1g2XJEiGlH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-va3ytG1g2XJEiGlH .error-icon{fill:#552222;}#mermaid-svg-va3ytG1g2XJEiGlH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-va3ytG1g2XJEiGlH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-va3ytG1g2XJEiGlH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-va3ytG1g2XJEiGlH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-va3ytG1g2XJEiGlH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-va3ytG1g2XJEiGlH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-va3ytG1g2XJEiGlH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-va3ytG1g2XJEiGlH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-va3ytG1g2XJEiGlH .marker.cross{stroke:#333333;}#mermaid-svg-va3ytG1g2XJEiGlH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-va3ytG1g2XJEiGlH p{margin:0;}#mermaid-svg-va3ytG1g2XJEiGlH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-va3ytG1g2XJEiGlH .cluster-label text{fill:#333;}#mermaid-svg-va3ytG1g2XJEiGlH .cluster-label span{color:#333;}#mermaid-svg-va3ytG1g2XJEiGlH .cluster-label span p{background-color:transparent;}#mermaid-svg-va3ytG1g2XJEiGlH .label text,#mermaid-svg-va3ytG1g2XJEiGlH span{fill:#333;color:#333;}#mermaid-svg-va3ytG1g2XJEiGlH .node rect,#mermaid-svg-va3ytG1g2XJEiGlH .node circle,#mermaid-svg-va3ytG1g2XJEiGlH .node ellipse,#mermaid-svg-va3ytG1g2XJEiGlH .node polygon,#mermaid-svg-va3ytG1g2XJEiGlH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-va3ytG1g2XJEiGlH .rough-node .label text,#mermaid-svg-va3ytG1g2XJEiGlH .node .label text,#mermaid-svg-va3ytG1g2XJEiGlH .image-shape .label,#mermaid-svg-va3ytG1g2XJEiGlH .icon-shape .label{text-anchor:middle;}#mermaid-svg-va3ytG1g2XJEiGlH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-va3ytG1g2XJEiGlH .rough-node .label,#mermaid-svg-va3ytG1g2XJEiGlH .node .label,#mermaid-svg-va3ytG1g2XJEiGlH .image-shape .label,#mermaid-svg-va3ytG1g2XJEiGlH .icon-shape .label{text-align:center;}#mermaid-svg-va3ytG1g2XJEiGlH .node.clickable{cursor:pointer;}#mermaid-svg-va3ytG1g2XJEiGlH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-va3ytG1g2XJEiGlH .arrowheadPath{fill:#333333;}#mermaid-svg-va3ytG1g2XJEiGlH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-va3ytG1g2XJEiGlH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-va3ytG1g2XJEiGlH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-va3ytG1g2XJEiGlH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-va3ytG1g2XJEiGlH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-va3ytG1g2XJEiGlH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-va3ytG1g2XJEiGlH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-va3ytG1g2XJEiGlH .cluster text{fill:#333;}#mermaid-svg-va3ytG1g2XJEiGlH .cluster span{color:#333;}#mermaid-svg-va3ytG1g2XJEiGlH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-va3ytG1g2XJEiGlH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-va3ytG1g2XJEiGlH rect.text{fill:none;stroke-width:0;}#mermaid-svg-va3ytG1g2XJEiGlH .icon-shape,#mermaid-svg-va3ytG1g2XJEiGlH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-va3ytG1g2XJEiGlH .icon-shape p,#mermaid-svg-va3ytG1g2XJEiGlH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-va3ytG1g2XJEiGlH .icon-shape .label rect,#mermaid-svg-va3ytG1g2XJEiGlH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-va3ytG1g2XJEiGlH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-va3ytG1g2XJEiGlH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-va3ytG1g2XJEiGlH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Gateway 响应式非阻塞
异步 I/O + 回调
异步 I/O + 回调
客户端 1
Netty EventLoop 线程
客户端 2
下游服务 A
下游服务 B
Zuul 1.x 同步阻塞
阻塞等待响应
阻塞等待响应
客户端
Tomcat 线程 1
客户端
Tomcat 线程 2
下游服务 A
下游服务 B

Zuul 2.x 为什么"理论上先进、实际上出局" :Zuul 2 也改成了 Netty 异步模型(过滤器类型变为 inbound / endpoint / outbound,上下文用 SessionContext 代替 RequestContext),与 Zuul 1 完全不兼容。但 Spring 团队 2018 年评估后决定自己造 Gateway(WebFlux 生态 + 响应式契约),不再集成 Netflix 组件。2021 年 Zuul 2 GA 时,Spring Cloud 2020.0 已经移除了 Zuul。

关键认知 :Gateway 的吞吐优势来自"线程不阻塞 ":同一个 EventLoop 线程可以同时服务成百上千个请求,瓶颈从"线程数"转移到"内存与 CPU"。代价是整个过滤器链必须是非阻塞的 ------链上任何一处同步阻塞调用(JDBC、RestTemplate、文件 I/O)都会卡死 EventLoop 线程,把 Gateway 的性能拉回 Zuul 1 水平。

3.2 性能表现(第三方基准数据)

基准一:4 核 8G 虚拟机,JMeter 压测,100 并发持续 5 分钟(Gateway vs Zuul 1.x):

指标 Zuul 1.x Spring Cloud Gateway
平均延迟 120 ms 45 ms
QPS 上限 ~4,800(难以突破 5,000) 22,000+
CPU 使用率 85% 60%
内存占用 320 MB 280 MB

基准二:2025 年对比(Gateway vs Zuul 2):

指标 Spring Cloud Gateway Netflix Zuul 2
平均延迟 12 ms 25 ms
最大吞吐量 15,000 RPS 8,000 RPS
CPU 使用率 更低(响应式技术栈) 更高

一句话概括 :多份第三方基准一致显示 Gateway 吞吐约为 Zuul 1 的 3~5 倍 、Zuul 2 的 约 2 倍,延迟显著更低。注意 Zuul 2 虽然同为 Netty,但响应式生态成熟度(连接池、背压、WebFlux 工具链)不如 Gateway。


4. 设计对比:路由与过滤器模型 ★★★

4.1 核心抽象:三要素 vs 四类过滤器

Gateway:路由三要素(全部面向"声明式配置"设计)

要素 含义 内置能力举例
Route(路由) id + uri + predicates + filters 的组合,一条转发规则 lb://user-service 走负载均衡,http://host:port 直连
Predicate(断言) 匹配请求的谓词,java.util.function.Predicate<ServerWebExchange> Path / Host / Method / Header / Query / Cookie / RemoteAddr / Weight / Before / After / Between
Filter(过滤器) 对请求/响应做加工 StripPrefix / AddRequestHeader / RewritePath / RequestRateLimiter / CircuitBreaker / Retry

一条路由 = 一组断言全部通过 → 走这组过滤器 → 转发到 uri。规则完全数据化,天然适合配置中心动态下发。

Zuul 1.x:四类过滤器(面向"请求生命周期阶段"设计)

类型 执行时机 典型过滤器
pre 路由转发之前 鉴权、限流、日志(PreDecorationFilter,order=5)
route 真正转发请求 RibbonRoutingFilter(order=10,走 Ribbon 选实例)/ SimpleHostRoutingFilter(order=100,直连 URL)
post 响应返回之后 SendResponseFilter(order=1000,写回响应)
error 任意阶段抛异常 SendErrorFilter(order=0)

一个过滤器通过 filterType() 声明自己属于哪个阶段,同阶段内按 filterOrder() 排序执行。路由匹配和转发逻辑本身也是过滤器PreDecorationFilter 负责路由匹配并把结果写进 RequestContext),与业务过滤器混在同一套机制里------这是 Zuul 设计上最"糊"的地方:路由规则和过滤器逻辑没有分层。

关键认知 :Gateway 把"匹配什么 "(Predicate)和"转发后做什么"(Filter)从代码里抽出来变成数据,路由本身是一等公民;Zuul 把一切都建模成过滤器,路由规则只是 pre 阶段某个过滤器算出来的中间结果。Gateway 的动态路由、Actuator 运行时管理能力,都源于这个抽象上的分层。

4.2 动态路由与配置方式

能力 Zuul 1.x Gateway
静态路由配置 zuul.routes.<id>.path/serviceId(properties/yml) spring.cloud.gateway.routes(yml)或 Java DSL(RouteLocatorBuilder
服务发现集成 DiscoveryClientRouteLocatorserviceId 直接对应 Eureka 服务名 lb://服务名 + ReactiveLoadBalancerClientFilter
运行时增删改路由 ❌ 需改配置 + refresh,不支持 API 级增删改 ✅ Actuator:POST /actuator/gateway/routes/{id}DELETE /actuator/gateway/routes/{id}
配置变更自动生效 需配合 @RefreshScope + /actuator/refresh CachingRouteLocator 监听 RefreshScopeRefreshedEvent / EnvironmentChangeEvent 自动失效缓存
脚本化扩展过滤器 ✅ Groovy 脚本动态加载(FilterFileManager 轮询目录) ❌ 无(可通过配置中心 + 重启或自定义扩展替代)

Zuul 1 唯一的动态优势是 Groovy 脚本过滤器(zuul.filters.root 指定目录,轮询热加载),但 Groovy 脚本在 Boot 3 生态已无意义。Gateway 的动态路由是内置的运行时能力,配合 Nacos/Apollo 可以做规则热下发。


5. 实现对比:请求处理链路源码解析 ★★★

5.1 Zuul 1.x:ZuulServlet 过滤器链

入口ZuulServlet(Netflix 开源 Netflix/zuul),被 @EnableZuulProxy 注册为 Servlet。service() 方法把一次请求切分为三段式:

java 复制代码
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
    try {
        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
        RequestContext context = RequestContext.getCurrentContext();   // ThreadLocal 上下文
        try {
            preRoute();          // ① 依次执行所有 pre 过滤器
        } catch (ZuulException e) { error(e); postRoute(); return; }
        try {
            route();             // ② 执行 route 过滤器(真正转发)
        } catch (ZuulException e) { error(e); postRoute(); return; }
        try {
            postRoute();         // ③ 执行 post 过滤器
        } catch (ZuulException e) { error(e); }
    } catch (Throwable e) {
        error(new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_FROM_FILTER_" + e.getClass().getName()));
    }
}

过滤器调度ZuulRunnerFilterProcessor.runFilters(type)FilterLoader.getFiltersByType(type),返回按 filterOrder() 排好序的列表逐个执行:

java 复制代码
public Object runFilters(String sType) throws Throwable {
    List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
    for (ZuulFilter zuulFilter : list) {
        Object result = processZuulFilter(zuulFilter);   // shouldFilter() 通过才 run()
        // 同步调用:run() 返回 null 表示继续,抛异常进入 error 阶段
    }
}

转发核心RibbonRoutingFilter.run()(Spring Cloud Netflix 扩展),同步调用链:Ribbon 选实例 → Apache HttpClient 同步发请求 → 线程阻塞等待响应

java 复制代码
public Object run() {
    RequestContext context = RequestContext.getCurrentContext();
    RibbonCommandContext commandContext = buildCommandContext(context);  // 封装 method/uri/headers/body
    ClientHttpResponse response = forward(commandContext);   // 同步阻塞:Ribbon LoadBalancer + CloseableHttpClient
    setResponse(response);                                    // 写回 RequestContext,交给 post 过滤器
    return response;
}

请求上下文RequestContext extends ConcurrentHashMap<String, Object>,挂在 ThreadLocal 上------同步模型的典型做法,每个线程一份,这也是"请求必须被一个线程从头跟到尾"的底层原因。

text 复制代码
浏览器请求
   │
   ▼
Tomcat 容器线程(每请求独占一个,同步阻塞)
   │
   ▼
ZuulServlet.service()
   ├─ ① preRoute()  → pre 过滤器:PreDecorationFilter(路由匹配) → 自定义鉴权
   ├─ ② route()     → route 过滤器:RibbonRoutingFilter(Ribbon 选实例 + HttpClient 同步调用,线程阻塞在此)
   ├─ ③ postRoute() → post 过滤器:SendResponseFilter 写回响应
   └─ 异常          → error 过滤器:SendErrorFilter

5.2 Gateway:DispatcherHandler 响应式链路

入口 :请求从 Netty 进入 WebFlux 的 HttpHandler 适配层,再进入 Spring 容器:

text 复制代码
Netty(EventLoop 线程)
   │
   ▼
ReactorHttpHandlerAdapter        # Reactor Netty ↔ WebFlux 适配
   │
   ▼
HttpWebHandlerAdapter            # ServerHttpRequest/Response 封装
   │
   ▼
ExceptionHandlingWebHandler      # 统一异常兜底
   │
   ▼
FilteringWebHandler              # WebFilter 链(CORS 等,与网关过滤器是两套)
   │
   ▼
DispatcherHandler ──► RoutePredicateHandlerMapping.lookupRoute()   # ① 路由匹配
   │                      │ 遍历 RouteLocator 的所有 Route
   │                      │ 每个 Predicate.test(exchange) 全部通过 → 命中
   │                      ▼
   └──► FilteringWebHandler.handle()                               # ② 过滤器链执行
           │  globalFilters + route.getFilters() 合并、按 Order 排序
           ▼
        DefaultGatewayFilterChain
           │
           ├─ AuthGlobalFilter(order=-100,自定义鉴权)
           ├─ ReactiveLoadBalancerClientFilter(order=10150,把 lb://服务名 解析为实例地址)
           └─ NettyRoutingFilter(order=Integer.MAX_VALUE,最后一个:Reactor Netty 异步转发)
                    │
                    ▼
              NettyWriteResponseFilter(order=-1,把下游响应写回客户端)

① 路由匹配RoutePredicateHandlerMapping,order=1,WebFlux 里第一个被询问的 HandlerMapping):

java 复制代码
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
    return this.routeLocator.getRoutes()
            .filterWhen(route -> Flux.fromIterable(route.getPredicates())
                    .all(p -> p.test(exchange))        // 全部断言通过才算命中
                    .defaultIfEmpty(false))
            .next()                                    // 取第一个命中的路由
            .map(route -> validateRoute(route, exchange));
}

② 过滤器链执行FilteringWebHandler):全局过滤器和路由级过滤器合并后统一按 Order 排序,包装成一条响应式链:

java 复制代码
public Mono<Void> handle(ServerWebExchange exchange) {
    Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
    List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);  // GlobalFilter → GatewayFilterAdapter
    combined.addAll(route.getFilters());
    AnnotationAwareOrderComparator.sort(combined);      // 全局/局部统一排序,这是与 Zuul 分阶段执行的本质区别
    return new DefaultGatewayFilterChain(combined).filter(exchange);
}

③ 转发NettyRoutingFilterGlobalFilter)排在最后order = Integer.MAX_VALUE),用 Reactor Netty HttpClient 异步转发;lb:// 协议由 ReactiveLoadBalancerClientFilter 提前解析成真实 host:port。整个链路返回的是 Mono<Void>------没有任何一处线程阻塞

关键认知 :Gateway 的过滤器不是"分阶段"而是"统一排序 "------一个 GlobalFilter 想跑在路由过滤器中间,只要调自己的 getOrder()。转发(NettyRoutingFilter)只是 order 最大的一个普通过滤器。这个设计让限流、鉴权、日志等横切能力可以精确插在任何位置。

5.3 关键类关系图

#mermaid-svg-8zQMuEoQeWOQLIWn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8zQMuEoQeWOQLIWn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8zQMuEoQeWOQLIWn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8zQMuEoQeWOQLIWn .error-icon{fill:#552222;}#mermaid-svg-8zQMuEoQeWOQLIWn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8zQMuEoQeWOQLIWn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8zQMuEoQeWOQLIWn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8zQMuEoQeWOQLIWn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8zQMuEoQeWOQLIWn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8zQMuEoQeWOQLIWn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8zQMuEoQeWOQLIWn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8zQMuEoQeWOQLIWn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8zQMuEoQeWOQLIWn .marker.cross{stroke:#333333;}#mermaid-svg-8zQMuEoQeWOQLIWn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8zQMuEoQeWOQLIWn p{margin:0;}#mermaid-svg-8zQMuEoQeWOQLIWn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8zQMuEoQeWOQLIWn .cluster-label text{fill:#333;}#mermaid-svg-8zQMuEoQeWOQLIWn .cluster-label span{color:#333;}#mermaid-svg-8zQMuEoQeWOQLIWn .cluster-label span p{background-color:transparent;}#mermaid-svg-8zQMuEoQeWOQLIWn .label text,#mermaid-svg-8zQMuEoQeWOQLIWn span{fill:#333;color:#333;}#mermaid-svg-8zQMuEoQeWOQLIWn .node rect,#mermaid-svg-8zQMuEoQeWOQLIWn .node circle,#mermaid-svg-8zQMuEoQeWOQLIWn .node ellipse,#mermaid-svg-8zQMuEoQeWOQLIWn .node polygon,#mermaid-svg-8zQMuEoQeWOQLIWn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8zQMuEoQeWOQLIWn .rough-node .label text,#mermaid-svg-8zQMuEoQeWOQLIWn .node .label text,#mermaid-svg-8zQMuEoQeWOQLIWn .image-shape .label,#mermaid-svg-8zQMuEoQeWOQLIWn .icon-shape .label{text-anchor:middle;}#mermaid-svg-8zQMuEoQeWOQLIWn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8zQMuEoQeWOQLIWn .rough-node .label,#mermaid-svg-8zQMuEoQeWOQLIWn .node .label,#mermaid-svg-8zQMuEoQeWOQLIWn .image-shape .label,#mermaid-svg-8zQMuEoQeWOQLIWn .icon-shape .label{text-align:center;}#mermaid-svg-8zQMuEoQeWOQLIWn .node.clickable{cursor:pointer;}#mermaid-svg-8zQMuEoQeWOQLIWn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8zQMuEoQeWOQLIWn .arrowheadPath{fill:#333333;}#mermaid-svg-8zQMuEoQeWOQLIWn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8zQMuEoQeWOQLIWn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8zQMuEoQeWOQLIWn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8zQMuEoQeWOQLIWn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8zQMuEoQeWOQLIWn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8zQMuEoQeWOQLIWn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8zQMuEoQeWOQLIWn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8zQMuEoQeWOQLIWn .cluster text{fill:#333;}#mermaid-svg-8zQMuEoQeWOQLIWn .cluster span{color:#333;}#mermaid-svg-8zQMuEoQeWOQLIWn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8zQMuEoQeWOQLIWn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8zQMuEoQeWOQLIWn rect.text{fill:none;stroke-width:0;}#mermaid-svg-8zQMuEoQeWOQLIWn .icon-shape,#mermaid-svg-8zQMuEoQeWOQLIWn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8zQMuEoQeWOQLIWn .icon-shape p,#mermaid-svg-8zQMuEoQeWOQLIWn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8zQMuEoQeWOQLIWn .icon-shape .label rect,#mermaid-svg-8zQMuEoQeWOQLIWn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8zQMuEoQeWOQLIWn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8zQMuEoQeWOQLIWn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8zQMuEoQeWOQLIWn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Zuul 1.x Servlet 架构
pre
route
route
post
error
ZuulServlet
ZuulRunner
FilterProcessor
FilterLoader

按类型取过滤器
ZuulFilter
PreDecorationFilter
SimpleHostRoutingFilter
RibbonRoutingFilter

同步 HttpClient
SendResponseFilter
SendErrorFilter
RequestContext

ThreadLocal
Gateway 响应式架构
RoutePredicateHandlerMapping

order=1
RouteLocator 接口
CachingRouteLocator

缓存+事件失效
RouteDefinitionRouteLocator
GatewayProperties

yaml 配置
FilteringWebHandler
GlobalFilter
NettyRoutingFilter

order=MAX_VALUE
ReactiveLoadBalancerClientFilter

order=10150
Route: id+uri+predicates+filters


6. 使用对比:配置与自定义开发 ★★★

6.1 Zuul 完整示例

⚠️ 版本前提:Zuul 仅支持到 Spring Boot 2.3.12 + Spring Cloud Hoxton.SR12 + Java 8。Spring Cloud 2020.0 起已无此依赖。遗留系统须用 Hoxton.SR12 以上(早期 2.2.x 有 Spring4Shell CVE-2022-22965 风险)。

pom.xml

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
</parent>
<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<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>
<dependencies>
    <!-- Zuul 网关:内部依赖 spring-boot-starter-web,必须跑在 Tomcat 上 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

application.yml

yaml 复制代码
server:
  port: 8080
spring:
  application:
    name: zuul-gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
zuul:
  routes:
    user-service:                          # 路由 id,对应 Eureka 服务名
      path: /user/**
      service-id: user-service             # 转发目标:Eureka 上的服务
      strip-prefix: true                   # 转发前去掉 /user 前缀
    order-service:
      path: /order/**
      service-id: order-service
      sensitive-headers: Cookie,Set-Cookie # 不把 Cookie 透传给下游
  host:
    connect-timeout-millis: 3000
    socket-timeout-millis: 10000

启动类 + 自定义 pre 过滤器

java 复制代码
@SpringBootApplication
@EnableZuulProxy   // = @EnableZuulServer + 服务发现 + Ribbon 路由能力
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}

@Component
public class AuthPreFilter extends ZuulFilter {

    @Override
    public String filterType() { return "pre"; }   // 声明阶段:pre/route/post/error

    @Override
    public int filterOrder() { return 1; }         // 同阶段内的排序

    @Override
    public boolean shouldFilter() { return true; } // 是否执行本过滤器

    @Override
    public Object run() {                          // 同步方法,返回 null 表示继续
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String token = request.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            ctx.setSendZuulResponse(false);        // 短路:不再转发
            ctx.setResponseStatusCode(401);
            ctx.setResponseBody("{\"code\":401,\"msg\":\"未认证\"}");
        }
        return null;
    }
}

调用链路 :浏览器 GET /user/1 → Tomcat 线程 → ZuulServlet → pre(AuthPreFilter 鉴权)→ route(RibbonRoutingFilter:Ribbon 选实例 + 同步 HttpClient 调用 http://实例IP:端口/1)→ post(SendResponseFilter 写回)→ 浏览器。

6.2 Gateway 完整示例

✅ 版本前提:Spring Boot 3.3.x + Spring Cloud 2023.0.x(Gateway 4.1.x)+ Java 17。

⚠️ 不能同时引入 spring-boot-starter-web(Tomcat):WebFlux 与 MVC 冲突,Gateway 会直接启动失败。整个项目(含自定义过滤器)都必须走响应式 API。

pom.xml

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.5</version>
</parent>
<properties>
    <java.version>17</java.version>
    <spring-cloud.version>2023.0.3</spring-cloud.version>
</properties>
<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>
<dependencies>
    <!-- Gateway:内部依赖 spring-boot-starter-webflux,跑在 Reactor Netty 上 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!-- 服务发现 + lb:// 负载均衡 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <!-- 熔断过滤器(Resilience4j) -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
    </dependency>
    <!-- 限流过滤器(Redis 令牌桶) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>
</dependencies>

application.yml

yaml 复制代码
server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      # 默认过滤器:对所有路由生效
      default-filters:
        - name: RequestRateLimiter                      # Redis 令牌桶限流
          args:
            redis-rate-limiter.replenishRate: 10        # 每秒补充令牌数
            redis-rate-limiter.burstCapacity: 20        # 桶容量
            key-resolver: "#{@ipKeyResolver}"           # SpEL 引用限流 Key 解析器
      routes:
        - id: user-service
          uri: lb://user-service                        # 经 LoadBalancer 负载均衡转发
          predicates:
            - Path=/user/**
          filters:
            - StripPrefix=1                             # 去掉 /user 前缀
            - AddRequestHeader=X-From-Gateway, api-gateway
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**,/cart/**                   # 一条路由可挂多个路径断言
          filters:
            - name: CircuitBreaker                      # Resilience4j 熔断
              args:
                name: orderCB
                fallbackUri: forward:/fallback/order    # 熔断后转发到本地兜底路由
        - id: fallback-order
          uri: http://localhost:9999                    # 兜底服务
          predicates:
            - Path=/fallback/order
# 暴露网关管理端点:运行时增删改路由
management:
  endpoints:
    web:
      exposure:
        include: gateway

限流 Key 解析器

java 复制代码
@Bean
public KeyResolver ipKeyResolver() {
    return exchange -> Mono.just(
        Optional.ofNullable(exchange.getRequest().getRemoteAddress())
                .map(addr -> addr.getAddress().getHostAddress())
                .orElse("unknown"));
}

自定义 GlobalFilter(鉴权 + 耗时统计,插在转发之前)

java 复制代码
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String token = request.getHeaders().getFirst("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();   // 短路:直接写响应,不再走后续过滤器
        }
        long start = System.nanoTime();
        return chain.filter(exchange)                       // 响应式:继续执行后续链
                .then(Mono.fromRunnable(() ->               // 下游响应完成后回调
                    log.info("{} {} 耗时 {}ms", request.getMethod(), request.getPath(),
                             (System.nanoTime() - start) / 1_000_000)));
    }

    @Override
    public int getOrder() { return -100; }                  // 负值:保证在 NettyRoutingFilter(MAX_VALUE) 之前
}

自定义 GatewayFilterFactory(yml 里 - Logging 即可启用,配置化复用)

java 复制代码
@Component
@Slf4j
public class LoggingGatewayFilterFactory
        extends AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {

    public LoggingGatewayFilterFactory() { super(Config.class); }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            log.info("[Gateway] 进入过滤器:{} {}", request.getMethod(), request.getPath());
            return chain.filter(exchange);
        };
    }

    // yml 中 Logging=xxx 的参数会映射到这个内部类
    public static class Config {
        // 例如 private String marker; 需要的话加 getter/setter
    }
}

Java DSL 方式定义路由(与 yml 等价,类型安全)

java 复制代码
@Bean
public RouteLocator customRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
            .route("java-dsl-route", r -> r.path("/api/**")
                    .filters(f -> f.stripPrefix(1).addRequestHeader("X-From", "dsl"))
                    .uri("lb://user-service"))
            .build();
}

运行时动态路由(Actuator)

bash 复制代码
# 查看全部路由定义
curl http://localhost:8080/actuator/gateway/routes

# 运行时新增一条路由(无需重启)
curl -X POST http://localhost:8080/actuator/gateway/routes/vip-order \
     -H 'Content-Type: application/json' \
     -d '{"id":"vip-order","uri":"lb://order-service","predicates":["Path=/vip/order/**"],"filters":["StripPrefix=1"]}'

# 刷新路由缓存,使配置中心下发的新规则立即生效
curl -X POST http://localhost:8080/actuator/gateway/refresh

# 删除路由
curl -X DELETE http://localhost:8080/actuator/gateway/routes/vip-order

调用链路 :浏览器 GET /user/1 → Netty EventLoop → DispatcherHandlerRoutePredicateHandlerMappingPath=/user/** 命中 user-service 路由)→ FilteringWebHandler(限流 → 鉴权 → ReactiveLoadBalancerClientFilter 选实例 → NettyRoutingFilter 异步转发 http://实例IP:端口/1)→ NettyWriteResponseFilter 写回。全程线程不阻塞。

6.3 从 Zuul 迁移到 Gateway

步骤(官方推荐把网关迁移放在最后,因为它影响面最大):

  1. 替换依赖spring-cloud-starter-netflix-zuulspring-cloud-starter-gateway,删除 spring-boot-starter-web(Gateway 不能与 Tomcat 共存)。
  2. 翻译路由配置(对照表):
Zuul 配置 Gateway 配置
zuul.routes.<id>.path=/user/** routes[0].predicates[0]=Path=/user/**
zuul.routes.<id>.service-id=user-service routes[0].uri=lb://user-service
zuul.routes.<id>.strip-prefix=true filters[0]=StripPrefix=1
zuul.routes.<id>.sensitive-headers=Cookie filters[i]=RemoveRequestHeader=Cookie
  1. 重写过滤器 (核心难点):ZuulFilter(同步、四阶段)→ GlobalFilter(响应式、统一排序);Groovy 脚本过滤器 → 编译期 Java 过滤器或配置中心下发。
  2. 排查阻塞调用 :过滤器链里的 RestTemplate / JDBC / 文件 I/O 会卡死 EventLoop,必须换成 WebClient / R2DBC / Mono.fromCallable().subscribeOn(Schedulers.boundedElastic())
  3. 替换连带组件:Ribbon → Spring Cloud LoadBalancer、Hystrix → Resilience4j、Turbine → Micrometer + 监控系统。

7. 总览大表

维度 Zuul 1.x Zuul 2.x Spring Cloud Gateway
运行时 Servlet 容器(Tomcat) Netty Netty(Reactor Netty)
I/O 模型 同步阻塞 异步非阻塞 异步非阻塞(响应式)
线程模型 每请求一线程(默认 200) EventLoop 复用 EventLoop 复用
协议 HTTP/1.1 HTTP/1.1 HTTP/1.1、HTTP/2、WebSocket、SSE
核心抽象 pre/route/post/error 四类过滤器 inbound/endpoint/outbound 过滤器 Route + Predicate + Filter 三要素
路由与过滤器关系 路由匹配也是过滤器,混在一起 同左 分层:路由是数据,断言/过滤器挂在路由上
动态路由 ❌ 改配置 + refresh ✅ Actuator 运行时增删改查
扩展机制 ZuulFilter / Groovy 脚本热加载 ZuulFilter(Async) GlobalFilter / GatewayFilterFactory
服务发现 Eureka + Ribbon Netflix 内部体系 lb:// + Spring Cloud LoadBalancer
限流 自研 自研 内置 RequestRateLimiter(Redis 令牌桶)
熔断 Hystrix(已停更) 自研 内置 Resilience4j
重试 Ribbon 重试 自研 内置 Retry 过滤器
可观测性 自定义 Spectator Micrometer / Actuator / Prometheus
Spring Cloud 集成 到 Hoxton 为止 从未集成 ✅ 官方组件,随 Boot 3.x 持续演进
维护状态 2020-12 被移除 Netflix 维护模式 活跃维护,官方唯一推荐

8. 选型建议

场景 建议
新项目 / Boot 3.x 项目 只能选 Gateway(Zuul 在 Boot 3 生态根本不存在)
高并发、长连接(WS/HTTP2)、需要限流熔断开箱即用 ✅ Gateway
需要运行时动态改路由(配置中心下发) ✅ Gateway(Actuator 端点直接支持)
遗留 Boot 2.x 系统、短期维持不动 ⚠️ Zuul 1 可继续用,但需升级到 Hoxton.SR12 以上(安全补丁),并规划迁移
考虑 Zuul 2 ❌ 不建议任何新项目使用:无 Spring 集成、无社区生态

一句话概括新项目 Gateway 是唯一选项,旧项目把 Zuul → Gateway 迁移排进路线图------性能提升只是副产品,真正的收益是回到官方支持的生命周期里。


9. 来源

相关推荐
鲨鱼辣钊3 小时前
【FastAPI筑基-Day19】APScheduler定时任务全实战|自动执行、动态启停、后台常驻
java·spring·fastapi
东小西8 小时前
【SAA实战】第 3 篇 · 工具调用全攻略:把业务能力交给 Agent 自己调度
java·后端·spring
东小西8 小时前
【SAA实战】第 4 篇 · Agent 短期记忆:saver 让 Agent 跨轮记得住(threadId 隔离)
java·后端·spring
独泪了无痕11 小时前
SpringBoot Event事件机制,轻松实现业务解耦
spring boot·后端·spring
小鹿的周先生11 小时前
Spring-AI-第2篇-ChatClient 实战:使用 DeepSeek 完成第一次 AI 对话
java·人工智能·spring
程序员黎剑13 小时前
Spring-Bean生命周期-构造器访问Autowired字段为null
java·后端·spring
SQL-First布道者17 小时前
⚡Spring JDBC 完整体系 · 第 10 集 · 手搓 `BaseCondition`
java·spring boot·后端·spring·mybatis·spring jdbc
随遇而安zx18 小时前
SpringCloud---Spring Cloud 分布式任务调度(XXL-Job / ElasticJob)
分布式·spring cloud
霸道流氓气质19 小时前
Spring AI Function Calling实现原理与自定义函数
java·spring·microsoft