选项
依赖注入解决的是服务的创建、依赖传递和生命周期管理。 Options 则把“配置”也变成一种可由 DI 统一管理的强类型对象,避免业务代码自己读配置、自己做转换、自己处理默认值。
在 Spire 中:
soulsoft_extensions_options负责选项对象的注册、命名、覆盖和验证soulsoft_extensions_options_configuration负责把IConfiguration绑定到选项对象
最小示例
import soulsoft_extensions_injection.*
import soulsoft_extensions_options.*
class DbOptions {
// 定义强类型数据库配置。
public var host: String = ""
public var port: Int64 = 0
}
main(): Int64 {
// 创建 DI 服务集合。
let services = ServiceCollection()
// 注册默认名称的 DbOptions 配置委托。
services.configure<DbOptions>({
options =>
options.host = "127.0.0.1"
options.port = 5432
})
// 构建容器并解析 IOptions<DbOptions>。
let provider = services.build()
let options = provider.getOrThrow<IOptions<DbOptions>>()
println(options.value.host)
println(options.value.port)
return 0
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
上面这段代码体现的是:
- 注册阶段,把“如何配置
DbOptions”的函数委托登记到容器 - 解析阶段,通过
IOptions<DbOptions>取回最终的强类型配置对象
注册选项
configure<TOptions>()
最常用的入口是 ServiceCollection.configure<TOptions>()。
import soulsoft_extensions_injection.*
import soulsoft_extensions_options.*
class CacheOptions {
// 定义缓存前缀和过期时间配置。
public var prefix: String = ""
public var ttlSeconds: Int64 = 0
}
main(): Int64 {
let services = ServiceCollection()
// 注册 CacheOptions 的默认配置。
services.configure<CacheOptions>({
options =>
options.prefix = "spire:"
options.ttlSeconds = 300
})
// 解析默认名称的选项对象。
let provider = services.build()
let options = provider.getOrThrow<IOptions<CacheOptions>>()
println(options.value.prefix)
println(options.value.ttlSeconds)
return 0
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
这个 API 会做两件事:
- 确保选项基础设施已注册
- 把当前这段配置逻辑以函数委托的形式登记到该类型的默认名称上
这里要特别注意:
configure<TOptions>({ ... })传入的是一个函数委托- 注册阶段只是把这个委托存进容器
- 不是在调用
configure()时立刻创建TOptions并执行赋值
真正的执行时机是在选项对象首次创建时。 当前实现里,容器会在创建该选项时取出这些已注册的配置委托,并按顺序依次作用到同一个 TOptions 实例上。
为什么不用普通单例
你当然也可以直接这样写:
// 直接把某个实例作为单例放进容器。
services.addSingleton(DbOptions())2
但这和 Options 不一样。
直接注册实例只有“把一个对象放进容器”这一层语义,而 Options 额外提供了:
- 默认名称和命名名称模型
configureAfterconfigureAll/configureAfterAll- 内置验证流程
IStartupValidator- 固定的执行顺序
所以 Options 的重点不是“容器里有一个配置对象”,而是“容器里有一套受控的配置生成与校验流程”。
解析选项
IOptions<T>
IOptions<T> 用于解析默认名称的选项实例。
// 解析默认名称的选项实例。
let options = provider.getOrThrow<IOptions<AppOptions>>()
println(options.value)2
3
它有两个关键语义:
value是懒加载的,首次访问时才真正创建选项对象- 在同一个
ServiceProvider中,默认选项只会创建一次并被缓存
这意味着你多次解析 IOptions<T>,拿到的是同一份默认配置对象。
IOptionsMonitor<T>
IOptionsMonitor<T> 用于解析命名选项。
// 解析命名选项访问器。
let monitor = provider.getOrThrow<IOptionsMonitor<AppOptions>>()
// currentValue 对应默认名称。
let defaultValue = monitor.currentValue
// get("tenant1") 读取指定名称的选项。
let tenant1Value = monitor.get("tenant1")2
3
4
5
6
这里的真实语义是:
currentValue等价于get("")- 默认名称就是空字符串
"" - 同名实例会缓存
- 不同名称之间相互隔离
命名选项
命名选项是 soulsoft_extensions_options 的核心能力之一。 它适合:
- 多租户
- 多实例客户端
- 同一类型需要维护多份独立配置
import soulsoft_extensions_injection.*
import soulsoft_extensions_options.*
class TenantOptions {
public var endpoint: String = ""
}
main(): Int64 {
let services = ServiceCollection()
// 注册默认配置。
services.configure<TenantOptions>({
options => options.endpoint = "https://default.example.com"
})
// 注册 tenant1 专属配置。
services.configure<TenantOptions>("tenant1", {
options => options.endpoint = "https://t1.example.com"
})
// 注册 tenant2 专属配置。
services.configure<TenantOptions>("tenant2", {
options => options.endpoint = "https://t2.example.com"
})
// 通过 IOptionsMonitor 读取不同名称的实例。
let provider = services.build()
let monitor = provider.getOrThrow<IOptionsMonitor<TenantOptions>>()
println(monitor.currentValue.endpoint)
println(monitor.get("tenant1").endpoint)
println(monitor.get("tenant2").endpoint)
return 0
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
这里要特别分清楚:
IOptions<T>只对应默认名称IOptionsMonitor<T>才能访问指定名称
如果你的场景天然存在“同类型多份配置”,那就应该直接以 IOptionsMonitor<T> 为主接口,而不是后面再补。
执行顺序
选项对象在创建时不是“随便执行几段委托”,而是有固定顺序的。
当前实现的顺序是:
- 执行所有
configure - 执行所有
configureAfter - 执行所有
validate
因此理解这套系统时,最重要的一个原则就是:
configure负责提供值,configureAfter负责最终覆盖,validate负责最后把关。
configureAfter
class ServerOptions {
public var port: Int64 = 0
}
let services = ServiceCollection()
// 先注册基础端口。
services.configure<ServerOptions>({
options => options.port = 8080
})
// 再通过 configureAfter 做最终覆盖。
services.configureAfter<ServerOptions>({
options => options.port = 9090
})
let provider = services.build()
let options = provider.getOrThrow<IOptions<ServerOptions>>()
println(options.value.port) // 90902
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
configureAfter 的定位不是“另一个 configure”,而是“在所有 configure 都执行完之后再做统一覆盖”。
configureAll 与 configureAfterAll
这两个 API 用于“作用于该类型的全部名称”。
class StorageOptions {
public var region: String = ""
public var bucket: String = ""
}
let services = ServiceCollection()
// 给 tenant1 设置独立 bucket。
services.configure<StorageOptions>("tenant1", {
options => options.bucket = "bucket-a"
})
// 给 tenant2 设置独立 bucket。
services.configure<StorageOptions>("tenant2", {
options => options.bucket = "bucket-b"
})
// 给全部名称统一设置 region。
services.configureAll<StorageOptions>({
options => options.region = "cn-hangzhou"
})
// 最后再对全部名称统一改写 bucket。
services.configureAfterAll<StorageOptions>({
options => options.bucket = "normalized-" + options.bucket
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
它们的语义分别是:
configureAll:对该类型全部名称执行前置配置configureAfterAll:对该类型全部名称执行后置配置
如果 configureAll 注册在某个命名 configure 之后,它会覆盖前面写入的值。 如果你需要“最后兜底改写”,通常优先考虑 configureAfterAll。
链式注册
当一份配置需要多个步骤共同组成时,可以使用 addOptions<TOptions>() 返回的 OptionsBuilder<T> 做链式注册。
import soulsoft_extensions_injection.*
import soulsoft_extensions_options.*
class ServiceOptions {
public var version: Int64 = 0
public var name: String = ""
}
main(): Int64 {
let services = ServiceCollection()
// 通过 OptionsBuilder 链式注册配置和校验。
services.addOptions<ServiceOptions>()
.configure {
options => options.version = 1
}
.configure {
options => options.name = "gateway"
}
.configureAfter {
options => options.version = 2
}
.validate("version 必须大于 1") {
options => options.version > 1
}
// 构建后解析最终配置结果。
let provider = services.build()
let options = provider.getOrThrow<IOptions<ServiceOptions>>()
println(options.value.version)
println(options.value.name)
return 0
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
这条链的本质仍然是注册:
.configure(...)注册前置配置委托.configureAfter(...)注册后置配置委托.validate(...)注册验证委托
除了只接收 TOptions 的版本外,configure、configureAfter、validate 都还有可访问 IServiceProvider 的重载。 这意味着你可以在配置和验证时读取容器中的其他服务。
验证
validate()
validate() 在选项对象第一次真正创建时执行。 如果校验失败,会抛出 OptionsValidationException。
class AppOptions {
public var port: Int64 = 0
}
let services = ServiceCollection()
// 注册一个非法端口值。
services.addOptions<AppOptions>()
.configure {
options => options.port = 0
}
.validate("port 必须大于 0") {
options => options.port > 0
}
let provider = services.build()
let options = provider.getOrThrow<IOptions<AppOptions>>()
// 首次访问 value 时触发校验。
let _ = options.value2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这个异常不是在注册时抛,也不是在 build() 时抛,而是在首次访问该选项时抛。
validateOnStart()
如果你不想把问题拖到第一次访问才暴露,就要用 validateOnStart()。
import soulsoft_extensions_injection.*
import soulsoft_extensions_options.*
import soulsoft_extensions_options.exceptions.*
class AppOptions {
public var version: Int64 = 0
}
main(): Int64 {
let services = ServiceCollection()
// 注册选项,并在启动阶段完成验证。
services.addOptions<AppOptions>()
.configure {
options => options.version = 1
}
.validate("version 必须大于 1") {
options => options.version > 1
}
.validateOnStart()
// 解析统一启动校验器并主动执行。
let provider = services.build()
let validator = provider.getOrThrow<IStartupValidator>()
validator.validate()
return 0
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
它的作用是把该选项登记到统一启动校验器中。
IStartupValidator.validate() 的行为是:
- 单个失败时抛
OptionsValidationException - 多个失败时抛
AggregateException
如果你只是想快速注册“默认开启启动校验”的选项,也可以用:
// 快速创建默认启用启动校验的选项构建器。
services.addOptionsWithValidateOnStart<AppOptions>()2
使用边界
1. 选项类型需要能被反射创建
当前实现会通过反射构造 TOptions。 因此类型需要:
- 继承自
Object - 具备可用的无参构造能力
否则在首次创建选项时会抛异常。
2. IOptions<T> 是默认名称,不是“全部名称”
这点很容易误解。
IOptions<T>只代表默认名称IOptionsMonitor<T>才代表“按名称读取”
所以只要你的配置模型里出现“tenant1 / tenant2 / default”这样的概念,就应该优先用 IOptionsMonitor<T>。
3. 选项对象是缓存的
当前实现里:
- 默认名称通过
IOptions<T>单例缓存 - 命名实例通过
IOptionsMonitor<T>按名称缓存
也就是说,Options 不是“每次 get 都重新跑一遍配置”,而是“首次创建,之后复用”。
4. configureAfter 的意义是覆盖,不是补充
如果某个字段既在 configure 中赋值,又在 configureAfter 中赋值,最终以后者为准。 理解这一点后,很多配置覆盖行为就不会混乱。
引入配置绑定
到这里为止,我们讲的都还是 soulsoft_extensions_options 本体。
但在真实项目里,配置往往不是手写在 configure { ... } 里,而是来自:
- JSON
- 环境变量
- 命令行
- 内存配置
- 自定义配置源
这时就应该在 options 之上,再引入 soulsoft_extensions_options_configuration。
它不是替代 options,而是建立在 options 之上的绑定扩展:
options负责选项对象的生命周期、命名模型和验证模型options_configuration负责把IConfiguration的值填进这个对象
配置绑定
最常见用法
import soulsoft_extensions_configuration.*
import soulsoft_extensions_injection.*
import soulsoft_extensions_options.*
import soulsoft_extensions_options_configuration.*
class ServerOptions {
public var host: String = ""
public var port: Int64 = 0
public var enabled: Bool = false
}
main(): Int64 {
// 构造一份配置根。
let root = ConfigurationManager()
.addJsonString(###"
{
"server": {
"host": "127.0.0.1",
"port": 8080,
"enabled": true
}
}
"###)
.build()
let services = ServiceCollection()
// 把 server 配置节绑定到 ServerOptions。
services.configure<ServerOptions>(root.getSection("server"))
// 解析绑定后的选项对象。
let provider = services.build()
let options = provider.getOrThrow<IOptions<ServerOptions>>()
println(options.value.host)
println(options.value.port)
println(options.value.enabled)
return 0
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
这时:
ConfigurationManager负责读取配置services.configure<TOptions>(section)负责把配置节绑定到选项类型IOptions<T>负责统一访问结果
它支持什么绑定
当前实现已经覆盖的常见场景包括:
StringBoolInt8到Int64UInt8到UInt64Float16到Float64?String?Bool?Int64?Float64Array<String>- 数值数组,如
Array<Int64>、Array<UInt32>、Array<Float64> - 嵌套类对象
嵌套对象
class DatabaseOptions {
public var host: String = ""
public var port: Int64 = 0
}
class AppOptions {
public var name: String = ""
public var database: DatabaseOptions = DatabaseOptions()
}
let root = ConfigurationManager()
.addMemory([
("name", "spire"),
("database:host", "localhost"),
("database:port", "3306")
])
.build()
let services = ServiceCollection()
// 把整棵配置根绑定到 AppOptions。
services.configure<AppOptions>(root)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这里 database 会自动绑定 database:* 这一层配置。
数组
class ClusterOptions {
public var nodes: Array<String> = []
public var ports: Array<Int64> = []
}
let root = ConfigurationManager()
.addMemory([
("nodes:0", "node-a"),
("nodes:1", "node-b"),
("ports:0", "8080"),
("ports:1", "9090")
])
.build()
let services = ServiceCollection()
// 按数组下标键把配置绑定到数组字段。
services.configure<ClusterOptions>(root)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
数组依赖 field:0、field:1 这样的子键。
可空字段
?T 字段不会被跳过。 当配置存在且能解析时,会绑定成 Some(value);否则保持默认值。
class FeatureOptions {
public var name: String = ""
public var enabled: ?Bool = None
}
let root = ConfigurationManager()
.addMemory([
("name", "search"),
("enabled", "true")
])
.build()
let services = ServiceCollection()
// 可空字段在配置存在时会绑定为 Some(value)。
services.configure<FeatureOptions>(root)2
3
4
5
6
7
8
9
10
11
12
13
14
15
绑定边界
1. 只绑定 var
配置绑定只会写入可变字段:
var会绑定let会跳过并保留原值
2. 解析失败时保留默认值
当前实现的策略是:
- 成功解析就写入
- 解析失败就不写入
- 键不存在也不写入
因此字段会保留类里的默认值,而不是直接抛出绑定异常。
3. 命名绑定只对同名实例生效
// tenant1 和 tenant2 会分别绑定到不同名称的选项实例。
services.configure<AppOptions>("tenant1", config1)
services.configure<AppOptions>("tenant2", config2)2
3
这两份绑定彼此隔离。 如果你读取一个未注册的名称,会创建一个默认实例,但不会套用其他名称上的绑定结果。
小结
理解这套系统时,建议按下面这个顺序记:
- 先把
soulsoft_extensions_options看成 DI 的配置扩展 - 先掌握
IOptions<T>、IOptionsMonitor<T>、OptionsBuilder<T> - 再理解命名选项、后置覆盖和验证
- 最后再引入
soulsoft_extensions_options_configuration,把IConfiguration绑定进来
如果只记一句话,可以记这一句:
soulsoft_extensions_options负责把配置变成可由 DI 管理的强类型对象,soulsoft_extensions_options_configuration负责把外部配置源填充进这个对象。