Skip to content

依赖注入

依赖注入(Dependency Injection, DI)是一种重要的软件设计模式,也是控制反转(Inversion of Control, IoC)原则的一种实现方式。它通过将对象的创建和绑定过程外部化,有效管理组件间的依赖关系,提高代码的可测试性和可维护性。

快速启动

只需要以下 5 个步骤即可开始使用依赖注入框架:

cangjie
import spire_extensions_injection.*

class DbContext {
    public init(connection: IDbConnection) {
    }
}

interface IDbConnection {}

class MysqlConnection <: IDbConnection {}

// 1. 创建并配置服务集合
let services = ServiceCollection()

// 2. 注册服务描述
services.addScoped<DbContext, DbContext>()
services.addScoped<IDbConnection, MysqlConnection>()

// 3. 构建服务提供者
let provider = services.build()

// 4. 创建作业域
try (scope = provider.createScope()) {
    // 5. 解析服务
    let context = scope.services.getOrThrow<DbContext>()
}

服务注册

基础注册

适用场景:简单的注册需求

cangjie
// 完整描述符方式
services.add(ServiceDescriptor.singleton<IDbConnection, MysqlConnection>())

// 简化泛型方式(推荐)
services.addSingleton<IDbConnection, MysqlConnection>()

// 类型信息方式
services.addSingleton(TypeInfo.of<IDbConnection>(), TypeInfo.of<MysqlConnection>())

实例注册

适用于已有实例对象的注册:

cangjie
// 构建一个日志工厂
let loggerFactory = LoggingBuilder()
    .addConsole()
    .build()

// 注册到容器
services.addSingleton(loggerFactory)

工厂注册

适用于需要自定义创建逻辑的场景:

cangjie
public class ConfigurationManager {
    public var connectionString = "mysql://localhost:3306"
}

let services = ServiceCollection()

services.addSingleton(ConfigurationManager())

// 使用函数方式创建
services.addSingleton<IDbConnection> { sp =>
    let configuration = sp.getOrThrow<ConfigurationManager>()  // 可以通过sp来解析容器中已有的服务
    return MysqlConnection(configuration.connectionString) 
}

let provider = services.build()

let context = provider.getOrThrow<DbContext>()

性能提示

工厂模式可以避免反射调用,提升性能

ActivatorUtilities 示例:

cangjie
// 我们不会将String注册到容器
class DbContext {
    public init(connection: IDbConnection, loggerFactory: ILoggerFactory, tenantId: String) {

    }
}

let services = ServiceCollection()

services.addSingleton<IDbConnection, MysqlConnection>()
services.addSingleton<ILoggerFactory, LoggerFactory>()

//使用ActivatorUtilities
services.addSingleton<DbContext> { sp =>
    //支持传入自定义参数
    ActivatorUtilities.createInstance<DbContext>(sp, ["t1"]) 
}

let provider = services.build()

let context = provider.getOrThrow<DbContext>()

注意

ActivatorUtilities 会使用反射创建实例,如需完全避免反射应手动解析依赖

服务解析

解析必需服务

确保服务已注册时使用:

cangjie
let services = ServiceCollection()

services.addSingleton<IDbConnection, MysqlConnection>()

let provider = services.build()

// 如果服务不存在将抛出异常
let connection = provider.getOrThrow<IDbConnection>()

解析可选服务

不确定服务是否注册时使用:

cangjie

let services = ServiceCollection()

let provider = services.build()

//如果服务解析失败返回None
let connection = provider.getOrDefault<IDbConnection>() ?? MysqlConnection()

解析多实现服务

适用于策略模式或插件系统:

cangjie
let services = ServiceCollection()

// 注册多个数据源
services.addSingleton<IDataSource, MysqlDataSource>()
services.addSingleton<IDataSource, SqlServerDataSource>()

//构建容器
let provider = services.build()

// 获取所有数据源
let strategies = provider.getAll<IDataSource>()

通过容器创建服务

解析未注册但依赖容器的服务:

cangjie
class DbContext {
    public init(connection: IDbConnection, loggerFactory: ILoggerFactory, tenant: String) {

    }
}
let services = ServiceCollection()

services.addSingleton<IDbConnection, MysqlConnection>()
services.addSingleton<ILoggerFactory, LoggerFactory>()

let provider = services.build()

//假设DbContext不希望注册到容器中
let context = ActivatorUtilities.createInstance<DbContext>(provider, ["t1"])

ActivatorUtilities 在服务注册和服务解析都存在使用需求

生命周期

生命周期作用范围典型应用场景
Singleton整个应用程序生命周期配置服务、日志服务
Scoped单个作用域范围内数据库上下文
Transient每次请求创建新实例轻量级临时服务

重要规则

  1. 单例服务不能依赖非单例服务
  2. 不能从根容器解析非单例服务

作用域示例

cangjie
let services = ServiceCollection()

// 注册为Singleton
services.addSingleton<IDbConnection, MysqlConnection>()

// 注册为Scoped
services.addScoped<IDbConnection, MysqlConnection>()

// 注册为Transient
services.addScoped<IDbConnection, MysqlConnection>()

let provider = services.build()

// 创建作用域
try(scope = provider.createScope()) {
    // 解析服务
    scope.services.getOrThrow<IDbConnection>()
}
// 程序运行到此处,如果IDbConnection注册方式是非单例的,就会被释放

资源释放

如果服务实现了 Resource 接口,在释放时容器会调用实例的 close 方法,来释放 非托管资源

高级功能

防止服务重复注册

重复注册问题:假设 addAuthorizationaddMemoryCache 是第三方开发者开发的,调用方不知道内部的注册逻辑。会导致某些服务重复注册

cangjie
//定义扩展方法1:
extend ServiceCollection {
    public func addMemoryCache() {
        //使用add如果重复执行将导致服务注册多次
        this.addSingleton<IMemoryCache, MemoryCache>()
    }
}

//定义扩展方法1:
extend ServiceCollection {
    public func addAuthorization() {
        this.addMemoryCache()
        this.addSingleton<AuthorizationService>()
    }
}

//会导致IMemoryCache重复注册
let services = ServiceCollection()
services.addMemoryCache()
//*** 重复注册IMemoryCache
services.addAuthorization()

尝试注册服务:如果多个方法内部多次注册了同一个服务,为了避免服务重复注册可以使用 tryAdd

cangjie
//定义扩展方法1:
extend ServiceCollection {
    public func addMemoryCache() {
        this.addSingleton<IMemoryCache, MemoryCache>() 
        this.tryAddSingleton<IMemoryCache, MemoryCache>() 
    }
}

//定义扩展方法1:
extend ServiceCollection {
    public func addAuthorization() {
        this.addMemoryCache()
        this.tryAddSingleton<AuthorizationService>()
    }
}

let services = ServiceCollection()
//抢先注册:替换掉IMemoryCache的实现
services.addSingleton<IMemoryCache, MyMemoryCache>()
services.addMemoryCache()
services.addAuthorization()

注册策略

  1. 如果一个服务使用 tryAdd 的方式注册的,表示该服务只允许注册一次
  2. 如果已知某个服务使用 tryAdd 的方式注册,那么可以通过在它注册之前抢先注册来替换掉默认实现

容器构建选项

cangjie
let provider = services.build { options =>
    options.validateScopes = true // 可以确保没有作用域服务被错误地从根提供程序解析
    options.validateLoopDependency = true  // 启用循环依赖检测
}

最佳实践

性能优化

  • 优先使用工厂注册替代反射
  • 高频创建的服务考虑使用 Transient 生命周期

生命周期选择

  • Singleton: 无状态全局服务
  • Scoped: 请求相关有状态服务
  • Transient: 轻量级临时服务

线程安全

  • ServiceCollection 构建前非线程安全
  • IServiceProvider 完全线程安全

开发建议

  • 开发环境启用所有验证选项
  • 生产环境关闭非必要检查提升性能