From 55c99b306e4f9e1a5f5873e2ecfb47c98e415367 Mon Sep 17 00:00:00 2001
From: bunny <1319900154@qq.com>
Date: Tue, 27 May 2025 16:37:01 +0800
Subject: [PATCH] =?UTF-8?q?:sparkles:=20gateway-=E8=B7=A8=E5=9F=9F?=
=?UTF-8?q?=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
cloud-demo/ReadMe.md | 558 ++++++++++++++++++
.../src/main/resources/application-route.yaml | 12 +
cloud-demo/images/image-20250527163525191.png | Bin 0 -> 4595 bytes
3 files changed, 570 insertions(+)
create mode 100644 cloud-demo/images/image-20250527163525191.png
diff --git a/cloud-demo/ReadMe.md b/cloud-demo/ReadMe.md
index 7ebf9f4..2bb3aaf 100644
--- a/cloud-demo/ReadMe.md
+++ b/cloud-demo/ReadMe.md
@@ -854,3 +854,561 @@ graph TD
- **统计时长**:60000ms
适用场景:适用于低流量但要求高可用的服务,可以快速响应偶发异常。
+
+## Gateway
+
+Spring Cloud Gateway 是基于 Spring 5、Spring Boot 2 和 Project Reactor 构建的 API 网关,提供了一种简单有效的方式来路由到 API,并为它们提供跨领域的关注点,如:安全性、监控/指标和弹性。
+
+> [!WARNING]
+>
+> **不要同时引入 `spring-boot-starter-web` 和 `spring-cloud-starter-gateway`**
+>
+> 报错原因是项目中同时存在了两个不兼容的 Web 框架:
+>
+> 1. Spring MVC (基于 Servlet API 的阻塞式编程模型)
+> 2. Spring Cloud Gateway (基于 Reactor 的非阻塞式编程模型)
+
+### 冲突分析与解决方案
+
+#### 原因分析
+
+1. **编程模型冲突**:
+ - Spring MVC 使用传统的同步阻塞式 I/O 模型
+ - Spring Cloud Gateway 基于 Project Reactor 和 Netty,使用异步非阻塞式响应式编程模型
+ - 这两种模型在底层处理 HTTP 请求的方式完全不同,无法共存
+
+2. **自动配置机制**:
+ - Spring Boot 的自动配置会根据类路径上的依赖自动配置应用
+ - 当检测到 `spring-boot-starter-web` 时,会配置为 Servlet 容器(如 Tomcat)
+ - 当检测到 `spring-cloud-starter-gateway` 时,会期望配置为 Netty 服务器
+
+3. **Web 应用类型冲突**:
+ - Spring Boot 2.x 引入了 `WebApplicationType` 概念,可以是 SERVLET、REACTIVE 或 NONE
+ - 系统无法自动决定应该使用哪种类型,因为发现了两种冲突的实现
+
+#### 解决方案
+
+##### 推荐方案
+
+在 `application.properties` 或 `application.yml` 中明确指定应用类型:
+
+```properties
+spring.main.web-application-type=reactive
+```
+
+##### 替代方案
+
+1. 完全移除 `spring-boot-starter-web` 依赖
+2. 如果需要某些 Spring MVC 的功能,考虑使用 `spring-boot-starter-webflux` 替代
+
+### 项目创建与基本配置
+
+#### 依赖配置
+
+```xml
+
+ org.springframework.cloud
+ spring-cloud-starter-gateway
+
+
+ org.springframework.cloud
+ spring-cloud-starter-loadbalancer
+
+```
+
+#### 基础配置示例
+
+```yaml
+server:
+ port: 8888
+
+spring:
+ application:
+ name: gateway
+ cloud:
+ nacos:
+ server-addr: 192.168.3.150:8848
+ gateway:
+ discovery:
+ locator:
+ enabled: true # 开启服务发现自动路由
+```
+
+### 路由配置详解
+
+Spring Cloud Gateway 提供了强大而灵活的路由功能,通过合理配置断言和过滤器,可以实现复杂的API网关需求。遵循响应式编程模型,确保不要与传统的Spring MVC混合使用,是成功使用Gateway的关键。
+
+#### 基本路由配置
+
+路由规则按从上到下的顺序加载,第一个匹配的规则将被执行(除非自定义规则顺序)。
+
+```yaml
+spring:
+ cloud:
+ gateway:
+ routes:
+ - id: order-route
+ uri: lb://service-order # lb://表示使用负载均衡
+ predicates:
+ - Path=/api/order/**
+ filters:
+ - StripPrefix=1 # 去掉前缀/api
+ - id: product-route
+ uri: lb://service-product
+ predicates:
+ - Path=/api/product/**
+ order: 1 # 优先级,数字越小优先级越高
+```
+
+#### URI 类型说明
+
+- `lb://service-name`: 通过服务发现进行负载均衡
+- `http://host:port`: 直接HTTP请求
+- `https://host:port`: 直接HTTPS请求
+
+### 断言(Predicates)详解
+
+断言用于定义路由的匹配条件,支持多种匹配方式。
+
+更多配置参考下面图片,全局搜索`RoutePredicateFactory`,之后`Ctrl+H`找到所有方法。
+
+
+
+#### 常用断言类型
+
+##### Path 断言
+
+```yaml
+predicates:
+ - Path=/api/product/**
+```
+
+或长格式:
+
+```yaml
+predicates:
+ - name: Path
+ args:
+ patterns: /api/product/**
+ matchTrailingSlash: true # 是否匹配结尾斜杠
+```
+
+##### Query 断言
+
+匹配请求参数:
+
+```yaml
+predicates:
+ - Query=q, 被世界温柔以待 # 参数名和正则表达式
+```
+
+长格式:
+
+```yaml
+predicates:
+ - name: Query
+ args:
+ param: q
+ regexp: 被世界温柔以待
+```
+
+##### Method 断言
+
+匹配HTTP方法:
+
+```yaml
+predicates:
+ - Method=GET,POST
+```
+
+##### Header 断言
+
+匹配请求头:
+
+```yaml
+predicates:
+ - Header=X-Request-Id, \d+ # 匹配数字
+```
+
+##### Cookie 断言
+
+匹配Cookie:
+
+```yaml
+predicates:
+ - Cookie=sessionid, abc.*
+```
+
+##### Host 断言
+
+匹配Host头:
+
+```yaml
+predicates:
+ - Host=**.example.com
+```
+
+##### 时间相关断言
+
+- After: 指定时间之后
+- Before: 指定时间之前
+- Between: 两个时间之间
+
+```yaml
+predicates:
+ - After=2023-01-20T17:42:47.789-07:00[America/Denver]
+```
+
+#### 自定义断言
+
+```java
+@Component
+public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory {
+
+ public VipRoutePredicateFactory() {
+ super(Config.class);
+ }
+
+ @Override
+ public List shortcutFieldOrder() {
+ return List.of("param", "value");
+ }
+
+ @Override
+ public Predicate apply(Config config) {
+ return (GatewayPredicate) serverWebExchange -> {
+ ServerHttpRequest request = serverWebExchange.getRequest();
+
+ String first = request.getQueryParams().getFirst(config.param);
+
+ return StringUtils.hasText(first) && first.equals(config.value);
+ };
+ }
+
+ @Getter
+ @Setter
+ @Validated
+ public static class Config {
+ @NotEmpty
+ private String param;
+
+ @NotEmpty
+ private String value;
+ }
+}
+```
+
+##### 配置使用方式
+
+**短写法配置**
+
+```
+predicates:
+ - Vip=user,bunny
+```
+
+**长写法配置**
+
+```
+predicates:
+ - name: Vip
+ args:
+ param: user
+ value: bunny
+```
+
+### 过滤器(Filters)详解
+
+过滤器用于修改请求和响应,可以在路由前后执行。
+
+> [!TIP]
+>
+> [更多参考文档](https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/gatewayfilter-factories/addrequestheader-factory.html)
+
+#### 常用内置过滤器
+
+##### 路径相关
+
+- `StripPrefix`: 去掉前缀
+
+ ```yaml
+ filters:
+ - StripPrefix=2 # 去掉前两级路径
+ ```
+
+- `PrefixPath`: 添加前缀
+
+ ```yaml
+ filters:
+ - PrefixPath=/api
+ ```
+
+##### 请求头相关
+
+> [!NOTE]
+>
+> 在有的浏览器中看不到`X-Request-red`,因为`x-`开头可能是自定义请求头,有的浏览器防止泄露隐私就隐藏了,需要在指定的下游接口中打印可以看到。
+
+- `AddRequestHeader`: 添加请求头
+
+ ```yaml
+ filters:
+ - AddRequestHeader=X-Request-red, blue
+ ```
+
+- `RemoveRequestHeader`: 移除请求头
+
+- `SetRequestHeader`: 设置请求头
+
+如果访问这个接口,可以输出`Received headers: blue`
+
+```java
+@Operation(summary = "读取配置")
+@GetMapping("config")
+public String config(HttpServletRequest request) {
+ String timeout = orderProperties.getTimeout();
+ String autoConfirm = orderProperties.getAutoConfirm();
+ String dbUrl = orderProperties.getDbUrl();
+
+ // 携带的请求头内容
+ String header = request.getHeader("X-Request-red");
+ log.info("Received headers: {}", header);
+
+ return "timeout:" + timeout + "\nautoConfirm:" + autoConfirm + "\norder.db-url" + dbUrl;
+}
+```
+
+##### 响应头相关
+
+- `AddResponseHeader`: 添加响应头
+- `RemoveResponseHeader`: 移除响应头
+- `SetResponseHeader`: 设置响应头
+
+##### 重定向相关
+
+- `RedirectTo`: 重定向
+
+ ```yaml
+ filters:
+ - RedirectTo=302, https://example.org
+ ```
+
+##### 断路器相关
+
+- `CircuitBreaker`: 断路器
+
+ ```yaml
+ filters:
+ - name: CircuitBreaker
+ args:
+ name: myCircuitBreaker
+ fallbackUri: forward:/fallback
+ ```
+
+##### 重试相关
+
+- `Retry`: 重试机制
+
+ ```yaml
+ filters:
+ - name: Retry
+ args:
+ retries: 3
+ statuses: BAD_GATEWAY
+ ```
+
+### 全局过滤器
+
+可以应用于所有路由的过滤器,常用于认证、日志等全局功能。
+
+```java
+@Bean
+public GlobalFilter customGlobalFilter() {
+ return (exchange, chain) -> {
+ // 前置处理
+ return chain.filter(exchange).then(Mono.fromRunnable(() -> {
+ // 后置处理
+ }));
+ };
+}
+```
+
+### 高级特性
+
+#### 自定义过滤器
+
+实现`GlobalFilter`
+
+```java
+@Slf4j
+@Component
+public class RTFilter implements GlobalFilter, Ordered {
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
+ ServerHttpRequest request = exchange.getRequest();
+ URI uri = request.getURI();
+ long start = System.currentTimeMillis();
+ log.error("请求【{}】开始时间:{}", uri, start);
+
+ // 处理逻辑
+ // return chain.filter(exchange)
+ // // 因为是异步的,不能写在下main,需要处理后续逻辑写在 doFinally
+ // .doFinally(result -> {
+ // long end = System.currentTimeMillis();
+ // log.error("请求【{}】结束 ,时间:{},耗时:{}", uri, end, end - start);
+ // });
+ return chain.filter(exchange)
+ .doOnError(e -> log.error("请求失败", e))
+ .doFinally(result -> {
+ long end = System.currentTimeMillis();
+ log.info("请求【{}】结束,状态:{},耗时:{}ms",
+ uri, result, end - start);
+ });
+ }
+
+ @Override
+ public int getOrder() {
+ return 0; // 执行顺序
+ }
+}
+```
+
+#### 自定义过滤器工厂
+
+添加完成后再请求头中添加`X-Response-Token`
+
+
+
+```java
+@Component
+public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
+
+ @Override
+ public GatewayFilter apply(NameValueConfig config) {
+ // 每次相应之前添加一次性令牌
+
+ return (exchange, chain) -> chain.filter(exchange)
+ .then(Mono.fromRunnable(() -> {
+ ServerHttpResponse response = exchange.getResponse();
+ HttpHeaders headers = response.getHeaders();
+
+ String name = config.getName();
+ String value = config.getValue();
+
+ if ("uuid".equalsIgnoreCase(value)) {
+ value = UUID.randomUUID().toString();
+ }
+
+ if ("jwt".equalsIgnoreCase(value)) {
+ value = "JWT的token";
+ }
+
+ headers.add(name, value);
+ }));
+ }
+}
+```
+
+**配置中设置**
+
+```yaml
+spring:
+ cloud:
+ gateway:
+ routes:
+ - id: order-route
+ uri: lb://service-order
+ predicates:
+ - Path=/api/order/**
+ filters:
+ - AddRequestHeader=X-Request-red, blue
+ - OnceToken=X-Response-Token, uuid
+```
+
+#### 动态路由
+
+可通过数据库或配置中心实现动态路由更新。
+
+```java
+@Autowired
+private RouteDefinitionLocator routeDefinitionLocator;
+
+@Autowired
+private RouteDefinitionWriter routeDefinitionWriter;
+
+public void updateRoutes() {
+ // 获取新路由定义
+ List definitions = ...;
+
+ // 清空现有路由
+ routeDefinitionLocator.getRouteDefinitions()
+ .collectList()
+ .subscribe(existing -> existing.forEach(route ->
+ routeDefinitionWriter.delete(Mono.just(route.getId()))));
+
+ // 添加新路由
+ definitions.forEach(definition ->
+ routeDefinitionWriter.save(Mono.just(definition)).subscribe());
+}
+```
+
+#### 跨域设置
+
+请求后会出有下面的内容:
+
+
+
+```yaml
+spring:
+ cloud:
+ gateway:
+ # 全局跨域
+ globalcors:
+ cors-configurations:
+ '[/**]':
+ allowed-headers:
+ - "*"
+ allowed-origin-patterns:
+ - "*"
+ allowed-methods:
+ - "*"
+```
+
+**CORS问题**:在Gateway层统一配置CORS
+
+```yaml
+spring:
+ cloud:
+ gateway:
+ globalcors:
+ cors-configurations:
+ '[/**]':
+ allowedOrigins: "*"
+ allowedMethods:
+ - GET
+ - POST
+```
+
+### 最佳实践
+
+1. **路由组织**:按业务功能组织路由,使用清晰的ID命名
+2. **优先级管理**:合理使用order属性控制路由匹配顺序
+3. **异常处理**:配置全局异常处理和fallback路由
+4. **监控**:集成Actuator监控端点
+5. **性能**:合理使用缓存和断路器
+
+### 常见问题
+
+1. **超时配置**:
+
+ ```yaml
+ spring:
+ cloud:
+ gateway:
+ httpclient:
+ connect-timeout: 1000
+ response-timeout: 5s
+ ```
+
+2. **负载均衡**:确保正确配置服务发现和负载均衡器
diff --git a/cloud-demo/gateway/src/main/resources/application-route.yaml b/cloud-demo/gateway/src/main/resources/application-route.yaml
index 6affeca..355efeb 100644
--- a/cloud-demo/gateway/src/main/resources/application-route.yaml
+++ b/cloud-demo/gateway/src/main/resources/application-route.yaml
@@ -1,6 +1,18 @@
spring:
cloud:
gateway:
+ # 全局跨域
+ globalcors:
+ cors-configurations:
+ '[/**]':
+ allowed-headers:
+ - "*"
+ allowed-origin-patterns:
+ - "*"
+ allowed-methods:
+ - "*"
+
+ # 路由
routes:
- id: order-route
uri: lb://service-order
diff --git a/cloud-demo/images/image-20250527163525191.png b/cloud-demo/images/image-20250527163525191.png
new file mode 100644
index 0000000000000000000000000000000000000000..c555ead11ba7e927d97e63611056763f7dd935db
GIT binary patch
literal 4595
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