Go-Spring 实战第 17 课 —— App 运行模型:启动、运行与关闭

上一篇,我们梳理了 Go-Spring IoC 容器的运行流程,了解了 Bean 注册、装配、初始化和销毁的过程。不过,IoC 容器只是应用完整运行流程中的一个环节。

在启动容器之前,应用还需要加载配置、初始化日志。容器刷新完成以后,应用还要依次执行 Runner、启动 Server,并等待所有 Server 就绪。程序退出时,则要先停止服务,再销毁服务依赖的对象。这些环节相互依赖,必须按照确定的顺序执行。

本篇咱们就来梳理一下 Go-Spring App 的运行模型,看看应用如何从 Run() 开始,一步步完成启动、运行、服务停止和资源释放。理解这套流程以后,我们就能明确配置从什么时候开始可用、应用在什么时候真正完成启动,以及遇到问题时应该从哪里开始排查。

运行流程

Go-Spring App 的运行流程可以拆成如下六个阶段:

  • 入口阶段:通过 Run() 或者 RunAsync() 启动 App。
  • 准备阶段:打印 Banner,执行启动配置,加载配置,初始化日志。
  • 装配阶段:启动 IoC 容器,创建并注入应用需要的 Bean。
  • 启动阶段:顺序执行所有 Runner,并行启动所有 Server,等待所有 Server 就绪。
  • 运行阶段:应用开始对外提供服务,等待退出动作。
  • 关闭阶段:取消应用 Context,停止所有 Server,关闭 IoC 容器和日志系统。

从这个流程可以看出,App 并不是简单地启动一个 HTTP Server,而是统一管理配置、日志、IoC 容器和长期运行的 Server。这套完整的生命周期管理,正是 App 运行模型要解决的核心问题。

入口阶段

Go-Spring 提供了 Run()RunAsync() 两种启动入口。两者的核心流程相同,区别只在于启动完成以后,由谁负责等待并触发应用关闭。

Run() 在启动完成以后会监听 SIGINTSIGTERM 信号,并阻塞等待,直到应用关闭。这种方式适合独立运行的服务。

RunAsync() 在启动完成以后会直接返回,不会监听操作系统信号。同时,它会返回一个 stop() 函数,供调用方在合适的时候触发应用关闭。这种方式适合将 Go-Spring 集成到已有运行流程的程序中。

无论使用哪个启动入口,后续的配置加载、容器启动、Runner 执行和 Server 执行都是一样的。

准备阶段

准备阶段主要完成三件事。

第一,打印 Banner,然后执行通过 Configure() 注册的启动配置。

启动配置发生在配置加载之前。这个阶段写入的默认配置,以及注册到当前 App 的 Bean 和 Root Bean,都可以参与本次应用的启动。当 App 开始加载配置时,启动前的配置阶段也就结束了。

第二,加载并合并配置。

App 会读取所有来源的配置,并按照优先级合并出最终结果。后续的日志初始化、条件判断和配置绑定都会使用这份配置。也就是说,后面各个阶段使用的是同一份最终配置,而不是各自读取不同的配置文件。

第三,初始化日志系统。

日志初始化依赖配置,因此必须在配置加载完成以后进行。而 IoC 容器在解析 Bean、创建对象和执行初始化方法时需要记录日志,这又要求日志系统在 IoC 容器启动之前准备好。

如果配置加载失败了,或者日志初始化失败了,App 会在准备阶段直接终止启动。因为此时继续后续流程已经没有意义了。

装配阶段

准备阶段完成以后,App 开始启动 IoC 容器。

首先,App 会把自己作为 Bean 注册到 IoC 容器中,使容器能够收集并注入 RunnerServer 和其他 Root Bean。然后,IoC 容器会根据已经加载完成的配置,依次执行 Bean 解析、条件判断、对象创建、依赖注入、配置绑定和初始化回调等一系列动作。这就是上一篇介绍的 IoC 容器运行流程。

容器刷新完成以后,App 需要的对象就已经全部准备就绪了。后面的启动流程只需要使用这些已经完成注入的普通 Go 对象,无需再进行运行时依赖装配。

Runner 阶段

Runner 用于表示服务启动前必须完成的一次性任务,比如数据库初始化、数据检查和缓存预热等。IoC 容器启动完成以后,App 会顺序执行所有的 Runner。因此,Runner 必须能够执行完毕。如果把长期阻塞任务放进 Runner,App 就会一直停留在这个阶段。

Runner 的接口如下:

go 复制代码
type Runner interface {
	Run(ctx context.Context) error
}

Runner 接收到的是应用的根 Context。启动任务可以继续把它传递给数据库、HTTP 客户端等支持 Context 的调用,使整个调用过程使用同一套 Context。

如果任意一个 Runner 返回 error,App 就会终止启动,不再执行后面的 Runner,也不会启动任何 Server。因此,需要在应用对外提供服务之前完成的检查,都可以放在 Runner 中执行。这样可以避免服务已经开始接收流量,才发现关键初始化尚未完成。

Server 阶段

Server 表示需要长期运行的服务,例如 HTTP Server、gRPC Server、TCP Server 或者消息消费者等。所有 Runner 执行完成以后,App 会并行启动所有的 Server,每个 Server 都在独立的 goroutine 中执行 Run() 方法。

Server 的接口如下:

go 复制代码
type Server interface {
	Run(ctx context.Context, sig ReadySignal) error
	Stop() error
}

Run() 用于启动并持续运行服务,通常会一直阻塞到服务停止。Stop() 用于在应用关闭时停止服务,并让对应的 Run() 方法返回。这两个方法共同构成了 Server 的运行和关闭流程。

不过,即使 Run() 已经开始在 goroutine 中执行,也不代表服务已经可以使用。此时,Server 可能还在绑定端口、建立连接或完成自身初始化。因此,Go-Spring 使用 ReadySignal 来协调多个 Server 的启动状态。

go 复制代码
type ReadySignal interface {
	TriggerAndWait() <-chan struct{}
}

每个 Server 都需要在完成启动准备动作以后触发 TriggerAndWait() 信号,并且等待其他 Server 准备完成。只有所有 Server 都触发 TriggerAndWait() 信号,App 才会认为启动完成,并让它们一起进入正式运行状态。

因此,TriggerAndWait() 必须在 Server 已经具备服务条件以后触发。例如,网络服务应该先成功绑定端口,消息消费者应该先完成客户端和订阅初始化。如果触发过早,App 会认为启动已经完成,但服务实际上还不能正常工作。

如果某个 ServerTriggerAndWait() 之前返回 error 或者发生 panic,App 会终止启动。如果某个 ServerRun() 运行阶段返回 error,App 会触发关闭。

运行阶段

在所有 Server 都触发了 TriggerAndWait() 信号以后,App 正式进入运行阶段。

如果使用 Run() 启动应用,App 会监听操作系统的退出信号,并阻塞等待,直到收到信号。如果使用 RunAsync() 启动应用,它会返回 stop(),由调用方继续管理当前进程,并在需要退出时调用这个函数。

在运行阶段,Server.Run() 通常会持续阻塞并提供服务。HTTP 请求、RPC 调用和消息消费等业务逻辑,都由具体的 Server 和业务对象处理。App 无需参与每一次业务调用,只需维护应用的根 Context、监控 Server 的运行状态,并在收到退出动作时进入关闭阶段。

应用的根 Context 必须贯穿 RunnerServer 的整个运行过程。业务代码需要从这个 Context 派生子 Context。这样在应用关闭时,取消信号就能沿着 Context 关系向下传递。

正常情况下,Server 会一直运行到 App 主动关闭。如果某个 Server.Run() 提前返回 error,App 会认为长期服务已经异常退出,并触发整个应用关闭,避免其他 Server 在应用已经处于异常状态时继续提供服务。

关闭阶段

进入关闭阶段以后,App 首先会取消应用的根 Context。使用这个根 Context 的后台任务可以据此收到退出通知,停止接收新任务,并完成自身的清理工作。不过,取消根 Context 只是发出通知,并不会强制终止 goroutine。业务逻辑需要主动响应 ctx.Done() 信号,才能退出 goroutine。

随后,App 会并行调用所有 ServerStop() 方法,并等待它们执行完成。App 没有为整个关闭过程设置统一的强制超时,因此,具体的 Stop() 方法或业务清理逻辑需要根据实际情况控制超时时间,确保对应的 Run() 方法能够退出。否则,App 会一直等待 Server 结束,无法继续关闭。

Go-Spring 还会等待所有 ServerRun() goroutine 真正结束。Stop() 返回只能说明停止动作已经执行,不代表 Server 的运行循环已经完全退出。只有 Stop() 执行完成,并且 Run() goroutine 全部结束,才能确认这些长期服务不再访问业务对象。

等所有 Server.Stop()Server.Run() 都返回以后,App 会继续关闭 IoC 容器。容器会按照依赖关系执行 Bean 的 Destroy 回调,释放数据库连接、客户端和其他资源。如果某个 Destroy 返回了 error,那么容器只会记录日志,而不会退出关闭流程。容器会继续执行后面的 Destroy,尽量完成其他资源的释放。

最后,App 会关闭日志系统,至此整个应用的运行流程也就结束了。日志系统必须放在最后关闭,因为 Server 停止和 Bean 销毁的过程中仍然可能需要记录日志。

Go-Spring App 运行模型

Go-Spring App 为应用建立了一套稳定的运行机制。下一篇,我们将进一步介绍这些能力的具体用法与定制方式。

相关推荐
JAVA学习通4 小时前
从 Bean 到微服务:一张图吃透 Spring 全家桶底层原理
java·前端·spring
Micro麦可乐4 小时前
最新Spring Security实战教程(十)权限表达式进阶 - 在SpEL在安全控制中的高阶魔法
java·spring boot·后端·spring·spring security·spel表达式
Jinkxs4 小时前
Resilience4j- 非 Spring 环境集成:纯 Java 项目中的手动配置实现
java·后端·spring
码不停蹄的玄黓4 小时前
SpringBoot 实现自定义注解
java·spring boot·spring
2601_961194024 小时前
2026六级词汇资料电子版|大学英语六级核心词汇PDF
java·spring·eclipse·pdf·tomcat·hibernate
9624564 小时前
Go 语言 x402 支付中间件与 DeepSeek 代理开发复盘
go
明月_清风5 小时前
图解 Socket 编程:一文吃透 TCP/UDP 编程模型(Go 实战版)
后端·tcp/ip·go
RR13355 小时前
Spring MVC and Spring Gateway 的差异,以及报错处理
spring·gateway·mvc
2601_9611940214 小时前
2026初级会计实务公式总结大全|计算题公式手册PDF
java·spring·eclipse·pdf·tomcat·hibernate