SpringCloud 核心组件解析:服务网关

SpringCloud 核心组件解析:服务网关

技术栈 :Spring Boot 3.2.0 + Spring Cloud 2023.0.0 + Spring Cloud Gateway

已不维护 :Netflix Zuul → 替代用 Gateway

前置网关:Nginx(简要介绍,通常作为 Gateway 的前置入口)


6.1 是什么 --- 网关的核心概念

6.1.1 生活化类比:小区门卫

复制代码
          ┌──────────┐
访客 ───→ │ 小区门卫  │ ───→ 各栋楼(微服务)
          │          │
          │ 职责:    │
          │ ① 查身份 │ ← 鉴权/认证
          │ ② 指路    │ ← 路由转发
          │ ③ 登记    │ ← 日志/监控
          │ ④ 限流    │ ← 防止太多人同时涌入
          └──────────┘

网关 = 系统的统一入口,所有外部请求先到网关,再由网关转发到内部微服务。

6.1.2 网关 vs 直连

复制代码
没有网关:用户 → http://192.168.1.10:8001/pay/get/1  ❌ 暴露内部 IP
有了网关:用户 → http://gateway.company.com/pay/get/1   ✅ 统一入口
                   │
                   ▼
               Gateway :9527 → Provider :8001/:8002

6.2 为什么 --- 从 Nginx 到 Zuul 到 Gateway

6.2.1 Nginx(了解即可)

Nginx 是高性能反向代理,通常作为 Gateway 的前置层:

复制代码
用户 → Nginx(SSL终结/静态资源) → Gateway(动态路由/鉴权) → 微服务
Nginx Spring Cloud Gateway
实现 C 语言 Java(Netty)
动态路由 需 reload 实时生效 ✅
与注册中心集成 需 Lua 扩展 原生支持 ✅

6.2.2 Netflix Zuul(已停更,了解即可)

Zuul 1.x Gateway
架构 同步 Servlet 异步 Netty
线程模型 一个请求一个线程 事件驱动
Spring Cloud 集成 停止维护 主推 ✅

6.3 怎么做 --- Gateway 完整实战

6.3.0 小 Demo:先暴露痛点

假设系统有 10 个微服务,各自不同端口。前端要记住 10 个 IP:Port → 维护噩梦 ;每个服务都要自己处理跨域、鉴权 → 代码重复 ;无法统一限流 → 安全风险

引入 Gateway 后:一个端口(9527)→ 统一入口 → 路径路由 → 自动转发

6.3.1 三板斧核心模型

复制代码
Route(路由)
  ├── id:唯一标识
  ├── uri:目标地址(lb://服务名)
  ├── predicates:断言(匹配规则)
  └── filters:过滤器(请求修改)

6.3.2 步骤 ①:引入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

⚠️ Gateway 基于 WebFlux,不能引入 spring-boot-starter-web,两者冲突!

6.3.3 步骤 ②:YAML 配置路由

yaml 复制代码
server:
  port: 9527

spring:
  application:
    name: cloud-gateway
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true
        service-name: ${spring.application.name}
    gateway:
      routes:
        # 路由1:基础路由 + 自定义断言
        - id: pay_routh1
          uri: lb://cloud-payment-service
          predicates:
            - Path=/pay/gateway/get/**
            - MyRoutePredicatedFactory="3"

        # 路由2:另一个路径
        - id: pay_routh2
          uri: lb://cloud-payment-service
          predicates:
            - Path=/pay/gateway/info/**

        # 路由3:带过滤器
        - id: pay_routh3
          uri: lb://cloud-payment-service
          predicates:
            - Path=/pay/gateway/filter/**
          filters:
            - My=atguigu

关键配置解读

配置项 说明
lb://cloud-payment-service lb:// 是 Gateway 内置的负载均衡协议
Path=/pay/gateway/get/** 通配符匹配路径(Ant 风格)
filters 只对当前路由生效的局部过滤器

6.3.4 步骤 ③:11 种内置断言

断言 示例 说明
After - After=2025-01-01T00:00:00+08:00 在此时间之后的请求
Before - Before=2025-12-31T23:59:59+08:00 在此时间之前的请求
Between - Between=...,... 两个时间之间的请求
Cookie - Cookie=username,zzyy Cookie 包含 username=zzyy
Header - Header=X-Request-Id, \\d+ Header 包含 X-Request-Id 且值为数字
Host - Host=**.atguigu.com Host 以 .atguigu.com 结尾
Method - Method=GET 指定 HTTP 方法
Path - Path=/api/v1/** 按路径匹配(最常用)
Query - Query=username, \\w+ 查询参数包含 username 且值为字母
RemoteAddr - RemoteAddr=192.168.1.1/24 按来源 IP 匹配
Weight - Weight=group1, 8 按权重分流(灰度发布)

6.3.5 步骤 ④:自定义断言 --- 会员等级限制

java 复制代码
// cloud-gateway-9527/.../mygateway/MyRoutePredicatedFactory.java
@Component
public class MyRoutePredicatedFactory
        extends AbstractRoutePredicateFactory<MyRoutePredicatedFactory.Config> {

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

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            String usertype = exchange.getRequest()
                .getQueryParams().getFirst("userType");
            return "3".equals(usertype);  // 只有三级会员能访问
        };
    }

    @Validated
    public static class Config {
        @Getter @Setter @NotEmpty
        private String usertype;
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("userType");
    }
}

YAML 中:- MyRoutePredicatedFactory="3" → 请求必须带 ?userType=3

6.3.6 步骤 ⑤:自定义局部过滤器 --- 参数校验

java 复制代码
// cloud-gateway-9527/.../mygateway/MyGatewayFilterFactory.java
@Component
public class MyGatewayFilterFactory
        extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config> {

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

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            // 请求必须包含参数 atguigu
            if (exchange.getRequest().getQueryParams().containsKey("atguigu")) {
                return chain.filter(exchange);  // ✅ 放行
            } else {
                exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
                return exchange.getResponse().setComplete();  // ❌ 拦截
            }
        };
    }

    public static class Config {
        @Setter @Getter
        private String status;
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("status");
    }
}

6.3.7 步骤 ⑥:全局过滤器 --- 请求日志

java 复制代码
// cloud-gateway-9527/.../mygateway/MyGlobalFilter.java
@Component
@Slf4j
public class MyGlobalFilter implements GlobalFilter, Ordered {

    private static final String BEGIN_VISIT_TIME = "begin_visit_time";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Pre-filter:记录进入时间
        exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());

        // Post-filter:打印日志
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            Long beginTime = exchange.getAttribute(BEGIN_VISIT_TIME);
            if (beginTime != null) {
                log.info("访问接口: {}:{}{}  参数: {}  耗时: {}ms",
                    exchange.getRequest().getURI().getHost(),
                    exchange.getRequest().getURI().getPort(),
                    exchange.getRequest().getURI().getPath(),
                    exchange.getRequest().getURI().getRawQuery(),
                    System.currentTimeMillis() - beginTime);
            }
        }));
    }

    @Override
    public int getOrder() { return 0; }
}

Pre Filter vs Post Filter

复制代码
请求 → [Pre Filter: 鉴权/日志] → 业务处理 → [Post Filter: 修改响应/记日志] → 响应

chain.filter(exchange).then(...) 实现了 Post Filter 的效果。

6.3.8 Provider 端网关专用 Controller

java 复制代码
// cloud-provider-payment8001/.../controller/PayGateWayController.java
@RestController
public class PayGateWayController {

    @Resource
    private PayService payService;

    @GetMapping("/pay/gateway/get/{id}")
    public ResultData<Pay> getById(@PathVariable("id") Integer id) {
        return ResultData.success(payService.getById(id));
    }

    @GetMapping("/pay/gateway/get/info")
    public ResultData<String> getInfo() {
        return ResultData.success("gateway info test " + IdUtil.simpleUUID());
    }

    // 测试过滤器效果
    @GetMapping("/pay/gateway/filter")
    public ResultData<String> getFilter(HttpServletRequest request) {
        String result = "";
        Enumeration<String> names = request.getHeaderNames();
        while (names.hasMoreElements()) {
            String name = names.nextElement();
            String value = request.getHeader(name);
            if (name.equalsIgnoreCase("X-Request-atguigu1")
                || name.equalsIgnoreCase("X-Request-atguigu2")) {
                result += name + "\t" + value + " ";
            }
        }
        return ResultData.success("gateWayFilter 过滤器test:" + result
            + "\t" + DateUtil.now());
    }
}

6.4 深入原理 --- Gateway 内部架构

复制代码
┌──────────────────────────────────────────┐
│           Spring Cloud Gateway            │
│  ┌─────────────────────────────────────┐ │
│  │     RoutePredicateHandlerMapping    │ │ ← 匹配路由(遍历所有路由规则)
│  └──────────────┬──────────────────────┘ │
│                 ▼                         │
│  ┌─────────────────────────────────────┐ │
│  │     FilteringWebHandler             │ │ ← 执行过滤器链
│  │     (Pre Filter → Proxy → Post)     │ │
│  └──────────────┬──────────────────────┘ │
│                 ▼                         │
│  ┌─────────────────────────────────────┐ │
│  │     NettyRoutingFilter              │ │ ← 基于 Netty 异步非阻塞
│  └─────────────────────────────────────┘ │
└──────────────────────────────────────────┘

6.5 内置过滤器速查

过滤器 示例 说明
PrefixPath - PrefixPath=/pay 给路径加前缀
SetPath - SetPath=/pay/gateway/{segment} 重写路径
RedirectTo - RedirectTo=302, https://baidu.com 重定向
AddRequestHeader - AddRequestHeader=X-Custom, value1 添加请求头
RemoveRequestHeader - RemoveRequestHeader=sec-fetch-site 删除请求头
AddRequestParameter - AddRequestParameter=foo, bar 添加查询参数
AddResponseHeader - AddResponseHeader=X-Response, Blue 添加响应头
RemoveResponseHeader - RemoveResponseHeader=Content-Type 删除响应头
RequestRateLimiter 配合 Redis 限流 请求限流

6.6 面试题

Q1:Gateway 和 Nginx 有什么区别?如何配合使用?

  • Nginx 是 C 语言高性能反向代理,适合静态资源、SSL 卸载、最外层入口
  • Gateway 是 Java 实现,适合动态路由、鉴权、与注册中心原生集成
  • 配合方式:用户 → Nginx(SSL, 静态资源) → Gateway(动态路由, 鉴权) → 微服务

Q2:Gateway 的 Pre Filter 和 Post Filter 有什么区别?

:Pre Filter 在转发下游前执行(鉴权、限流、加请求头);Post Filter 在下游返回响应后执行(加响应头、记日志)。Post Filter 通过 chain.filter(exchange).then(...) 实现。

Q3:如何实现网关层面的灰度发布?

:使用 Gateway 的 Weight 路由,同一个路径按权重分流:

yaml 复制代码
- id: service_v1
  uri: lb://service
  predicates: [Path=/api/**, Weight=group1, 80]  # 80% 流量
- id: service_v2
  uri: lb://service-v2
  predicates: [Path=/api/**, Weight=group1, 20]  # 20% 流量

6.7 踩坑指南

现象 原因 解决
🔴 Gateway 与 Web 冲突 启动报错 Gateway 基于 WebFlux,不能引入 spring-boot-starter-web 移除 web starter
🔴 路由不生效 请求 404 predicates 路径与请求路径不匹配 检查 /** vs /* 的区别
🔴 lb:// 不生效 UnknownHostException 未注册到 Consul 或服务名拼写错误 检查 Provider 注册状态
🔴 自定义过滤器不生效 return 了 Mono.empty() 正确的拦截姿势是 exchange.getResponse().setComplete() 参考标准写法
🔴 Spring Boot 3.x NoClassDefFoundError: javax Jakarta 迁移 所有 javax.* 改为 jakarta.*

6.8 章节总结

要点 说明
三板斧 Route(路由规则)+ Predicate(匹配条件)+ Filter(请求处理)
核心配置 uri: lb://服务名 + predicates: Path=/xxx/**
11 种内置断言 After/Before/Cookie/Header/Host/Method/Path/Query/RemoteAddr/Weight 等
自定义组件 继承 AbstractRoutePredicateFactory(断言)/AbstractGatewayFilterFactory(局部过滤器)/ 实现 GlobalFilter(全局过滤器)
Pre/Post Filter Pre 在转发前执行,Post 用 chain.filter().then() 实现
Zuul 已停更,Gateway 是官方替代(异步非阻塞 Netty)
相关推荐
lulu12165440781 小时前
OpenAI 如何用开源前端生态为 GPT-5.6 铺路? - 微元算力(weytoken)
java·前端·人工智能·python·gpt·开源·ai编程
北城以北88882 小时前
RocketMQ简介
java·spring boot·后端·rocketmq
折哥的程序人生 · 物流技术专研9 小时前
Java面试85题图解版 · 特别篇:2026后端高频面试题复盘(算法底层逻辑+高并发架构设计全解析,附Java实战代码)
java·网络·数据库·算法·面试
GoGeekBaird9 小时前
从 Prompt Engineering 到 Loop Engineering,我觉得 AI 开发这事儿终于开始变味了
后端·github
一条泥憨鱼10 小时前
【Redis】数据类型和常用命令
java·数据库·redis·后端·缓存
云烟成雨TD10 小时前
Spring AI Alibaba 1.x 系列【78】沙箱(Sandbox)
java·人工智能·spring
程序员二叉10 小时前
【Java】 异常高频面试题精讲 | 易错点+对比总结
java·开发语言·面试
周航宇JoeZhou10 小时前
JB3-9-SpringAI(二)
java·ai·agent·多智能体·调度·智能体·观察
好家伙VCC10 小时前
Web Components主题热切换方案揭秘
java·前端