生命周期守卫
InferDI 提供三种生命周期:
| 种类 | 创建时机 | 缓存于 | 由容器释放 |
|---|---|---|---|
singleton | 每个拥有它的容器创建一次 | 拥有它的容器 | 是 |
scoped | 每个子作用域创建一次 | 子作用域 | 是 |
transient | 每次解析都创建 | 从不缓存 | 否 |
在 strict: true 模式下,从根容器解析 scoped 键会抛出 Scoped "key" cannot be resolved from the root container. Use createScope().。请从 createScope() 返回的子容器解析 scoped 服务。
生命周期规则
单例不能直接依赖 scoped 或 transient 服务。单例只创建一次并在每个请求间共享,因此如果它捕获了一个作用域级的值——当前请求的上下文、用户或事务——那么这一个请求的状态就会悄无声息地泄漏到所有其他请求中。InferDI 让这种边界情况在类型系统中无法被表达出来,而不是把它留给代码评审。
new Container()
.registerClass('request', RequestContext, [], 'scoped')
.registerClass('users', UserService, ['request'], 'singleton')该注册会被 TypeScript 拒绝。在严格模式下,如果有类型转换绕过了类型系统,同样的形态在运行时也会被拒绝。
严格模式
strict: true 是默认值。它能捕捉:
- 直接从根容器解析 scoped 键
- 由类型转换引入的单例到作用域级或单例到瞬态的违规
- 捕获外层容器的工厂泄漏
- 同步单例循环
- 同步瞬态循环
- 绕过静态检查的动态键误用
const root = new Container({ strict: true })快速模式
仅在测试已证明依赖图形态之后才使用 strict: false:
const root = new Container({ strict: false })快速模式会从解析路径中移除运行时的循环与生命周期记账。scope 会直接读取不可变的根注册表,不再遍历父链,并把委托解析的 singleton 镜像到 scope 缓存中。strict scope 在每次本地未命中时遍历精确的父链,因此无需保留查找元数据也能立即看到依赖树变更。owned 实例的身份去重在两种模式下都于 disposal 阶段执行。它不会改变类型层面的契约,但也无法防御不诚实的类型转换、捕获的外层容器或循环。它也会跳过根容器的 scoped 保护,因此应用代码不得从根容器解析 scoped 键。
推荐的工作流程:在严格模式下开发和测试,通过单一线性 fluent 链对每个运行时键只注册一次,并在首次解析或创建 scope 前完成注册。然后只为经过审计且不可变的生产依赖图切换模式,并先释放子 scope,再释放其祖先。
