应用生命周期
Rain 的生命周期不是单一回调,而是一条从配置、容器、扫描、元数据注册到资源启动的编排链。框架扩展应根据工作发生的时机选择 Module、Loader 或 ApplicationService,而不是把全部初始化塞进入口函数。
启动流程
Application.start() 依次执行以下工作:
- 读取
rain.runMode和rain.launchPackage系统属性。 - 创建并初始化
ConfigImpl。 - 创建 DI 上下文
ContextImpl。 - 获取
AppLoader,扫描并注册 Bean、Loader 与应用服务。 - 按优先级启动全部
ApplicationService。 - 发布
AppStatusEvent.AppStarted。 - 注册 JVM shutdown hook。
在存在 pom.xml、build.gradle 或 build.gradle.kts 的目录中启动时,默认运行模式为 dev;否则默认落到 prod。
启动对象如何协作
Application
├─ ConfigImpl
│ └─ 合并模块默认配置与应用配置
├─ ContextImpl
│ ├─ 创建 ClassContext
│ ├─ 创建 BeanCreator / BeanInjector
│ └─ 提供 ConfigReader / DataReader
├─ AppLoader
│ ├─ Module.onLoad
│ ├─ 扫描 rain.scanPackages
│ ├─ 建立 AutoBind 与 BeanFactory 上下文
│ ├─ 调用 ClassRegister
│ └─ 执行 Loader
└─ ApplicationServiceLoader
├─ start
└─ stopApplication 直接构造 ConfigImpl 和 ContextImpl,随后才从容器取得 AppLoader。这意味着普通应用扩展不能仅靠定义一个 Bean 来替换配置或基础 Context;需要替换它们时,应自定义启动编排,而不是假设 @AutoBind 会影响已经完成的构造。
三种初始化入口
Module:扫描前
rain.modules 中列出的类型会在类扫描前由容器创建并调用:
class MetricsModule(
private val context: DiContext,
) : Module {
override fun onLoad() {
context.putBean(MetricRegistry::class.java, MetricRegistry())
}
}rain.modules=com.example.metrics.MetricsModule仅当工作必须早于扫描时使用 Module。普通元数据发现应交给 Loader。
Loader:扫描后
Loader 获得已经按触发方式分组的 LoadItem,适合构建路由、监听器、任务、命令等静态注册表。此时容器已经可用,但 ApplicationService 尚未启动。
ApplicationService:资源启动
ApplicationService 适合线程、端口、连接池等长期资源。它能依赖 Loader 已经建立的元数据,因此服务器启动时不需要再次扫描业务类。
优先级与确定性
Loader 和 ApplicationService 都按 priority() 数值升序执行,默认值为 10。优先级是模块间契约,不应随意使用极端数值争抢顺序。
框架模块需要另一个模块的 Loader 结果时,建议:
- 在公开文档中声明顺序依赖。
- 给两个 Loader 使用有间隔的稳定优先级。
- 在消费方启动时验证前置注册表是否已完成。
- 避免依赖同优先级对象的偶然扫描顺序。
启动失败语义
Module.onLoad()抛错会直接中断扫描。- Loader 创建失败或
load()抛错会中断应用启动。 ApplicationService.start()抛错会记录日志并继续向上抛出。- 当前
Application没有自动回滚已经启动成功的服务。
因此每个 Service 的 start() 应尽量先构造局部资源,确认成功后再发布到字段;框架若管理多个资源,需自行在失败分支关闭此前已创建的部分。
停止流程
应用停止时先发布 AppStatusEvent.AppStopping,再依次调用应用服务的 stop()。
class StorageService : ApplicationService {
override fun priority() = 20
override fun start() {
// 建立连接
}
override fun stop() {
// 释放资源
}
}WARNING
当前实现中服务停止顺序与启动顺序相同,并非反向顺序。设计依赖关系时不要假设后启动的服务会先停止。
Application.stop() 先发布 AppStopping,再调用 ApplicationServiceLoader。监听该事件的代码仍可访问尚未停止的服务,但不应启动新的长期任务。
当前实现没有重复停止保护。JVM shutdown hook、测试代码或嵌入式宿主若可能多次调用 stop(),服务实现应自行保持幂等:
class IdempotentServer : ApplicationService {
private var server: Server? = null
override fun start() {
check(server == null) { "server already started" }
server = Server().also { it.open() }
}
override fun stop() {
val current = server ?: return
server = null
current.close()
}
}框架作者检查表
- 元数据发现是否在 Loader 中完成?
- 外部资源是否延迟到 ApplicationService 启动?
- Loader 和服务优先级是否形成文档化契约?
- 启动中途失败是否会泄漏已创建资源?
stop()是否允许重复调用?- 停止逻辑是否错误假设了反向顺序?
- 测试是否覆盖 AppStarted、AppStopping 和启动失败?
所有扩展阶段的选择见扩展点总览。