前几天我们线上网关遇到一个奇怪现象改了路由配置重启过了两三分钟才开始有流量进来还有一次因为端口被占用应用日志里明明显示启动成功外部却怎么也访问不通。排查到最后发现都是对 Spring Cloud Gateway 启动流程的细节没吃透。这篇文章我就把自己从源码层面过完 Gateway 启动流程后整理的笔记放出来重点聊聊从 Spring Boot 入口开始到路由装配完成、Netty 端口真正监听之间到底发生了什么。1. 启动流程的全貌从 Boot 引导到端口监听1.1 把启动流程当成 bootloader 看我做嵌入式出身后来转做 Java 后端一直觉得看启动流程最好的切入点是先把“分阶段状态迁移”的思维带进来。单片机 STM32 的启动流程要经历向量表、时钟配置、数据段初始化最后才跳到 mainAndroid 系统里 PackageManagerService 启动也要先扫描系统目录、再扫描用户目录、最后才对外提供查询能力。Spring Cloud Gateway 的启动流程也是同样的逻辑环境准备 - 容器刷新 - WebServer 启动 - 路由对外生效。这四个阶段不是割裂的前一个阶段没做完后面阶段要么直接失败要么“假成功”。比如 Spring 容器刷新还没结束Netty 端口可能已经绑上了但此时路由表还是空的请求打过来只会 404又比如环境属性里没有把spring.main.web-application-type设置成 reactive那么后续装配的就不是 Netty 而是 TomcatGateway 核心组件直接失效。所以看源码前我习惯先在脑子里画一张阶段图SpringApplication.run 负责整个流程的驱动EnvironmentPostProcessor 负责调整环境AutoConfiguration 负责把 Gateway 的 Bean 塞进容器BeanPostProcessor 和 ApplicationListener 负责在合适的时机做增强和事件通知最后 ReactiveWebServerFactory 创建 Netty 服务端。下面每个章节都会落到这几个关键角色上。1.2 我建议的源码阅读姿势不建议直接把整个 Spring Cloud Gateway 源码下载下来从头读。更快的办法是在你自己的网关工程里加上依赖后到本地 Maven 仓库找对应的 jar 包源码。我用的是spring-cloud-starter-gateway底层核心包是spring-cloud-gateway-server。在 IDEA 里直接搜索GatewayAutoConfiguration就能看到自动配置的类骨架。Spring Boot 2.4 之后不再用spring.factories注册自动配置而是改用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。你在 IDE 里展开 jar 包可以看到类似这样的一行org.springframework.cloud.gateway.config.GatewayAutoConfiguration这也是 Gateway 能“凭空出现”的起点。读源码时我习惯直接下断点跑一次启动过程。常用的断点位置包括GatewayAutoConfiguration里各Bean方法RouteDefinitionRouteLocator#getRoutes()CachingRouteLocator#getRoutes()RoutePredicateHandlerMapping#getHandlerInternal()NettyReactiveWebServer#start()这些位置涵盖了从配置加载、路由构建到端口绑定的完整链路。下面逐个拆开。2. 自动装配Gateway 如何在 Spring Boot 里现身2.1 GatewayWebFluxEnvironmentPostProcessor 做了什么Spring 容器启动非常早的阶段会调用EnvironmentPostProcessor来修改环境变量。Gateway 里有一个专门的类名字很长GatewayWebFluxEnvironmentPostProcessor。这个类的作用很直接强制把 Web 应用类型设置成 REACTIVE。如果你不显式配置spring.main.web-application-type并且spring.cloud.gateway.enabled默认是 true那么它就会在 Environment 里插入一个属性源把spring.main.web-application-type设为REACTIVE。为什么需要这一步因为 Gateway 是基于 WebFlux 的不是 Spring MVC。如果工程里同时引入了spring-boot-starter-web和spring-boot-starter-gatewaySpring Boot 默认会判断成 SERVLET 类型然后创建 Tomcat导致 Gateway 的路由核心压根不生效。源码里这个后置处理器就是用来避免这种“两种容器同时存在”的混乱局面的。这里有一个值得留意的小细节EnvironmentPostProcessor 的执行时机非常靠前发生在 ApplicationContext 创建之前。所以在启动阶段如果你观察到某些属性异常得先怀疑是不是这一步被干扰了。比如自己写了一个 EnvironmentPostProcessor顺序排在 Gateway 前面把spring.cloud.gateway.enabled改成了 false那后面所有 Gateway 自动配置都会退出。2.2 GatewayAutoConfiguration 的 Bean 装配GatewayAutoConfiguration是整个 Gateway 自动配置的核心。它身上有一堆ConditionalOnXxx注解只有满足条件才创建对应 Bean。这里不罗列所有条件只说我实际跟踪启动流程时最关心的几个 BeanGatewayProperties负责读取spring.cloud.gateway前缀的配置PropertiesRouteDefinitionLocator把配置文件里的 routes 转成RouteDefinitionRouteDefinitionRouteLocator把RouteDefinition转换成真正可匹配的RouteRoutePredicateHandlerMapping请求进来后匹配路由用的 HandlerMappingFilteringWebHandler把全局过滤器 GlobalFilter 和网关过滤器 GatewayFilter 组合成调用链这些 Bean 遵循一个共同套路配置属性类解码配置Locator 加载路由HandlerMapping 完成请求到路由的映射WebHandler 组装执行链。启动阶段如果这些 Bean 创建失败通常日志里会直接报某个构造函数依赖找不到或者是类型不匹配。我在排查一个启动失败案例时发现RouteDefinitionRouteLocator的构造函数需要注入ListRoutePredicateFactory和ListGatewayFilterFactory。这些工厂通过Bean方法注册比如AfterRoutePredicateFactory、PathRoutePredicateFactory、StripPrefixGatewayFilterFactory等。如果你在工程里自定义了一个 Filter 工厂名字不小心起成了MyGatewayFilterFactory但类名对应规则不符合 Spring Boot 约定就有可能导致它没有被自动收集到集合里然后启动阶段报“找不到 filter”而不是运行时路由不生效。2.3 配置属性绑定routes 怎么从 yaml 变成对象网关里最常见的配置是这样的spring: cloud: gateway: routes: - id: route-1 uri: lb://service-a predicates: - Path/a/** filters: - StripPrefix1很多人以为这段配置在启动时被“直接读成了 Route”其实中间隔了一层。真正做属性绑定的是GatewayProperties它被ConfigurationProperties(prefix spring.cloud.gateway)标记内部结构大致是public class GatewayProperties { private ListRouteDefinition routes new ArrayList(); private ListFilterDefinition defaultFilters new ArrayList(); private ListPredicateDefinition predicates new ArrayList(); }Spring Boot 的 Binder 会把 yaml 里的 routes 数组逐项绑定成RouteDefinitionRouteDefinition里包含id、uri、predicates、filters等信息。这一步发生在 Environment 准备完成后、自动配置类创建 Bean 时理论上只要配置格式正确这个过程不会出问题。但常见的坑是 yaml 缩进写错或者routes下面没有用- id:这种数组格式导致绑定结果为空启动后路由一个都不生效。我想提醒的是配置属性绑定完成不代表路由已经就绪。它只是把文本配置变成了对象真正转换成 Route 是在后面 RouteLocator 链路中进行的。3. RouteLocator 链路路由定义如何加载成可匹配的 Route3.1 路由定义来源不止配置文件一处我在刚接触 Gateway 时总以为路由只来自 yaml其实源码里RouteDefinitionLocator是有多个实现的启动时会组合成列表一起使用PropertiesRouteDefinitionLocator读取spring.cloud.gateway.routesRouteDefinitionRepository路由定义仓库支持内存、Redis 等实现DiscoveryClientRouteDefinitionLocator从注册中心服务发现生成路由GatewayAutoConfiguration里会把这些 Locator 组装成一个CompositeRouteDefinitionLocator。你可以把它理解成多路数据源每个 Locator 都返回FluxRouteDefinition最后合并到一块。线上环境如果用了 Nacos 或 Consul 做动态路由启动时会走DiscoveryClientRouteDefinitionLocator。这个 Locator 在启动阶段就调用注册中心接口拉取服务列表如果注册中心暂时连不上路由加载不会直接报错但日志里可能会有 timeout路由表为空。这解释了为什么有些网关重启后服务列表半天才出来——不是 Gateway 的问题而是下游服务注册数据还没准备好。3.2 RouteDefinitionRouteLocator 的转换过程真正把RouteDefinition变成Route的核心类是RouteDefinitionRouteLocator。启动时 Spring 容器会调用它的getRoutes()方法。我断点看源码时这个方法的关键动作是遍历每个 RouteDefinition然后执行convertToRoute。convertToRoute会去ListRoutePredicateFactory里找一个和配置中的 predicate 名字匹配的工厂然后调用工厂创建 Predicate 对象。比如配置是Path/a/**它就会匹配到PathRoutePredicateFactory最终生成一个PredicateServerWebExchange。过滤器也是同理StripPrefix1会匹配到StripPrefixGatewayFilterFactory生成一个GatewayFilter。这里有一个极其重要的顺序问题RouteDefinitionRouteLocator里的 predicates 和 filters 两个列表来自自动配置类中所有Bean工厂方法的收集结果。如果这些工厂 Bean 的初始化顺序不稳定理论上会导致多路由匹配时出现诡异现象。实际中 Spring 容器对这些 Bean 的收集结果取决于 class 扫描顺序虽然大多数情况下是稳定的但如果你自定义的谓词工厂重名或者存在多个同 name 的工厂就会抛出“Unable to find RoutePredicateFactory with name xxx”之类的异常启动阶段马上暴露。转换完成后Route对象包含 id、uri、predicate、filters 和 order。此时启动流程已经从“配置”走到了“运行时匹配规则”。3.3 CachingRouteLocator 缓存与 RefreshRoutesEvent在RouteDefinitionRouteLocator外面还包了一层CachingRouteLocator。这个类做了两件事缓存兜底内部持有一个AtomicReferenceFluxRoute cachegetRoutes()返回的是缓存流监听刷新实现ApplicationListenerRefreshRoutesEvent收到事件后把 cache 重新设置成 delegate 的getRoutes()正是因为这一层缓存Gate 启动时如果反复调用getRoutes()实际只从最底层 RouteDefinition 构建一次。如果没有 CachingRouteLocator 包裹每次请求进来做路由匹配都要重新遍历一遍配置并创建 Predicate性能和稳定性都会很差。启动阶段 CachingRouteLocator 的初始化顺序很关键。它作为 ApplicationListener 注册进容器后只要有人发布RefreshRoutesEvent就会强制重算路由。比如动态路由场景下注册中心推送变更事件后代码里常常会调applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this))。如果你在启动早期就发布了这个事件而此时底层 RouteDefinitionRouteLocator 还没准备好就可能拿到空路由。所以我在自定义路由刷新逻辑时都会确保 Gateway 的 RouteLocator Bean 已经创建完成后再 publish 事件。3.4 RoutePredicateHandlerMapping 如何接到请求路由装配完成后真正处理路由匹配的是RoutePredicateHandlerMapping。它实现了 Spring WebFlux 的AbstractHandlerMapping被注册到容器后当 Netty 收到请求、DispatcherHandler 开始分发时会按顺序调用各个 HandlerMapping其中一个就是它。RoutePredicateHandlerMapping.getHandlerInternal的关键逻辑是从ServerWebExchange里拿到当前请求然后调用RouteLocator.getRoutes()用route.getPredicate().test(exchange)逐一执行谓词判断命中后返回对应的FilteringWebHandler并把命中的Route放到 exchange 属性里。这意味着启动阶段路由表为空不代表启动失败只是匹配不到 Handler。等到 CachingRouteLocator 从底层拿到路由数据后请求才会被正常转发。很多“启动成功但请求 404”的问题本质上都是 RouteLocator 这一层返回了空。4. Netty 服务端启动链路4.1 谁在真正管理 Netty 生命周期Gateway 默认情况下并没有在GatewayAutoConfiguration里去直接new一个 Netty 服务端。它依赖的是 Spring Boot 对 WebFlux 的自动配置。启动流程走到容器刷新阶段时Spring Boot 会根据当前 Environment 中的spring.main.web-application-typeREACTIVE装配出ReactiveWebServerFactory的实现类NettyReactiveWebServerFactory。容器刷新的过程中onRefresh阶段会调用ReactiveWebServerApplicationContext里的createWebServer()。这里会创建NettyReactiveWebServer并把它包装成WebServerManager。真正绑定端口是在容器做到SmartLifecycle阶段时调start()触发的。我建议你在NettyReactiveWebServer.start()方法里下一个断点可以看到类似channel.bind(...)的逻辑。这里绑定端口成功之后日志才会输出 “Netty started on port(s): 8080”。所以如果端口被占用启动流程不是直接失败而是抛WebServerException: Port already in use但此时前面的 Spring 容器已经全部刷新完成了日志里看起来“大部分启动完成”。4.2 请求进入网关的 handler 链Netty 启动不等于 Gateway 路由体系已经能处理请求。请求从端口进来之后还会经过一套 WebFlux 的 Handler 链。NettyReactiveWebServer内部使用的是 reactor-netty 的HttpServer收到请求后交给ReactorHttpHandlerAdapter再到HttpWebHandlerAdapter。这一层会把 Reakt 的请求、响应对象转换成ServerWebExchange然后进入ExceptionHandlingWebHandler最后到达FilteringWebHandler。FilteringWebHandler里维护了 GlobalFilter 列表和 GatewayFilter 列表负责组装完整的过滤链。启动阶段看这条链的意义在于即使路由表已经加载完成某个 GlobalFilter 初始化失败也可能导致所有请求进到链路后直接报错。我见过一个同事在PostConstruct里预加载了大量下游接口数据结果下游服务没起来Gateway 启动时容器刷新卡了很久最终超时。这种问题从日志上看是“启动失败”根源却在过滤器初始化阶段的副作用操作。4.3 自定义 GlobalFilter 的初始化时机GlobalFilter 在 Gateway 里是以 Bean 形式存在的FilteringWebHandler启动时会从容器里拿到所有GlobalFilter并排序。如果你自定义了两个 GlobalFilter分别实现了Ordered接口那么它们的初始化顺序会直接影响 FilteringWebHandler 内部GlobalFilter路由链的构造顺序。我推荐一个排查习惯在自己的 GlobalFilter 构造函数和filter()方法里分别打日志启动的时候看哪一条日志先出现。如果构造函数执行了但 filter 方法还没执行说明启动流程还停留在装配阶段Netty 端口可能已经起来了但请求还没进来。这种“半启动”状态最容易让人误解成“卡死了”。另外注意FilteringWebHandler是在容器启动阶段创建的但它持有的过滤器链是在首次请求时才真正组合还是启动阶段就组合好不同版本有差异。我用的 3.1.x 版本中FilteringWebHandler构造函数里已经通过ListGlobalFilter构造了GatewayFilterChain的工厂所以 GlobalFilter 的 Bean 必须在创建 FilteringWebHandler 之前全部初始化完毕。如果你在自定义 GlobalFilter 的构造函数里依赖了一个尚未实例化的 Bean很可能会遇到循环依赖问题。5. 启动阶段常见的坑与排查方法5.1 端口绑定失败为什么日志显示启动成功但访问不了这是线上最容易踩的坑。Netty 绑定端口失败时Spring Boot 通常会跑出异常并让应用退出但某些容器环境里应用可能没退出只是端口没起来表现为“进程还在请求 502”。我常用的排查命令是lsof -i :8080 netstat -anp | grep 8080注意区分主机端口和容器内端口。Docker 部署时如果SERVER_PORT8080但宿主机端口映射8888:8080你需要确认 netstat 里监听的是容器内的 8080 而不是宿主机的 8888不然会误判端口冲突。从源码角度看端口冲突导致的启动失败发生在NettyReactiveWebServer.start()阶段这个阶段的异常包装在ReactiveWebServerException里。如果你在启动日志里看到Error creating bean with name webServerStartStopLifecycle基本可以确认是 WebServer 生命周期管理这一步出了状况优先查端点和网络权限。5.2 路由不生效检查顺序不能乱路由一个都不生效时不建议先去改配置。我按源码执行顺序整理了一个排查顺序表排查层次检查点对应源码位置配置层spring.cloud.gateway.routes是否正确绑定GatewayProperties定义层RouteDefinitionLocator是否返回非空PropertiesRouteDefinitionLocator#getRouteDefinitions转换层RouteDefinitionRouteLocator#getRoutes是否成功 convertconvertToRoute缓存层CachingRouteLocator缓存是否被刷新为空CachingRouteLocator#onApplicationEvent匹配层RoutePredicateHandlerMapping是否能命中getHandlerInternal启动后先访问网关的/actuator/gateway/routes接口看路由表如果这个接口返回空列表问题基本出在配置层或定义层。如果返回了路由但请求仍 404重点查匹配层比如 Predicate 写错、Path 大小写敏感等。5.3 依赖冲突与运行模式不对Gateway 要求 WebFlux 环境但很多人会习惯性引入spring-boot-starter-web结果启动时出现类似提示Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway这时候 Gateway 自动配置里的GatewayWebFluxEnvironmentPostProcessor虽然设置 web-application-type 为 REACTIVE但如果 MVC 相关类仍然存在某些 Bean 还是会冲突。解决办法是去掉 web starter或者在依赖关系里把 web 排除掉。查看依赖树用mvn dependency:tree -Dincludesorg.springframework.boot:spring-boot-starter-web如果真的因为历史原因没法排掉 MVC至少要保证spring.main.web-application-typereactive显式设置并且容器环境中没有 Tomcat 监听端口。5.4 自定义谓词/过滤器工厂的命名规则自定义RoutePredicateFactory或GatewayFilterFactory时Spring Boot 有一个隐藏约定Bean 名称必须等于工厂名字 相应后缀。比如你定义了一个叫CheckAuthGatewayFilterFactory的类Bean 名称默认就是checkAuthGatewayFilterFactory。它在配置里对应的过滤器名称是去掉GatewayFilterFactory后缀后的CheckAuth注意大小写和缩写。如果配置写成了checkauth匹配不到对应工厂启动时可能不报错但 filter 会被忽略或者直接构不成路由。RoutePredicateFactory 同理比如HeaderRoutePredicateFactory对应Header配置。我建议在启动日志里加一行配置项打印把spring.cloud.gateway.routes中的所有 predicates 和 filters 名称都打印出来和工厂名称做对比。这样能快速发现命名不一致的问题。6. 顺着启动流程学到的几件小事6.1 不要在 Bean 初始化阶段做重磅远程调用看完整条启动链路后我给自己定了一个规矩任何自定义组件的构造方法、PostConstruct 里不要做远程调用、连接池预创建、大规模数据加载。原因很直接启动流程是有状态的Bean 初始化出现在容器刷新阶段此时下游依赖未必可用Netty 端口也未必起来一旦阻塞就会拖慢整个启动过程甚至引起后面组件超时失败。如果确实需要预加载我一般会监听ApplicationReadyEvent也就是整个 SpringApplication.run 执行完成后、应用完全对外提供服务之前的那一瞬间。在这个事件里做预热就算失败也不影响启动如果预热失败还可以通过ApplicationRunner的返回值或日志做失败标记配合健康检查把实例摘掉。6.2 BeanPostProcessor 与 Gateway 组件的依赖关系启动流程中有一类角色非常容易被忽略——BeanPostProcessor。它的执行时机在 Bean 实例化之后、初始化前后之前热搜里提到的“Spring 容器启动流程中后置处理器的依赖关系图”本质上就是要在脑子里建立一张这种执行时序图。Gateway 本身虽然没有大量依赖 BeanPostProcessor 的组件但 Spring Boot 的 WebFlux 自动配置和 Spring Cloud 的上下文增强却依赖它。比如ConfigurationPropertiesBindingPostProcessor负责给ConfigurationProperties的 Bean 做属性绑定它必须早于普通业务 Bean 的初始化执行。如果你在启动阶段自定义了一个 BeanPostProcessor并且它的postProcessBeforeInitialization里访问了 Gateway 的某个组件就要注意执行顺序否则可能拿到尚未完成属性绑定的半成品对象。我常用的定位方式是在自定义 BeanPostProcessor 里对目标 Bean 类型做一个判断然后打印当前时间戳和 BeanName。多跑几次启动就能画出一张“谁先谁后”的关系图排查顺序类 Bug 非常有用。6.3 后续源码还能怎么读启动流程只是 Gateway 源码的第一步。顺着“启动后一个请求如何被转发出去”这条线我推荐继续看这几个类RouteToRequestUrlFilter处理lb://协议并得到真实 URLReactiveLoadBalancerClientFilter接入 Spring Cloud LoadBalancer做服务实例选择NettyRoutingFilter真正发起下游 HTTP 请求RequestRateLimiterGatewayFilterFactory了解限流实现与 Redis 的交互这几个类的初始化同样发生在启动阶段但行为要等请求进来才触发。理解了启动流程再看这些过滤链的执行顺序就不会被网上零散的配置示例带偏。最后再分享一个小技巧我在排网关启动问题时候最常用的一招是在GatewayAutoConfiguration的类头部加一个条件断点。具体操作是在 IDEA 里打开这个类对类成员变量logger的初始化那行打断点条件填spring.cloud.gateway.enabled相关的环境变量判断。这样每次启动时只要自动配置类被加载就能立刻看到当前 Environment 是否已经准备完成以及 web-application-type 是什么。踩过几次坑之后我的体会是Spring Cloud Gateway 的启动流程并不复杂但涉及环境准备、自动装配、路由构建、WebServer 生命周期四个阶段每个阶段都有隐藏的“假成功”风险。源码里那些类名看着多真正核心的也就十个左右你只需要抓住 RouteDefinition - Route - RoutePredicateHandlerMapping 这条主线启动阶段再诡异的问题都能找到方向。