通用主机
Host.createBuilder(args)一行搞定配置、日志、DI、环境、生命周期五大基础设施,让所有非 Web 程序的启动入口统一。
适用场景
- 控制台工具、后台服务、定时任务——任何需要配置 + 日志 + DI 的非 Web 程序
- 希望用统一入口管理应用启动和优雅关闭
- 需要在不同环境(开发、预发布、生产)自动切换配置
如果你的程序是 Web 应用,直接看 Web 主机——它在通用主机基础上扩展了 HTTP 能力。
没有通用主机时,每个程序都要手搭配置、日志、DI,启动顺序靠程序员自觉,关闭流程没人管。通用主机把这些重复劳动收进 Host.createBuilder(args) → build() → run() 三步。
没有主机 vs 有主机的对比
// 没有主机——每次都要手搭五大件
import soulsoft_extensions_logging.*
import soulsoft_extensions_injection.*
import soulsoft_extensions_configuration.*
import soulsoft_extensions_logging_console.*
main(args: Array<String>): Int64 {
let config = ConfigurationManager()
.addJsonFile("appsettings.json")
.addEnvVars("cangjie_")
.addCmdArgs(args)
.build()
let services = ServiceCollection()
services.addSingleton<IConfiguration>(config)
services.addLogging { builder => builder.addConsole() }
let provider = services.build()
println("启动完成,按 Ctrl+C 停止")
sleep(Duration.second * 3600)
return 0
}// 有主机——三行搞定
import soulsoft_extensions_hosting.*
main(args: Array<String>): Int64 {
let host = Host.createBuilder(args).build()
host.run()
return 0
}快速开始
引入 soulsoft_extensions_hosting 后,三步跑起一个后台服务:
import soulsoft_extensions_hosting.*
import soulsoft_extensions_logging.*
import soulsoft_extensions_injection.*
class TimerWorker <: BackgroundService {
private let logger: ILogger
public init(factory: ILoggerFactory) {
logger = factory.createLogger<TimerWorker>()
}
public func run(): Unit {
while (!Thread.currentThread.hasPendingCancellation) {
logger.info("心跳...")
sleep(Duration.second * 5)
}
}
}
main(args: Array<String>): Int64 {
// 1. 创建构建器 → 2. 注册自己的服务 → 3. 构建并运行
let builder = Host.createBuilder(args)
builder.services.addHostedService<TimerWorker>()
let host = builder.build()
host.run()
return 0
}运行后按 Ctrl+C,主机会自动触发优雅关闭——TimerWorker.run() 中的循环检测到取消标记后退出。
整个流程只有三个角色:
| 步骤 | 做什么 | 用的类型 |
|---|---|---|
| 创建构建器 | 自动加载环境、配置、日志,准备 DI 容器 | Host.createBuilder(args) |
| 配置服务 | 往容器里注册你自己的服务 | builder.services.addXxx() |
| 构建并运行 | 固化容器,启动后阻塞直到收到关闭信号 | build() → run() |
核心概念
主机三阶段
主机分三个阶段:创建构建器 → 构建 → 运行。这张图展示了每个阶段的角色和产物:
Host.createBuilder(args)
│
▼
HostBuilder build()
├─ services 注册服务 ──────────▶ Host (IHost)
├─ configuration 追加配置源 ├─ services 已固化的容器
├─ environment 环境判断 ├─ configuration 合并后的配置
├─ logging 日志提供者 ├─ environment 环境信息
└─ properties 自定义元数据 ├─ logger 主机日志器
├─ lifetime 生命周期回调
├─ run() 启动 + 阻塞等待关闭
├─ start() 启动(非阻塞)
└─ stop() 停止Host.createBuilder() 干了五件事:
- 从环境变量
cangjie_environment读取环境名,默认为Production - 自动加载
appsettings.json+appsettings.{env}.json+ 环境变量 + 命令行参数 - 注册控制台日志 + 从配置中读取日志过滤规则
- 注册生命周期服务(
IHostApplicationLifetime、IHostLifetime) - 非生产环境下自动开启
validateScopes和validateOnBuild
build() 把这些固化下来,此后容器不可再改。run() 启动所有托管服务,然后在当前线程阻塞,直到收到 Ctrl+C 或调用了 stopApplication()。
启动流程
host.run() 内部走的是严格顺序:
start()
├── 1. 打印环境信息(环境名、内容根路径)
├── 2. 执行启动校验器(如果你注册了 IStartupValidator)
├── 3. IHostLifetime.waitForStart()
│ └── Linux/macOS:注册 SIGINT/SIGTERM 信号处理器
│ └── Windows:空操作
├── 4. 启动所有 IHostedService
└── 5. 触发 onStarted 回调
[外部信号: Ctrl+C / SIGTERM]
└── Linux/macOS:ConsoleLifetime 信号处理器调用 stopApplication()
├── 触发 onStopping 回调
└── 解除 waitForShutdown() 阻塞
waitForShutdown()
├── 6. 阻塞直到 stopApplication() 被调用后返回
└── 7. 调用 stop()
stop()
├── 8. 停止所有 IHostedService
├── 9. 触发 onStopped 回调
└── 10. IHostLifetime.stop()(注销信号处理器)
close()
└── 11. 关闭 IServiceProvider,释放所有 Resource关键规则:
start()之后的步骤如果抛异常,run()的finally块会确保close()被调用。- 第 5 步
onStarted在所有托管服务启动之后才触发——此时应用已完全就绪。 - 第 8 步停止托管服务时,
BackgroundService的stop()调用Future.cancel()取消执行——你的run()方法需要配合检查取消标记。
警告
Windows 上 ConsoleLifetime 的 waitForStart() 和 stop() 为空操作,不注册信号处理器。Windows 下优雅关闭依赖其他机制触发 stopApplication()。
环境
IHostEnvironment 用三个属性描述主机运行环境:
| 属性 | 说明 | 默认值 |
|---|---|---|
environmentName | 环境名称 | "Production" |
contentRootPath | 应用内容根路径 | 当前工作目录 |
applicationName | 应用名称 | 空字符串 |
三种常用的设值方式:
系统环境变量(Windows):
$env:cangjie_environment = "Development"
cjpm runVS Code(.vscode/settings.json):
{
"terminal.integrated.env.windows": {
"cangjie_environment": "Development"
}
}命令行参数:
cjpm run -- --environment=Development
--之后的内容会原样传给main的args,主机在启动时自动解析。
使用内置方法判断环境:
import soulsoft_extensions_hosting.*
main(args: Array<String>): Int64 {
let host = Host.createBuilder(args).build()
if (host.environment.isDevelopment()) {
println("开发环境——开启详细日志")
}
if (host.environment.isProduction()) {
println("生产环境——精简日志")
}
host.run()
return 0
}如果只想判断自定义环境名,用 isEnvironment():
if (host.environment.isEnvironment("QA")) {
// QA 环境特殊逻辑
}三个标准环境常量在 Environments 中:
| 常量 | 值 |
|---|---|
Environments.Development | "Development" |
Environments.Staging | "Staging" |
Environments.Production | "Production" |
生命周期
IHostApplicationLifetime 提供三个回调注册点和一个主动触发关闭的方法:
import soulsoft_extensions_hosting.*
main(args: Array<String>): Int64 {
let builder = Host.createBuilder(args)
builder.services.addHostedService<TimerWorker>()
let host = builder.build()
host.lifetime.onStarted {
println("所有托管服务已启动,应用已就绪")
}
host.lifetime.onStopping {
println("收到关闭信号,准备清理...")
}
host.lifetime.onStopped {
println("所有托管服务已停止,再见")
}
host.run()
return 0
}| 回调 | 触发时机 |
|---|---|
onStarted | IHostedService 全部启动后 |
onStopping | 收到关闭信号,在 IHostedService.stop() 之前 |
onStopped | IHostedService 全部停止后 |
如果想在程序内部主动触发关闭(比如健康检查发现致命错误),调用 stopApplication():
host.lifetime.stopApplication() // 触发完整关闭流程,仅首次调用生效stopApplication() 内部用互斥锁保护,多次调用安全但只有第一次会触发 onStopping 回调。
常见用法
配置
Host.createBuilder() 自动按以下顺序加载配置(后加载的覆盖先加载的):
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1(最低) | appsettings.json | 基础默认配置 |
| 2 | appsettings.{env}.json | 环境专属配置(如 appsettings.Production.json) |
| 3 | 环境变量(cangjie_ 前缀) | 容器注入的覆盖值 |
| 4(最高) | 命令行参数 | 启动时手动指定 |
构建完成后通过 host.configuration 读取:
import soulsoft_extensions_hosting.*
main(args: Array<String>): Int64 {
let host = Host.createBuilder(args).build()
let dbHost = host.configuration["database:host"]
let dbPort = host.configuration.getValue<Int64>("database:port")
println("数据库地址:${dbHost}:${dbPort}")
host.run()
return 0
}如果你想在构建阶段追加额外的配置源,在 build() 之前通过 builder.configuration 添加——它是 ConfigurationManager,支持所有内置配置源 API:
let builder = Host.createBuilder(args)
builder.configuration.addJsonFile("custom.json")
builder.configuration.addEnvVars("MYAPP_")
builder.services.addHostedService<TimerWorker>()
let host = builder.build()如果需要运行时读取配置的详细用法,见 配置管理。
托管服务
托管服务是主机的核心扩展点——主机负责生命周期,你负责业务逻辑。
继承 BackgroundService——实现 run() 方法即可。重点关注 while 条件中对取消标记的检查:
import soulsoft_extensions_hosting.*
import soulsoft_extensions_logging.*
import soulsoft_extensions_injection.*
class DataSyncWorker <: BackgroundService {
private let logger: ILogger
public init(factory: ILoggerFactory) {
logger = factory.createLogger<DataSyncWorker>()
}
public func run(): Unit {
while (!Thread.currentThread.hasPendingCancellation) {
logger.info("同步数据...")
sleep(Duration.second * 30)
}
logger.info("数据同步已停止")
}
}
main(args: Array<String>): Int64 {
let builder = Host.createBuilder(args)
builder.services.addHostedService<DataSyncWorker>()
let host = builder.build()
host.run()
return 0
}要点:
BackgroundService在start()时通过spawn创建新的 Future 执行run()stop()调用Future.cancel()取消执行- 你的
run()方法需要配合检查取消标记——用Thread.currentThread.hasPendingCancellation作为循环条件
警告
addHostedService<T>() 注册为 Singleton。Singleton 不能依赖 Scoped 服务——如果你需要在托管服务中使用 Scoped 服务(如数据库连接),注入 IServiceScopeFactory 手动创建作用域。详见常见问题。
手动实现 IHostedService——如果你的逻辑更复杂,比如需要在 start() 和 stop() 中做额外的初始化/清理:
import soulsoft_extensions_hosting.*
import soulsoft_extensions_logging.*
import soulsoft_extensions_injection.*
import std.sync.*
class ConnectionPoolService <: IHostedService {
private let logger: ILogger
private var started = AtomicBool(false)
public init(factory: ILoggerFactory) {
logger = factory.createLogger<ConnectionPoolService>()
}
public func start(): Unit {
started.store(true)
logger.info("连接池已初始化,最大连接数:10")
}
public func stop(): Unit {
started.store(false)
logger.info("连接池已关闭,释放所有连接")
}
}
main(args: Array<String>): Int64 {
let builder = Host.createBuilder(args)
builder.services.addHostedService<ConnectionPoolService>()
let host = builder.build()
host.run()
return 0
}主机启动时遍历所有 IHostedService,按注册顺序逐个调用 start();停止时逐个调用 stop()。
内置服务
Host.createBuilder().build() 自动往 DI 容器注册以下服务——你不需要手动注册,直接注入就能用:
| 服务接口 | 实现类 | 生命周期 | 说明 |
|---|---|---|---|
IHostEnvironment | HostEnvironment | Singleton | 环境名、内容根路径、应用名 |
IConfiguration | ConfigurationManager.build() | Singleton | 合并后的完整配置(JSON + 环境变量 + 命令行) |
IHostApplicationLifetime | ApplicationLifetime | Singleton | 生命周期回调 + stopApplication() |
IHostLifetime | ConsoleLifetime | Singleton(tryAdd) | 平台信号处理:Linux/macOS 注册 SIGINT/SIGTERM,Windows 空操作 |
HostedServiceExecutor | HostedServiceExecutor | Singleton | 启动/停止所有 IHostedService,内部使用 |
ILoggerFactory | LoggerFactory | Singleton | 日志工厂,已注册控制台提供者 + 从 logging 配置节读取过滤规则 |
IServiceProvider | DI 容器自身 | Singleton | 服务定位器,build() 返回后固化不可再改 |
此表中所有内置服务均为 Singleton——它们是主机基础设施,必须全局唯一。标了 tryAdd 的表示你可以提前注册自己的实现覆盖默认值;未标的用 addSingleton 注册,重复注册会抛异常。
内置环境变量
主机自动读取以下 cangjie_ 前缀的环境变量:
| 环境变量 | 写入字段 | 默认值 | 等效命令行参数 |
|---|---|---|---|
cangjie_environment | environmentName | "Production" | --environment=Staging |
cangjie_contentRootPath | contentRootPath | 当前工作目录 | --contentRootPath=/app |
cangjie_applicationName | applicationName | 空字符串 | --applicationName=myapp |
cangjie_* | IConfiguration[key] | — | — |
前三个写入 IHostEnvironment 的对应字段;所有 cangjie_ 前缀的变量(包括前三个)同时进入 IConfiguration——比如 cangjie_database__host=db01 可通过 host.configuration["database:host"] 读取(双下划线 __ 自动转为冒号分隔符 :)。
最佳实践
一个生产级的主机程序,结构清晰、层次分明。重点看三个层次——选项定义、服务实现、启动入口如何通过 DI 串联:
选项定义
把可调参数收敛到一个强类型类里:
import soulsoft_extensions_hosting.*
import soulsoft_extensions_logging.*
import soulsoft_extensions_options.*
import soulsoft_extensions_injection.*
// 1. 定义选项:把可调参数收敛到一个强类型类里
class OrderCleanupOptions {
public var maxAgeDays: Int64 = 30
public var intervalMinutes: Int64 = 10
}
### 服务实现
后台服务注入 `IOptions<T>`,只管自己的业务逻辑:
class OrderCleanupWorker <: BackgroundService {
private let logger: ILogger
private let options: OrderCleanupOptions
public init(factory: ILoggerFactory, options: IOptions<OrderCleanupOptions>) {
logger = factory.createLogger<OrderCleanupWorker>()
this.options = options.value
}
public func run(): Unit {
while (!Thread.currentThread.hasPendingCancellation) {
logger.info("清理 ${options.maxAgeDays} 天前的过期订单...")
sleep(Duration.minute * options.intervalMinutes)
}
}
}
### 启动入口
配置选项 + 注册服务,一行链完成:
main(args: Array<String>): Int64 {
let builder = Host.createBuilder(args)
builder.services.configure<OrderCleanupOptions> { options =>
options.intervalMinutes = 5
options.maxAgeDays = 60
}
builder.services.addHostedService<OrderCleanupWorker>()
let host = builder.build()
host.lifetime.onStarted { println("订单清理服务就绪") }
host.lifetime.onStopping { println("订单清理服务即将停止") }
host.run()
return 0
}也可以从配置文件绑定选项——只需把 configure 换成 addOptions<T>().bind(config.getSection(...)),详见选项模式。
三条原则:
- 一个
BackgroundService只做一件事——定时任务之间互不干扰。 - 通过构造函数注入依赖——不要在
run()里new对象。 - 非生产环境自动开启校验——
HostBuilder.build()在非 Production 环境自动开启validateScopes和validateOnBuild,帮你尽早发现配置错误。这是因为生产环境需要启动性能,而开发环境优先暴露问题。
常见问题
run() 阻塞了主线程,我想同时做别的事怎么办?
用 start() + waitForShutdown() 替代 run(),中间插入你的逻辑:
let host = Host.createBuilder(args).build()
host.start()
// 在这里做别的事——比如给外部监控发一个就绪信号
host.waitForShutdown()IHostedService 里怎么用 Scoped 服务?
addHostedService<T>() 注册为 Singleton,不能直接注入 Scoped 服务。解决办法:注入 IServiceScopeFactory,在 run() 里自己创建作用域:
import soulsoft_extensions_hosting.*
import soulsoft_extensions_logging.*
import soulsoft_extensions_injection.*
class OrderCleanupWorker <: BackgroundService {
private let logger: ILogger
private let scopeFactory: IServiceScopeFactory
public init(factory: ILoggerFactory, scopeFactory: IServiceScopeFactory) {
logger = factory.createLogger<OrderCleanupWorker>()
this.scopeFactory = scopeFactory
}
public func run(): Unit {
while (!Thread.currentThread.hasPendingCancellation) {
try (scope = scopeFactory.createScope()) {
let db = scope.services.getOrThrow<IDbConnection>()
db.query("DELETE FROM orders WHERE expired = true")
}
sleep(Duration.minute * 10)
}
}
}try 块结束时 scope 自动关闭,其中的 Scoped 服务(如 IDbConnection)一并释放。Singleton 不能依赖 Scoped 的原因是:Singleton 被整个应用长期持有,而 Scoped 应跟随请求结束而释放;一旦前者持有后者,会把短生命周期对象错误地拉长,造成资源释放时机错乱。
多个 BackgroundService 的启动顺序是什么?
按注册顺序逐个调用 start()。但因为 BackgroundService.start() 是用 spawn 启动新 Future,实际上是并发的。如果你需要严格顺序(比如 A 初始化完成后 B 才能启动),实现 IHostedService 并自己控制。
我不想用默认的 JSON 文件和环境变量前缀怎么办?
通常不需要——Host.createBuilder() 已覆盖绝大多数场景。如果你的配置文件名或环境变量前缀确实不同,通过 builder.configuration 追加即可:
let builder = Host.createBuilder(args)
builder.configuration.addJsonFile("myconfig.json")
builder.configuration.addEnvVars("MYAPP_")