通用主机
通用主机用于把配置、日志、依赖注入、生命周期和托管服务组织到同一个启动入口里。
它的意义,不是“帮你少写一行 main”,而是把应用启动时反复出现的基础设施问题收口到同一个模型里。
如果没有通用主机,一个稍微像样的服务程序通常都要自己处理这些事情:
- 先读取命令行、环境变量和配置文件
- 再初始化日志系统
- 再创建和组装依赖注入容器
- 再决定应用什么时候启动、什么时候停止
- 如果还有后台任务,还要自己管理这些任务的启动和关闭顺序
这些代码往往不是业务本身,却几乎每个应用都要重复写一遍。
通用主机解决的就是这类重复问题:
- 给应用提供统一的启动入口
- 让配置、日志、DI 和生命周期按固定顺序完成装配
- 让后台服务跟随主机一起启动和停止
- 让应用代码更早进入“注册服务和编写业务逻辑”这一层,而不是反复搭底座
如果只看最小示例,通用主机其实就是这几个动作:
import std.sync.*
import soulsoft_extensions_hosting.*
main(args: Array<String>): Int64 {
// 使用命令行参数创建主机构建器,并构建可运行主机。
let host = Host.createBuilder(args).build()
// 注册主机启动完成后的回调。
host.lifetime.onStarted({ =>
println("host started")
})
// 注册主机即将停止时的回调。
host.lifetime.onStopping({ =>
println("host stopping")
})
// 注册主机完全停止后的回调。
host.lifetime.onStopped({ =>
println("host stopped")
})
// 启动一个后台任务,1 秒后主动触发应用停止。
spawn {
sleep(Duration.second)
host.lifetime.stopApplication()
}
// 启动主机,并阻塞到应用关闭。
host.run()
return 0
}主机构建
通用主机的入口是:
let builder = Host.createBuilder(args)这一步不只是“new 一个 builder”,而是先把主机启动所需的默认基础设施准备好,再把可继续扩展的 HostBuilder 交给你。
默认环境
主机环境由一组单独的配置读取出来:
- 读取环境变量,前缀是
cangjie_ - 读取命令行参数
- 从中取出
environment、contentRootPath、applicationName
如果没有显式提供值,默认行为是:
| 键 | 默认值 |
|---|---|
environment | Production |
contentRootPath | 当前工作目录 |
applicationName | 空字符串 |
IHostEnvironment 还提供了几个常用判断方法:
isDevelopment()isStaging()isProduction()isEnvironment(name)
其中 isEnvironment(name) 是大小写敏感的。
默认配置与日志
主机默认按下面的顺序注册配置源:
./appsettings.json./appsettings.{environment}.json- 前缀为
cangjie_的环境变量 - 命令行参数
后注册的配置源优先级更高,所以命令行参数会覆盖前面的值。
如果你再通过 builder.configuration.add... 追加配置源,那么你追加的源会继续覆盖前面这些默认值。
日志的默认行为是:
- 注册控制台日志提供者
- 读取
logging配置节并应用日志规则
因此,只要配置里有 logging:* 相关键值,默认控制台日志就会跟着生效。
默认注册的服务
调用 build() 之后,主机会把这些服务放进容器:
| 服务 | 说明 |
|---|---|
IHostEnvironment | 当前环境名、内容根路径、应用名 |
IConfiguration | 合并后的配置对象 |
ILoggerFactory | 日志工厂 |
IHostApplicationLifetime | 启动和关闭回调 |
IHostLifetime | 主机与外部运行环境之间的生命周期桥接 |
如果你已经注册了自定义 IHostLifetime,主机不会再覆盖它;否则默认使用 ConsoleLifetime。
启动前校验
host.start() 在真正进入主机生命周期之前,会先检查容器里是否存在 IStartupValidator。
如果存在,就会先执行校验;校验通过后才继续启动托管服务和触发 onStarted。
这通常和选项系统一起使用:
import soulsoft_extensions_options.*
import soulsoft_extensions_hosting.*
import soulsoft_extensions_injection.*
class ServerOptions {
// 模拟需要在启动时校验的配置项。
public var port: Int64 = 0
}
main(args: Array<String>): Int64 {
// 创建通用主机构建器。
let builder = Host.createBuilder(args)
// 注册选项,并声明“启动时校验”。
let _ = builder.services.addOptionsWithValidateOnStart<ServerOptions>()
.configure { options =>
// 这里给选项一个默认端口。
options.port = 8080
}
.validate("port must be greater than 0") { options =>
// 启动前要求端口必须大于 0。
options.port > 0
}
// 构建主机后手动启动。
let host = builder.build()
host.start()
// 示例结束后主动停止主机。
host.stop()
return 0
}如果校验失败,host.start() 会直接抛出异常,onStarted 也不会执行。
托管服务
如果你希望主机启动时自动拉起后台任务,可以注册 IHostedService。
最常见的写法是继承 BackgroundService:
import std.sync.*
import soulsoft_extensions_hosting.*
import soulsoft_extensions_injection.*
main(args: Array<String>): Int64 {
// 创建主机构建器。
let builder = Host.createBuilder(args)
// 注册一个托管后台服务。
builder.services.addHostedService<TickWorker>()
// 构建主机。
let host = builder.build()
// 3 秒后触发停止,方便示例自动退出。
spawn {
sleep(Duration.second * 3)
host.lifetime.stopApplication()
}
// 运行主机。
host.run()
return 0
}
class TickWorker <: BackgroundService {
public override func run(): Unit {
// 后台服务持续执行,直到主机关闭。
while (true) {
println("tick")
sleep(Duration.second)
}
}
}这里的行为要点是:
addHostedService<T>()会把T注册成单例IHostedService- 主机启动时会对所有
IHostedService调用start() - 主机停止时会对所有
IHostedService调用stop() BackgroundService.start()会在后台Future中执行run()
当前实现里,BackgroundService.stop() 只是对内部 Future 发起取消请求,并不会把 isClosed() 置为 true。
这意味着你的 run() 最好写成可取消的循环,不要依赖“调用 stop() 后对象一定已经彻底结束”这种假设。
生命周期
IHostApplicationLifetime 提供 3 组回调:
| 回调 | 触发时机 |
|---|---|
onStarted | 主机完成启动后 |
onStopping | 调用 stopApplication() 时 |
onStopped | 主机停止托管服务后 |
这里有一个很重要的实现细节:
onStopping是由stopApplication()触发的host.stop()本身不会主动触发onStopping
如果你的清理逻辑必须放在“收到停止信号但服务还没停掉”的阶段,应当把它注册到 onStopping,并通过 stopApplication() 发起关闭。
ApplicationLifetime 的回调注册和关闭触发都做了线程同步,重复调用 stopApplication() 也是幂等的。
主机
对应用代码来说,通用主机最终表现为一个统一的运行外壳。
最常见的几个动作是:
build()把 builder 变成可运行的主机run()启动主机并阻塞等待关闭start()启动主机但不阻塞stop()执行停止流程
run() 做了什么
run() 的行为可以理解成:
- 调用
start() - 等待关闭信号
- 执行停止流程
- 最后释放主机资源
所以如果你只是要运行一个标准的后台应用,通常直接调用 host.run() 就够了。
平台差异
默认的 ConsoleLifetime 只在 os != "Windows" 时注册 SIGINT 和 SIGTERM 处理器。
这意味着:
- 在 Linux 和 macOS 上,
host.run()可以通过控制台信号进入关闭流程 - 在 Windows 上,当前实现的
ConsoleLifetime.waitForStart()和stop()是空实现
如果你要在 Windows 上获得同样的控制台关闭体验,应该自己注册一个 IHostLifetime,或者在应用里主动调用 host.lifetime.stopApplication()。