一、问题出现
最近把一个 Spring Boot 3.x 的项目升级到 Spring Boot 4.0.6 + Spring Cloud 2025.1.x(对应 Gateway 5.x),顺便引入 Spring Cloud Gateway 做统一的 API 网关。本以为只是个常规的架构调整,结果折腾了一整个下午。
前端项目跑在 localhost:3000,网关跑在 8080,按理说配个路由和跨域就完事了。结果启动后遇到三个连环报错:
第一关:Gateway 启动就挂
ClassNotFoundException: org.springframework.boot.web.context.WebServerInitializedEvent
第二关:路由全 404
No RouteDefinition found for [Exchange: POST http://localhost:8080/api/auth/login/password]
第三关:跨域重复
Access-Control-Allow-Origin header contains multiple values 'http://localhost:3000, http://localhost:3000'
这三个问题其实是三个独立的原因,只是凑在一起爆发了。
二、问题原因
2.1 依赖坐标改了
第一个问题最直接。旧版 Gateway 的依赖是:
<!-- 旧版(Spring Cloud 2024.x 及之前) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
到了 5.x,这个 artifact 被重命名了:
<!-- 新版(Spring Cloud 2025.x / Gateway 5.x) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>
直接用旧坐标会导致类找不到 WebServerInitializedEvent——因为旧的 spring-cloud-starter-gateway 依赖了 spring-cloud-gateway-server,而后者在 5.x 中被拆分为 spring-cloud-gateway-server-webflux,包结构和类名都有变化。
2.2 配置前缀改了——这是最坑的
这是今天最大的坑。Gateway 5.x 把配置前缀改了:
版本
配置前缀
Gateway 4.x(旧)
spring.cloud.gateway
Gateway 5.x(新)
spring.cloud.gateway.server.webflux
看源码的 GatewayProperties.java 第 45 行:
public static final String PREFIX = "spring.cloud.gateway.server.webflux";
也就是说,路由配置要从:
# ❌ 旧写法——Gateway 5.x 不认
spring:
cloud:
gateway:
routes:
- id: auth-service
uri: http://localhost:8001
改成:
# ✅ 新写法
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: auth-service
uri: http://localhost:8001
不只是路由,globalcors 的配置也一样:
# ✅ globalcors 也在新前缀下面
spring:
cloud:
gateway:
server:
webflux:
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
我当时就纳闷,明明配置看起来没问题,为什么路由就是加载不到?看启动日志只看到 No RouteDefinition found,没有任何报错。翻源码才找到这个前缀变化。
2.3 跨域重复——Gateway 和下游服务同时在设
第三个问题最有意思。Gateway 的 RoutePredicateHandlerMapping 在匹配路由后,会自动根据 globalcors 配置往响应头里写 Access-Control-Allow-Origin。
但我的下游服务(auth-service、user-service 等)也各自配了 Spring Security 的 CORS:
// 每个服务的 SecurityConfig 都有这个
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
@Bean
public CorsConfigurationSource corsConfigurationSource() {
// ... 设置了 allowedOrigins、allowedMethods 等
}
结果就是:浏览器发一个请求,Gateway 写一次 CORS 头,下游服务又写一次。最终响应头里出现两个一模一样的 Access-Control-Allow-Origin: http://localhost:3000,浏览器直接拒绝。
三、解决问题
3.1 更换依赖坐标
把 pom.xml 中的 spring-cloud-starter-gateway 换成 spring-cloud-starter-gateway-server-webflux:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>
注意 Spring Cloud BOM 版本也要对得上:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2025.1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3.2 修正配置前缀
把 application.yml 中的所有 spring.cloud.gateway.* 配置移到 spring.cloud.gateway.server.webflux.* 下面。包括 routes 和 globalcors。
路由改前改后对比:
# ❌ 改前(404 找不着路由)
spring:
cloud:
gateway:
routes:
- id: auth-service
uri: http://localhost:8001
predicates:
- Path=/api/auth/**
# ✅ 改后(路由正常加载)
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: auth-service
uri: http://localhost:8001
predicates:
- Path=/api/auth/**
3.3 统一 CORS 处理
思路很简单:网关是唯一入口,跨域只在网关层处理。下游服务只被网关调用,不需要自己处理跨域。
Gateway 加上 globalcors:
spring:
cloud:
gateway:
server:
webflux:
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
- PATCH
allowedHeaders: "*"
allowCredentials: true
maxAge: 3600
所有下游服务去掉 CORS 配置:
每个微服务的 *SecurityConfig.java 中去掉三样东西:
-
删除
.cors(cors -> cors.configurationSource(...))这一行 -
删除
corsConfigurationSource()方法 -
删除相关 import(
CorsConfiguration、CorsConfigurationSource、UrlBasedCorsConfigurationSource)// ❌ 改前:每个服务都有自己的 CORS .cors(cors -> cors.configurationSource(corsConfigurationSource())) // ...
@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("http://localhost:3000", ...)); // ... return source; }
// ✅ 改后:完全去掉,让 Gateway 统一处理
四、补充:Predicate 的写法也变了?
这里还有个细节。旧版 Gateway 支持在 Path 谓词里用逗号分隔多个路径:
# 旧写法(一个 Path 配多个模式)
predicates:
- Path=/api/auth/**,/api/admin/auth/**
在 5.x 里,这个写法也行,但要确认 ShortcutType.GATHER_LIST_TAIL_FLAG 的解析逻辑。最稳妥的做法是拆成多个 Path 条目(注意这里是多条路由,不是多个谓词):
# 最稳妥的写法:一条路由配一个 Path
- id: auth-service
uri: http://localhost:8001
predicates:
- Path=/api/auth/**
- id: auth-service-admin
uri: http://localhost:8001
predicates:
- Path=/api/admin/auth/**
不过要注意,不能在同一条路由下写两个 Path 谓词:
# ❌ 错误:同一个 route 里多个 Path 是 AND 关系,永远匹配不到
- id: auth-service
uri: http://localhost:8001
predicates:
- Path=/api/auth/**
- Path=/api/admin/auth/**
因为路由的多个谓词之间是 AND 关系,一个路径不可能同时匹配 /api/auth/** 和 /api/admin/auth/**。
五、总结
这次升级 Gateway 5.x 遇到的核心问题就一个:旧资料还停留在 4.x,但 5.x 的配置体系改了。
几个教训:
- 依赖坐标变了就查官方源码——网上搜到的教程大概率还是旧版的
spring-cloud-starter-gateway - 配置前缀变了也查源码——
GatewayProperties.java那条PREFIX常量一眼就能看到 - CORS 只管一层——网关架构下,跨域在网关层统一处理就好,下游服务不需要操这个心
- 启动日志里有黄金——
TRACE级别的日志能看到路由匹配的全过程,比瞎猜有用得多
如果升级过程中也遇到类似的"配置明明写了但不生效"的问题,大概率也是前缀或者坐标的问题。