Skip to content

通用主机

通用主机用于把配置、日志、依赖注入、生命周期和托管服务组织到同一个启动入口里。

它的意义,不是“帮你少写一行 main”,而是把应用启动时反复出现的基础设施问题收口到同一个模型里。

如果没有通用主机,一个稍微像样的服务程序通常都要自己处理这些事情:

  • 先读取命令行、环境变量和配置文件
  • 再初始化日志系统
  • 再创建和组装依赖注入容器
  • 再决定应用什么时候启动、什么时候停止
  • 如果还有后台任务,还要自己管理这些任务的启动和关闭顺序

这些代码往往不是业务本身,却几乎每个应用都要重复写一遍。

通用主机解决的就是这类重复问题:

  • 给应用提供统一的启动入口
  • 让配置、日志、DI 和生命周期按固定顺序完成装配
  • 让后台服务跟随主机一起启动和停止
  • 让应用代码更早进入“注册服务和编写业务逻辑”这一层,而不是反复搭底座

如果只看最小示例,通用主机其实就是这几个动作:

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

主机构建

通用主机的入口是:

cangjie
let builder = Host.createBuilder(args)

这一步不只是“new 一个 builder”,而是先把主机启动所需的默认基础设施准备好,再把可继续扩展的 HostBuilder 交给你。

默认环境

主机环境由一组单独的配置读取出来:

  • 读取环境变量,前缀是 cangjie_
  • 读取命令行参数
  • 从中取出 environmentcontentRootPathapplicationName

如果没有显式提供值,默认行为是:

默认值
environmentProduction
contentRootPath当前工作目录
applicationName空字符串

IHostEnvironment 还提供了几个常用判断方法:

  • isDevelopment()
  • isStaging()
  • isProduction()
  • isEnvironment(name)

其中 isEnvironment(name) 是大小写敏感的。

默认配置与日志

主机默认按下面的顺序注册配置源:

  1. ./appsettings.json
  2. ./appsettings.{environment}.json
  3. 前缀为 cangjie_ 的环境变量
  4. 命令行参数

后注册的配置源优先级更高,所以命令行参数会覆盖前面的值。

如果你再通过 builder.configuration.add... 追加配置源,那么你追加的源会继续覆盖前面这些默认值。

日志的默认行为是:

  • 注册控制台日志提供者
  • 读取 logging 配置节并应用日志规则

因此,只要配置里有 logging:* 相关键值,默认控制台日志就会跟着生效。

默认注册的服务

调用 build() 之后,主机会把这些服务放进容器:

服务说明
IHostEnvironment当前环境名、内容根路径、应用名
IConfiguration合并后的配置对象
ILoggerFactory日志工厂
IHostApplicationLifetime启动和关闭回调
IHostLifetime主机与外部运行环境之间的生命周期桥接

如果你已经注册了自定义 IHostLifetime,主机不会再覆盖它;否则默认使用 ConsoleLifetime

启动前校验

host.start() 在真正进入主机生命周期之前,会先检查容器里是否存在 IStartupValidator

如果存在,就会先执行校验;校验通过后才继续启动托管服务和触发 onStarted

这通常和选项系统一起使用:

cangjie
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

cangjie
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() 的行为可以理解成:

  1. 调用 start()
  2. 等待关闭信号
  3. 执行停止流程
  4. 最后释放主机资源

所以如果你只是要运行一个标准的后台应用,通常直接调用 host.run() 就够了。

平台差异

默认的 ConsoleLifetime 只在 os != "Windows" 时注册 SIGINTSIGTERM 处理器。

这意味着:

  • 在 Linux 和 macOS 上,host.run() 可以通过控制台信号进入关闭流程
  • 在 Windows 上,当前实现的 ConsoleLifetime.waitForStart()stop() 是空实现

如果你要在 Windows 上获得同样的控制台关闭体验,应该自己注册一个 IHostLifetime,或者在应用里主动调用 host.lifetime.stopApplication()