Skip to content

通用主机

Host.createBuilder(args) 一行搞定配置、日志、DI、环境、生命周期五大基础设施,让所有非 Web 程序的启动入口统一。

适用场景

  • 控制台工具、后台服务、定时任务——任何需要配置 + 日志 + DI 的非 Web 程序
  • 希望用统一入口管理应用启动和优雅关闭
  • 需要在不同环境(开发、预发布、生产)自动切换配置

如果你的程序是 Web 应用,直接看 Web 主机——它在通用主机基础上扩展了 HTTP 能力。

没有通用主机时,每个程序都要手搭配置、日志、DI,启动顺序靠程序员自觉,关闭流程没人管。通用主机把这些重复劳动收进 Host.createBuilder(args)build()run() 三步。

没有主机 vs 有主机的对比
cangjie
// 没有主机——每次都要手搭五大件
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
}
cangjie
// 有主机——三行搞定
import soulsoft_extensions_hosting.*

main(args: Array<String>): Int64 {
    let host = Host.createBuilder(args).build()
    host.run()
    return 0
}

快速开始

引入 soulsoft_extensions_hosting 后,三步跑起一个后台服务:

cangjie
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() 干了五件事:

  1. 从环境变量 cangjie_environment 读取环境名,默认为 Production
  2. 自动加载 appsettings.json + appsettings.{env}.json + 环境变量 + 命令行参数
  3. 注册控制台日志 + 从配置中读取日志过滤规则
  4. 注册生命周期服务(IHostApplicationLifetimeIHostLifetime
  5. 非生产环境下自动开启 validateScopesvalidateOnBuild

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 步停止托管服务时,BackgroundServicestop() 调用 Future.cancel() 取消执行——你的 run() 方法需要配合检查取消标记。

警告

Windows 上 ConsoleLifetimewaitForStart()stop() 为空操作,不注册信号处理器。Windows 下优雅关闭依赖其他机制触发 stopApplication()

环境

IHostEnvironment 用三个属性描述主机运行环境:

属性说明默认值
environmentName环境名称"Production"
contentRootPath应用内容根路径当前工作目录
applicationName应用名称空字符串

三种常用的设值方式:

系统环境变量(Windows):

powershell
$env:cangjie_environment = "Development"
cjpm run

VS Code(.vscode/settings.json):

json
{
    "terminal.integrated.env.windows": {
        "cangjie_environment": "Development"
    }
}

命令行参数:

bash
cjpm run -- --environment=Development

-- 之后的内容会原样传给 mainargs,主机在启动时自动解析。

使用内置方法判断环境:

cangjie
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()

cangjie
if (host.environment.isEnvironment("QA")) {
    // QA 环境特殊逻辑
}

三个标准环境常量在 Environments 中:

常量
Environments.Development"Development"
Environments.Staging"Staging"
Environments.Production"Production"

生命周期

IHostApplicationLifetime 提供三个回调注册点和一个主动触发关闭的方法:

cangjie
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
}
回调触发时机
onStartedIHostedService 全部启动后
onStopping收到关闭信号, IHostedService.stop() 之前
onStoppedIHostedService 全部停止后

如果想在程序内部主动触发关闭(比如健康检查发现致命错误),调用 stopApplication()

cangjie
host.lifetime.stopApplication()  // 触发完整关闭流程,仅首次调用生效

stopApplication() 内部用互斥锁保护,多次调用安全但只有第一次会触发 onStopping 回调。

常见用法

配置

Host.createBuilder() 自动按以下顺序加载配置(后加载的覆盖先加载的):

优先级来源说明
1(最低)appsettings.json基础默认配置
2appsettings.{env}.json环境专属配置(如 appsettings.Production.json
3环境变量(cangjie_ 前缀)容器注入的覆盖值
4(最高)命令行参数启动时手动指定

构建完成后通过 host.configuration 读取:

cangjie
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:

cangjie
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 条件中对取消标记的检查:

cangjie
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
}

要点:

  • BackgroundServicestart() 时通过 spawn 创建新的 Future 执行 run()
  • stop() 调用 Future.cancel() 取消执行
  • 你的 run() 方法需要配合检查取消标记——用 Thread.currentThread.hasPendingCancellation 作为循环条件

警告

addHostedService<T>() 注册为 Singleton。Singleton 不能依赖 Scoped 服务——如果你需要在托管服务中使用 Scoped 服务(如数据库连接),注入 IServiceScopeFactory 手动创建作用域。详见常见问题

手动实现 IHostedService——如果你的逻辑更复杂,比如需要在 start()stop() 中做额外的初始化/清理:

cangjie
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 容器注册以下服务——你不需要手动注册,直接注入就能用

服务接口实现类生命周期说明
IHostEnvironmentHostEnvironmentSingleton环境名、内容根路径、应用名
IConfigurationConfigurationManager.build()Singleton合并后的完整配置(JSON + 环境变量 + 命令行)
IHostApplicationLifetimeApplicationLifetimeSingleton生命周期回调 + stopApplication()
IHostLifetimeConsoleLifetimeSingleton(tryAdd平台信号处理:Linux/macOS 注册 SIGINT/SIGTERM,Windows 空操作
HostedServiceExecutorHostedServiceExecutorSingleton启动/停止所有 IHostedService,内部使用
ILoggerFactoryLoggerFactorySingleton日志工厂,已注册控制台提供者 + 从 logging 配置节读取过滤规则
IServiceProviderDI 容器自身Singleton服务定位器,build() 返回后固化不可再改

此表中所有内置服务均为 Singleton——它们是主机基础设施,必须全局唯一。标了 tryAdd 的表示你可以提前注册自己的实现覆盖默认值;未标的用 addSingleton 注册,重复注册会抛异常。

内置环境变量

主机自动读取以下 cangjie_ 前缀的环境变量:

环境变量写入字段默认值等效命令行参数
cangjie_environmentenvironmentName"Production"--environment=Staging
cangjie_contentRootPathcontentRootPath当前工作目录--contentRootPath=/app
cangjie_applicationNameapplicationName空字符串--applicationName=myapp
cangjie_*IConfiguration[key]

前三个写入 IHostEnvironment 的对应字段;所有 cangjie_ 前缀的变量(包括前三个)同时进入 IConfiguration——比如 cangjie_database__host=db01 可通过 host.configuration["database:host"] 读取(双下划线 __ 自动转为冒号分隔符 :)。

最佳实践

一个生产级的主机程序,结构清晰、层次分明。重点看三个层次——选项定义、服务实现、启动入口如何通过 DI 串联:

选项定义

把可调参数收敛到一个强类型类里:

cangjie
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(...)),详见选项模式

三条原则:

  1. 一个 BackgroundService 只做一件事——定时任务之间互不干扰。
  2. 通过构造函数注入依赖——不要在 run()new 对象。
  3. 非生产环境自动开启校验——HostBuilder.build() 在非 Production 环境自动开启 validateScopesvalidateOnBuild,帮你尽早发现配置错误。这是因为生产环境需要启动性能,而开发环境优先暴露问题。

常见问题

run() 阻塞了主线程,我想同时做别的事怎么办?

start() + waitForShutdown() 替代 run(),中间插入你的逻辑:

cangjie
let host = Host.createBuilder(args).build()
host.start()
// 在这里做别的事——比如给外部监控发一个就绪信号
host.waitForShutdown()

IHostedService 里怎么用 Scoped 服务?

addHostedService<T>() 注册为 Singleton,不能直接注入 Scoped 服务。解决办法:注入 IServiceScopeFactory,在 run() 里自己创建作用域:

cangjie
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 追加即可:

cangjie
let builder = Host.createBuilder(args)
builder.configuration.addJsonFile("myconfig.json")
builder.configuration.addEnvVars("MYAPP_")

相关专题

  • Web 主机 — 在通用主机基础上扩展 HTTP 服务器和中间件管道
  • 配置管理 — 配置源的完整 API 和绑定方式
  • 选项模式 — 从配置到强类型选项的绑定和校验
  • 日志记录 — 日志提供者注册和过滤规则
  • 依赖注入 — 服务注册、生命周期和作用域校验