第 2 章 应用生命周期:从 NewApp 到 WebShell
本文导航

第 2 章 应用生命周期:从 NewApp 到 WebShell
上一章能跑起来,但顺序是「黑盒」。这一章把启动过程拆开,解释每一步为什么这样安排。理解了顺序,后面遇到「配置没生成」「依赖注入失败」「迁移没执行」时,才知道该看哪一步。
两个入口
owl.NewApp(subApps...).WebShell() // HTTP 服务
owl.NewApp(subApps...).ConsoleShell(rootCmd) // 命令行
- WebShell:注册路由、收集菜单、执行
Bootstrap,最后启动 Gin。 - ConsoleShell:只执行
Bootstrap,并把各子应用的RegisterCommands()挂到 Cobra,然后执行命令。
两种模式都会做依赖注入、配置生成、统一迁移和 Provider 的 Boot()。区别只在「是否注册 HTTP 路由和菜单」。
NewApp 阶段
owl.NewApp() 依次做六件事:
- 初始化雪花 ID 生成器:
utils.InitSnowFlakeWorker(1, 3); - 创建 dig 容器:
dig.New(); - 推断路径:
setPath(),得到runDir(当前工作目录)和basePath(可执行文件目录); - 确保配置目录存在:
ensureConfDir(); - 注册基础绑定:
registerBaseBindings(),把*Application注册为foundation.Application,并注册MenuRepository; - 注册基础 Provider:
registerBaseServiceProviders(),包括 conf、log、otel、event、appconf、validator。
注意:此时还没有读取配置。框架刻意把「生成配置文件」和「加载配置」拆成两个阶段。
为什么配置要分两阶段
如果启动时直接用 NewConfigure 扫描 conf/,而某个 Provider 的默认配置还没写盘,读取时就会因为缺键而 panic。Owl 的处理顺序是:
各 Provider 的 Conf() 写盘/补键
↓
各 Provider 的 Register()(只注册工厂,不读配置)
↓
initLogger() 首次解析 Logger,触发 NewConfigure 一次性加载 conf/
↓
后续 RegisterRouters / Bootstrap 才真正使用配置
- 文件不存在:写入默认内容;
- 文件已存在:
conf.ValidateConfigKeys把默认内容里缺少的键补进去,已有值不覆盖; - 补不进去的键只告警,不阻断启动。
这条规则解释了为什么升级框架后,旧的 conf/*.yaml 会自动多出新字段。
newSubApp 阶段
WebShell / ConsoleShell 最终都会进入 newSubApp(subApps...),顺序如下:
1. 对每个 SubApp 注入 app,并注册 Binds 里的构造函数
2. 收集 RegisterCommands()
3. 收集 ServiceProviders()
4. generateProviderConfigs(子应用 Provider) // 先落盘配置
5. registerProviders(子应用 Provider) // 只注册工厂
6. initLogger() + initTelemetry() // 加载 conf/
7. collectConfigs() // 收集 setting.Registrar
8. Web 模式:
RegisterRouters() → RegisterMenus() → AddMenu() → Bootstrap()
Console 模式:
只执行 Bootstrap()
9. runAutoMigrate() // Before → 按 hash 迁移 → After
10. 所有 Provider 的 Boot()
11. Web 模式启动 HTTP;Console 模式执行 Cobra
几个容易记错的点:
Bootstrap()在所有子应用的RegisterRouters()和菜单收集之后执行,适合做 Seeder、事件监听注册;RegisterMigrate()返回的模型在所有Bootstrap()结束后统一迁移;Provider.Boot()最后执行,此时依赖已全部就绪;- 系统配置(
setting.Registrar)在注册路由之前收集,Web 和命令行都会执行。
菜单的收集与归属
Web 模式下,框架对每个子应用调用 RegisterMenus()。拿到菜单后,框架会做一件事:
registered := app.RegisterMenus()
for _, menu := range registered {
menu.SubApp = app.Name() // 标记根菜单属于哪个子系统
}
这样前端可以按 ?app=admin,cms 过滤只显示指定子系统的菜单。FilterMenusBySubApp 只检查根菜单的 SubApp,子节点跟随根节点。
迁移的顺序
所有 SubApp 的 BeforeMigrate(串行)
↓
按模型类型计算 schema hash(并行)
↓
只对 hash 变化的模型执行 AutoMigrate(串行)
↓
所有 SubApp 的 AfterMigrate(并行)
hash 记录在 storage/migrate_hash.txt,每行是 包名.类型名=hash。结构没变的模型不会重复迁移;但 BeforeMigrate / AfterMigrate 每次启动都会执行。
如果某个模型结构没变、但你想强制迁移,删掉 storage/migrate_hash.txt 对应行即可。
路径推断规则
GetConfigPath()、GetStoragePath() 等最终都走 inferDir:
先看 可执行文件目录/basePath + path
不存在 → 再看 当前工作目录/runDir + path
开发时通常用 go run .,可执行文件在临时目录,basePath 下没有 conf/,于是回落到项目目录,看起来一切正常。但打成二进制并用 systemd / Docker 启动时,工作目录可能不同,conf/ 和 storage/ 的落点会变。部署时务必确认实际路径。
启动日志怎么读
框架在 debug 日志级别下会打印每个阶段耗时:
startup begin Binds[order]
startup Binds[order] took 120µs
startup begin generateProviderConfigs
...
startup begin AutoMigrate(1 models)
如果启动卡住,看最后一条 startup begin 就知道停在哪个阶段。排查「依赖注入失败」时,通常会停在 Binds 或某个 RegisterRouters。
自检
- [ ] 能按顺序说出
NewApp的六件事; - [ ] 能解释为什么先
Conf()落盘、再Register()、最后initLogger(); - [ ] 能说出 Web 与 Console 模式下
RegisterRouters/RegisterMenus/Bootstrap是否执行; - [ ] 能解释
storage/migrate_hash.txt的作用; - [ ] 部署时能确认
conf/、storage/实际落在哪个目录。
评论
加载评论中…