java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringCloud 服务网关

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

作者:我登哥MVP

这段文章主要SpringCloudGateway作为微服务网接口层的统一入口,替代Nginx,实现动态路由、鉴权与限流等功能,感兴趣的朋友跟随小编一起看看吧

技术栈: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(动态路由/鉴权) → 微服务
NginxSpring Cloud Gateway
实现C 语言Java(Netty)
动态路由需 reload实时生效 ✅
与注册中心集成需 Lua 扩展原生支持 ✅

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

Zuul 1.xGateway
架构同步 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 步骤 ①:引入依赖

<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 配置路由

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-servicelb:// 是 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,zzyyCookie 包含 username=zzyy
Header- Header=X-Request-Id, \\d+Header 包含 X-Request-Id 且值为数字
Host- Host=**.atguigu.comHost 以 .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 步骤 ④:自定义断言 — 会员等级限制

// 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 步骤 ⑤:自定义局部过滤器 — 参数校验

// 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 步骤 ⑥:全局过滤器 — 请求日志

// 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

// 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 有什么区别?如何配合使用?

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

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

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

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

- 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
🔴 路由不生效请求 404predicates 路径与请求路径不匹配检查 /** vs /* 的区别
🔴 lb:// 不生效UnknownHostException未注册到 Consul 或服务名拼写错误检查 Provider 注册状态
🔴 自定义过滤器不生效return 了 Mono.empty()正确的拦截姿势是 exchange.getResponse().setComplete()参考标准写法
🔴 Spring Boot 3.xNoClassDefFoundError: javaxJakarta 迁移所有 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 FilterPre 在转发前执行,Post 用 chain.filter().then() 实现
Zuul已停更,Gateway 是官方替代(异步非阻塞 Netty)

到此这篇关于SpringCloud 核心组件解析之服务网关的文章就介绍到这了,更多相关SpringCloud 服务网关内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文