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`找到所有方法。 + +![image-20250527141438554](./images/image-20250527141438554.png) + +#### 常用断言类型 + +##### 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` + +![image-20250527163116327](./images/image-20250527163116327.png) + +```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()); +} +``` + +#### 跨域设置 + +请求后会出有下面的内容: + +![image-20250527163525191](./images/image-20250527163525191.png) + +```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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