依赖注入
依赖注入(Dependency Injection,简称 DI)用于管理对象的创建、依赖关系和生命周期。 在 Spire 的实现中,这一能力来自源码模块 soulsoft_extensions_injection。
在 soulsoft_extensions_injection 中,依赖注入有 4 个最重要的特性:
- 用
ServiceCollection统一注册服务 - 用
IServiceProvider统一解析服务 - 支持
Singleton、Scoped、Transient三种生命周期 - 作用域结束时会自动释放实现了
Resource的服务,并支持工厂注册、keyed service 和反射注册
最小示例
这套容器最常见的使用流程只有 4 步:
- 创建
ServiceCollection - 注册服务
- 调用
build()构建容器 - 通过
IServiceProvider解析服务
可以先看一个最小示例。
import soulsoft_extensions_injection.*
interface IDbConnection {}
class MySqlConnection <: IDbConnection {}
class DbContext {
// 通过构造函数声明对 IDbConnection 的依赖。
public DbContext(public let connection: IDbConnection) {}
}
main(): Int64 {
// 创建服务集合并注册接口和实现。
let services = ServiceCollection()
services.addSingleton<IDbConnection, MySqlConnection>()
services.addSingleton<DbContext, DbContext>()
// 构建根容器并解析 DbContext。
let root = services.build()
let context = root.getOrThrow<DbContext>()
println(context.connection is MySqlConnection)
return 0
}这个例子体现了 DI 的两个核心价值:
DbContext不需要自己创建MySqlConnection- 容器会在解析
DbContext时,自动补齐它构造函数里的依赖
核心类型
| 类型 | 作用 |
|---|---|
ServiceCollection | 注册服务。它本质上是一个 List<ServiceDescriptor> |
ServiceDescriptor | 描述一条注册信息,包括服务类型、实现类型、生命周期、可选 key 等 |
IServiceProvider | 解析服务 |
IServiceScope | 表示一个作用域,内部通过 services 暴露当前作用域的 IServiceProvider |
IServiceScopeFactory | 创建作用域 |
ServiceProviderOptions | 控制 validateScopes、validateOnBuild 等校验行为 |
其中最常用的是前 3 个:
- 用
ServiceCollection注册 - 用
build()得到IServiceProvider - 用
IServiceProvider解析服务
服务注册
泛型注册
这是最常见的形式,表示“请求接口时,创建某个实现类型”。
let services = ServiceCollection()
// 注册单例、作用域和瞬时服务。
services.addSingleton<IDbConnection, MySqlConnection>()
services.addScoped<DbContext, DbContext>()
services.addTransient<ReportService, ReportService>()反射注册
除了泛型重载,容器也公开了 TypeInfo 版本的注册 API。 这类 API 适合“类型在运行时才确定”的场景。
import std.reflect.*
import soulsoft_extensions_injection.*
let services = ServiceCollection()
// 直接用 TypeInfo 注册实现类型本身。
services.addSingleton(TypeInfo.of<MySqlConnection>())
// 注册接口到实现类型的映射。
services.addSingleton(TypeInfo.of<IDbConnection>(), TypeInfo.of<MySqlConnection>())
// 注册作用域服务。
services.addScoped(TypeInfo.of<DbContext>())
// 注册瞬时服务。
services.addTransient(TypeInfo.of<MySqlConnection>())如果你需要自己做按包扫描、约定式注册或其他反射式注册逻辑,通常也是以这些 TypeInfo 重载为基础来实现。
实例注册
如果实例已经由你自己创建好了,可以直接把实例放进容器。
let services = ServiceCollection()
// 直接把现成实例放进容器。
services.addSingleton<IDbConnection>(MySqlConnection())这适合:
- 配置对象
- 预热好的客户端
- 明确只想复用某一个现成实例的场景
工厂注册
当创建逻辑比较特殊时,可以使用工厂。
let services = ServiceCollection()
services.addSingleton<IDbConnection, MySqlConnection>()
// 用工厂函数显式控制 DbContext 的创建过程。
services.addSingleton<DbContext> { sp =>
DbContext(sp.getOrThrow<IDbConnection>())
}工厂函数的参数类型是 (IServiceProvider) -> TService,这是源码里真实公开的签名,不是文档约定。
这一方式适合:
- 构造过程需要条件判断
- 需要手动选择依赖
- 想避免纯反射式构造,显式控制对象创建
tryAdd 和 tryAddEnumerable
当你希望“只有没注册过才注册”时,可以使用 tryAdd*。
let services = ServiceCollection()
services.addSingleton<IDbConnection, MySqlConnection>()
// 只有在同 serviceType + key 未注册时才添加。
services.tryAddSingleton<IDbConnection, MsSqlConnection>()上面的第二次注册不会生效。对 tryAdd* 来说,判重条件是:
serviceTypeserviceKey
如果你需要“同一服务类型保留多个实现,但避免重复实现重复注册”,可以使用 tryAddEnumerable。 它的判重条件更严格:
serviceTypeimplementationTypeserviceKey
服务解析
get<T>() 和 getOrThrow<T>()
解析服务最常用的两个方法是:
get<T>():解析失败时返回NonegetOrThrow<T>():解析失败时抛出UnsupportedException
let services = ServiceCollection()
services.addSingleton<IDbConnection, MySqlConnection>()
let root = services.build()
// get 返回 Option,解析失败不会抛异常。
let connection1 = root.get<IDbConnection>()
// getOrThrow 解析失败会直接抛异常。
let connection2 = root.getOrThrow<IDbConnection>()
// 未注册服务时返回 None。
let missing = root.get<DbContext>()使用建议:
- 服务是“可选”的时候,用
get<T>() - 服务是“必须存在”的时候,用
getOrThrow<T>()
getAll<T>()
如果同一个服务类型注册了多个实现,可以通过 getAll<T>() 一次取回全部实现。
let services = ServiceCollection()
services.addSingleton<IDbConnection, MsSqlConnection>()
services.addSingleton<IDbConnection, MySqlConnection>()
let root = services.build()
// 一次性取回该服务类型的全部实现。
let all = root.getAll<IDbConnection>().toArray()
println(all[0] is MsSqlConnection)
println(all[1] is MySqlConnection)这里有两个很重要的真实行为:
getAll<T>()按注册顺序返回getOrThrow<T>()在多实现场景下,返回最后一次注册的那个实现
// 单个解析时会返回最后一次注册的实现。
let current = root.getOrThrow<IDbConnection>()
println(current is MySqlConnection)按 key 解析
如果同一个服务类型需要同时保留多套实现,可以使用 keyed service。
let services = ServiceCollection()
// 同一服务类型用不同 key 保留多套实现。
services.addKeyedSingleton<IDbConnection, MySqlConnection>("mysql")
services.addKeyedSingleton<IDbConnection, MsSqlConnection>("mssql")
let root = services.build()
// 按 key 解析对应的实现。
let mysql = root.getOrThrow<IDbConnection>("mysql")
let mssql = root.getOrThrow<IDbConnection>("mssql")对应的解析方法包括:
get<T>(serviceKey: String)getOrThrow<T>(serviceKey: String)getAll<T>(serviceKey: String)
生命周期
源码中定义了 3 种生命周期:
| 生命周期 | 含义 | 典型行为 |
|---|---|---|
Singleton | 整个根容器只保留一份实例 | 多次解析返回同一对象 |
Scoped | 每个作用域保留一份实例 | 同一作用域相同,不同作用域不同 |
Transient | 每次解析都创建新实例 | 每次解析都不同 |
Singleton
let services = ServiceCollection()
services.addSingleton<IDbConnection, MySqlConnection>()
let root = services.build()
// 同一个根容器里,两次解析拿到同一单例对象。
let a = root.getOrThrow<IDbConnection>()
let b = root.getOrThrow<IDbConnection>()
println(refEq((a as Object).getOrThrow(), (b as Object).getOrThrow()))输出会是 true。
Scoped
下面这个例子能更直观地看到 scoped 的行为。
import soulsoft_extensions_injection.*
interface IDbConnection <: Resource {
func isClosed(): Bool
}
class MySqlConnection <: IDbConnection {
private var _isClosed = false
public func isClosed(): Bool {
_isClosed
}
public func close(): Unit {
// 作用域释放时把连接标记为已关闭。
_isClosed = true
}
}
main(): Int64 {
let services = ServiceCollection()
// 把连接注册为 scoped 服务。
services.addScoped<IDbConnection, MySqlConnection>()
let root = services.build()
var fromScope1: ?IDbConnection = None
var fromScope2: ?IDbConnection = None
try (scope1 = root.createScope()) {
// 同一作用域内两次解析会返回同一实例。
fromScope1 = scope1.services.getOrThrow<IDbConnection>()
let sameScope = scope1.services.getOrThrow<IDbConnection>()
println(refEq(
(fromScope1.getOrThrow() as Object).getOrThrow(),
(sameScope as Object).getOrThrow()
))
}
try (scope2 = root.createScope()) {
// 新作用域会得到新的实例。
fromScope2 = scope2.services.getOrThrow<IDbConnection>()
}
// 比较不同作用域的实例,并观察作用域结束后的释放状态。
println(refEq(
(fromScope1.getOrThrow() as Object).getOrThrow(),
(fromScope2.getOrThrow() as Object).getOrThrow()
))
println(fromScope1.getOrThrow().isClosed())
return 0
}这个例子会体现 3 件事:
- 同一作用域内,多次解析得到同一实例
- 不同作用域内,得到不同实例
- 如果服务实现了
Resource,作用域结束时会自动释放
Transient
Transient 和 Scoped 的区别在于:即使在同一个作用域里,每次解析也会得到新实例。
下面这个例子把 Transient 的几个关键行为都展示出来:
import soulsoft_extensions_injection.*
interface IDbConnection <: Resource {
func isClosed(): Bool
}
class MySqlConnection <: IDbConnection {
private var _isClosed = false
public func isClosed(): Bool {
_isClosed
}
public func close(): Unit {
// 作用域结束时释放瞬时对象。
_isClosed = true
}
}
main(): Int64 {
let services = ServiceCollection()
// 注册瞬时服务,每次解析都创建新实例。
services.addTransient<IDbConnection, MySqlConnection>()
let root = services.build()
var fromScope1First: ?IDbConnection = None
var fromScope1Second: ?IDbConnection = None
var fromScope2: ?IDbConnection = None
try (scope1 = root.createScope()) {
fromScope1First = scope1.services.getOrThrow<IDbConnection>()
fromScope1Second = scope1.services.getOrThrow<IDbConnection>()
// 同一作用域内,两次瞬时解析得到不同对象。
println(refEq(
(fromScope1First.getOrThrow() as Object).getOrThrow(),
(fromScope1Second.getOrThrow() as Object).getOrThrow()
))
}
try (scope2 = root.createScope()) {
fromScope2 = scope2.services.getOrThrow<IDbConnection>()
}
// 比较跨作用域实例,并查看资源释放状态。
println(refEq(
(fromScope1First.getOrThrow() as Object).getOrThrow(),
(fromScope2.getOrThrow() as Object).getOrThrow()
))
println(fromScope1First.getOrThrow().isClosed())
println(fromScope1Second.getOrThrow().isClosed())
return 0
}这个例子会体现 3 件事:
- 同一作用域内,两次解析也会得到不同实例
- 不同作用域之间,拿到的仍然是不同实例
- 如果瞬时服务实现了
Resource,作用域结束后也会被释放
生命周期规则
实际使用时,有两条规则需要记住:
- 单例不要依赖作用域服务
- 开启
validateScopes后,不要从根容器直接解析Scoped服务
这是源码和单测都明确覆盖过的行为。
作用域校验与构建校验
构建容器时可以配置两个开关:
// 开启作用域校验和构建期预检查。
let root = services.build({ options =>
options.validateScopes = true
options.validateOnBuild = true
})它们的默认值都为 false。
validateScopes
开启后,容器会额外检查作用域相关的错误。
最常见的一类错误是:
- 从根容器直接解析
Scoped服务 - 从根容器解析一个依赖了
Scoped服务的对象图
validateOnBuild
开启后,容器会在 build() 阶段提前验证服务图。
但这里有一个容易误解的点:
- 它不是“所有错误都在构建期报出来”
- 按当前实现,它主要在构建期帮助你尽早发现“单例依赖作用域服务”这类问题
例如,下面这个注册在同时开启两个开关时,会在 build() 阶段失败:
let services = ServiceCollection()
services.addScoped<IDbConnection, MySqlConnection>()
services.addSingleton<DbContext, DbContext>()
// 单例依赖 scoped 服务时,构建阶段就会失败。
services.build({ options =>
options.validateScopes = true
options.validateOnBuild = true
})原因是 DbContext 是单例,但它依赖 IDbConnection,而 IDbConnection 被注册成了 Scoped。
构造函数注入
对于普通类型映射注册,容器会根据构造函数参数继续解析依赖。
例如:
interface IDbConnection {}
class MySqlConnection <: IDbConnection {}
class DbContext {
public DbContext(public let connection: IDbConnection) {}
}
let services = ServiceCollection()
// 注册依赖关系后,容器会自动完成构造函数注入。
services.addSingleton<IDbConnection, MySqlConnection>()
services.addSingleton<DbContext, DbContext>()
let root = services.build()
let context = root.getOrThrow<DbContext>()这里 DbContext 并没有自己写工厂函数,但依然能被正常构造,因为容器会继续解析它构造函数里的 IDbConnection。
ActivatorUtilities
有时候你并不想把某个类型注册进容器,但又希望它的依赖仍然从容器中解析。 这时可以使用 ActivatorUtilities。
import soulsoft_extensions_injection.*
interface IDbConnection {}
class MySqlConnection <: IDbConnection {}
class DbContext {
public DbContext(public let connection: IDbConnection) {}
}
class DbDataSource {
public DbDataSource(public let context: DbContext, public let name: String) {}
}
main(): Int64 {
let services = ServiceCollection()
services.addSingleton<IDbConnection, MySqlConnection>()
services.addSingleton<DbContext, DbContext>()
let root = services.build()
// 手动传入 name,其余依赖由容器补齐。
let source = ActivatorUtilities.createInstance<DbDataSource>(root, ["reporting"])
println(source.name)
println(source.context.connection is MySqlConnection)
return 0
}这个例子里:
DbDataSource没有注册到容器"reporting"由调用方显式传入DbContext仍然由容器负责解析
按当前实现,匹配规则是:
- 用户传入的参数优先匹配构造函数参数
- 剩余参数再从容器解析
- 如果找不到合适构造函数,会抛出异常
- 如果构造函数存在歧义,也会抛出异常
内置服务
有几类服务不需要手动注册,容器会自动提供:
IServiceProviderIServiceScopeFactoryIServiceProviderIsServiceIServiceProviderIsKeyedService
例如:
let services = ServiceCollection()
let root = services.build()
// 这些基础服务由容器自动提供。
let scopeFactory = root.getOrThrow<IServiceScopeFactory>()
let checker = root.getOrThrow<IServiceProviderIsService>()
println(checker.isService<IServiceProvider>())其中:
IServiceScopeFactory用来创建作用域IServiceProviderIsService用来判断某个类型是否可解析IServiceProviderIsKeyedService用来判断某个 key 下的类型是否可解析
进阶:keyed service 的构造函数注入
如果你已经在使用 keyed service,可以进一步配合两个注解:
@FromKeyedServices@ServiceKey
显式指定 key
class ExplicitKeyedService {
public let connection: IDbConnection
public ExplicitKeyedService(@FromKeyedServices["mysql"] connection: IDbConnection) {
// 强制从 mysql 这个 key 对应的注册项解析依赖。
this.connection = connection
}
}这表示:无论外层服务怎么注册,这个参数都固定取 "mysql" 对应的实现。
继承当前服务的 key
class InheritKeyedService {
public let connection: IDbConnection
public InheritKeyedService(@FromKeyedServices connection: IDbConnection) {
// 不显式写 key 时,继承当前服务的注册 key。
this.connection = connection
}
}如果 InheritKeyedService 自己是以 "mysql" 注册的,那么这里会自动继承 "mysql"。
把当前 key 注入为字符串
class ServiceKeyReceiver {
public let key: String
public init(@ServiceKey key: String) {
// 把当前服务的 key 直接注入为字符串。
this.key = key
}
}常见注意事项
build() 之后不能再修改 ServiceCollection
ServiceCollection.build() 调用之后,集合会变成只读状态。 如果你继续增删改服务,会抛出异常。
let services = ServiceCollection()
services.addSingleton<IDbConnection, MySqlConnection>()
services.build()
// build() 之后再修改集合会抛异常。
services.addSingleton<DbContext, DbContext>() // 这里会抛异常多实现场景要分清“单个解析”和“全部解析”
- 想拿“当前默认实现”,用
get<T>()或getOrThrow<T>() - 想拿“全部实现”,用
getAll<T>()
这两类 API 的行为不同,不要混用。
validateOnBuild 不是万能预检
如果你开启了 validateOnBuild,确实能更早发现一部分问题; 但不要把它理解成“构建成功就代表运行期一定不会出错”。
尤其是作用域问题,还需要结合 validateScopes 一起理解。
小结
初学时,建议按下面的顺序掌握:
- 先熟悉
ServiceCollection、build()、getOrThrow<T>() - 再理解
Singleton、Scoped、Transient的区别 - 接着掌握工厂注册和
ActivatorUtilities - 最后再看 keyed service、
@FromKeyedServices和@ServiceKey
如果你只记住一句话,可以记这一句:
ServiceCollection负责“描述要注册什么”,IServiceProvider负责“真正把对象解析出来”。