Skip to content

依赖注入

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

快速启动

容器的基本使用步骤如下:

  1. 定义服务和依赖关系
  2. 创建服务描述集合,并注册服务
  3. 构建容器
  4. 解析服务
cangjie
import spire_extensions_injection.*

main(): Int64 {
    // 1. 定义服务描述集合
    let services = ServiceCollection()
    // 2. 注册服务
    services.addSingleton<DbContext, DbContext>()
    services.addSingleton<IDbConnection, MySqlConnection>()
    // 3. 构建容器
    let provider = services.build()
    // 4. 解析服务
    let connection = provider.getOrThrow<DbContext>()
    return 0
}

public interface IDbConnection  { }

public class MySqlConnection <: IDbConnection{ }

public class DbContext {
    // 由容器注入
    public DbContext(let connection: IDbConnection) {
    }
}

提示

只需要上面的简单步骤即可创建并运行容器,容器会根据依赖关系生成图纸,并基于图纸进行绘制。

基础概念

我们通过一个场景来简单解释一下依赖注入的工作流程和术语。

场景导入

想象一下,我们走进一家餐厅用餐的完整过程,这个场景恰好可以帮助理解依赖注入的核心概念:

点餐流程

📝 点餐开始

  • 顾客需求:我们需要点喜欢的菜品
  • 服务请求:向服务员索要菜单
  • 对应概念:这就是服务解析的过程

🍽️ 菜单系统

  • 菜名:每道菜的名称标识
  • 对应概念服务类型 (ServiceType) - 用于告诉容器我们需要哪个服务
  • 菜品描述:菜单上的配图和文字说明
  • 对应概念服务描述 (ServiceDescriptor) - 描述如何创建和配置服务

📋 菜单本身

  • 完整菜单:包含所有可选菜品的列表
  • 对应概念服务描述集合 (ServiceCollection) - 实现了 List<ServiceDescriptor> 接口,存储所有服务注册信息

👨‍🍳 后厨制作

  • 厨师工作:根据点单制作实际菜品
  • 对应概念服务解析(IServiceProvider) - 容器根据注册信息创建服务实例
  • 食材依赖:制作"番茄炒鸡蛋"需要番茄和鸡蛋
  • 对应概念依赖关系 - 服务可能依赖其他服务才能正常工作

🎯 成品上桌

  • 制作完成的菜品:从概念变成实物的菜肴
  • 对应概念服务实例 - 容器解析出来的具体对象

⏰ 菜品特性

  • 保质期限:凉菜、热菜、咸菜的保存时间各不相同
  • 对应概念生命周期 (Lifetime) - 服务实例的存活时间策略

概念总结

餐厅场景依赖注入概念说明
菜名ServiceType标识所需服务的类型
菜品描述ServiceDescriptor描述如何创建和配置服务
菜单列表ServiceCollection所有服务注册的集合
点餐过程服务解析请求获取服务实例
制作菜品实例创建容器根据描述创建服务
成品菜肴服务实例实际可用的对象
保质期限生命周期服务实例的存活策略

这个类比帮助初学者理解:依赖注入框架就像一个智能的餐厅系统,我们只需要"点菜"(请求服务),系统会自动处理所有的"备料"和"烹饪"过程(依赖解析和实例创建),最终将"成品菜肴"(服务实例)呈现给我们使用。

服务注册

基础注册

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

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

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

// 类型信息方式(适合动态注册)
services.addSingleton(TypeInfo.of<IDbConnection>(), TypeInfo.of<MySqlConnection>())

// 现有实例
services.addSingleton<IDbConnection>(MySqlConnection())

// 工厂模式(推荐性能最佳,无需反射参与)
services.addSingleton<DbContext>{sp => 
    DbContext(sp.getOrThrow<IDbConnection>())
}

协调器

适用场景:复杂的注册需求

假设我们有一个服务A,它依赖了B,C,D,E服务,同时依赖了一个字符串,已知容器已经注册了B,C,D,E服务,但是没有注入字符串,请问如何注册服务A?

答:我们可以使用ActivatorUtilities,它是容器协调器,支持提供未注册的服务一起完成服务解析。

cangjie
services.addSingleton(B())
services.addSingleton(C())
services.addSingleton(D())
services.addSingleton(E())

services.addSingleton<A>{sp => 
    // 容器中未注册的服务,通过第二个参数提供
    return ActivatorUtilities.CreateInstance<A>(sp, "spire")
}

public class A {
    public A(name: String, a: B, c: C, d: D, e: E) {

    }
}

服务解析

依赖注入框架提供多种服务解析方式,并使用不同的内部机制来创建和管理服务实例。

解析必需服务

确保服务已注册时使用:

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.get<IDbConnection>()

解析多实现服务

cangjie
interface IDbConnection {}
class MysqlConnection <: IDbConnection {}
class SqlConnection <: IDbConnection {}

let services = ServiceCollection()

// 注册多个数据库链接实现
services.addSingleton<IDbConnection, SqlConnection>()
services.addSingleton<IDbConnection, MysqlConnection>()

let provider = services.build()

// 获取所有注册的链接实现
let connections = provider.getAll<IDbConnection>()

解析构造器依赖

cangjie
interface IDbConnection {}
class MysqlConnection <: IDbConnection {}
class SqlConnection <: IDbConnection {}

class DbContext {
    public DbContext(let connections: Collection<IDbConnection>) {

    }
}

let services = ServiceCollection()

// 注册多个数据库链接实现
services.addSingleton<IDbConnection, SqlConnection>()
services.addSingleton<IDbConnection, MysqlConnection>()
services.addSingleton<DbContext, DbContext>()
let provider = services.build()

// 获取所有注册的链接实现
let context = provider.getOrThrow<DbContext>()

协调器

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

cangjie
interface IDbConnection {}
class MysqlConnection <: IDbConnection {}
class SqlConnection <: IDbConnection {}

class DbContext {
    public DbContext(tenant: String, let connections: Collection<IDbConnection>) {

    }
}

let services = ServiceCollection()

// 注册多个数据库链接实现
services.addSingleton<IDbConnection, SqlConnection>()
services.addSingleton<IDbConnection, MysqlConnection>()
// services.addSingleton<DbContext, DbContext>() 不注册
let provider = services.build()

// 获取所有注册的链接实现,并可以提供额外参数
let context = ActivatorUtilities.createInstance<DbContext>(provider, "spire")

注意:由于DbContext并未注册到容器,而是通过ActivatorUtilities创建的,DbContext实例是不受容器托管的。即失去了生命周期管理。

生命周期

对于基于构造器解析的服务满足如下约束

  1. 单例服务不能依赖非单例服务
  2. 不能从根容器解析非单例服务
生命周期作用范围典型应用场景
Singleton整个应用程序生命周期配置服务、日志服务
Scoped单个作用域范围内数据库上下文
Transient每次请求创建新实例轻量级临时服务

作用域示例

cangjie
import std.random.*
import spire_extensions_injection.*

main(): Int64 {
    let services = ServiceCollection()
    services.addSingleton<Singleton, Singleton>()
    services.addScoped<Scoped, Scoped>()
    services.addTransient<Transient, Transient>()
    let provider = services.build()
    // 创建作用域1
    println("===============Scope2====================")
    try (scope1 = provider.createScope()) {
        let singleton = scope1.services.getOrThrow<Singleton>()
        let scoped = scope1.services.getOrThrow<Scoped>()
        let transient = scope1.services.getOrThrow<Transient>()
        println(scope1.services.getOrThrow<Singleton>().id)
        println(scope1.services.getOrThrow<Singleton>().id)
        println(scope1.services.getOrThrow<Scoped>().id)
        println(scope1.services.getOrThrow<Scoped>().id)
        println(scope1.services.getOrThrow<Transient>().id)
        println(scope1.services.getOrThrow<Transient>().id)
    }
    println("===============Scope2====================")
    // 创建作用域2
    try (scope2 = provider.createScope()) {
        let singleton = scope2.services.getOrThrow<Singleton>()
        let scoped = scope2.services.getOrThrow<Scoped>()
        let transient = scope2.services.getOrThrow<Transient>()
        println(scope2.services.getOrThrow<Singleton>().id)
        println(scope2.services.getOrThrow<Singleton>().id)
        println(scope2.services.getOrThrow<Scoped>().id)
        println(scope2.services.getOrThrow<Scoped>().id)
        println(scope2.services.getOrThrow<Transient>().id)
        println(scope2.services.getOrThrow<Transient>().id)
    }
    return 0
}

public class Singleton {
    public var id: String = "Singleton:" + Random().nextInt64().toString()
}

public class Scoped {
    public var id: String = "Scoped:" + Random().nextInt64().toString()
}

public class Transient {
    public var id: String = "Transient:" +  Random().nextInt64().toString()
}
bash
===============Scope2====================
Singleton:-6446799371252424971
Singleton:-6446799371252424971
Scoped:-8239741083227323237
Scoped:-8239741083227323237
Transient:-455999103032063464
Transient:-3732090699255222433
===============Scope2====================
Singleton:-6446799371252424971
Singleton:-6446799371252424971
Scoped:2953664531395606013
Scoped:2953664531395606013
Transient:3501588865321937698
Transient:7833323014559001106

资源释放

运行下面的示例,可以发现当作用域结束的时候,容器会自动释放服务,如果实现了Resource那么会调用它的close方法。

cangjie
main(): Int64 {
    let services = ServiceCollection()
    services.addScoped<IDbConnection, SqlConnection>()
    let provider = services.build()
    // 创建作用域
    try (scope = provider.createScope()){
        scope.services.getOrThrow<IDbConnection>()
    }
    // 作用域结束
    return 0
}

interface IDbConnection {}

class MysqlConnection <: IDbConnection {}

class SqlConnection <: IDbConnection & Resource {
    private var _isClosed = false
    public func close() {
        println("链接已关闭")
    }
    public func isClosed() {
        _isClosed
    }
}

内置服务

依赖注入框架自动提供三个内置服务,这些服务无需手动注册。

IServiceProvider

容器可以解析当前作用域的容器实例本身即自我解析。它的使用场景是服务内部需要在某个条件成立时才解析某个服务。

cangjie
public class SomeService {
    public SomeService(let provider: IServiceProvider) {

    }

    public func getConnection(): ?IDbConnection {
        // 可以由服务内部来决策是否解析IDbConnection
        if(DateTime.now().year % 2 == 0) {
            provider.getOrThrow<IDbConnection>()
        }
        return None
    }
}
// 无需注册IServiceProvider
let services = ServiceCollection()
services.addSingleton<IDbConnection, SqlConnection>()
services.addSingleton<SomeService, SomeService>()
let provider = services.build()
let service = provider.getOrThrow<SomeService>()
service.getConnection().isSome() |> println

IServiceScopeFactory

作用域工厂,用于创建作用域,使用场景很少

cangjie
let services = ServiceCollection()
let provider = services.build()
let factory = provider.getOrThrow<IServiceScopeFactory>()
// 创建作用域
try(scope = factory.createScope()) {

}

IServiceProviderIsService

该服务用于判断否个服务是否注册,使用场景很少

cangjie
let services = ServiceCollection()
services.addSingleton<IDbConnection, SqlConnection>()
let provider = services.build()
let callSiteFactory = provider.getOrThrow<IServiceProviderIsService>()
callSiteFactory.isService<IDbConnection>() |> println // true
callSiteFactory.isService<DbContext>() |> println // false

高级功能

尝试注册

cangjie
let services = ServiceCollection()
services.tryAddSingleton<IDbConnection, SqlConnection>()
services.tryAddSingleton<IDbConnection, SqlConnection>() // 如果某个ServiceType已注册那么将不再注册

组件扫描

服务注册在实际开发过程中是非常繁琐的,此时我们可以使用组件扫描的方式来注册服务。我们并没有对组件扫描进行封装。开发者可以自行发挥。

比如某个package下面的服务一般都具有共同的特征,比如Services都应该注册为瞬时的。

如果需要进行更加灵活的控制,我们可以定义一个注解,扫描时根据注解信息来进行注册。

cangjie
import std.reflect.*
import demo.services.*
import spire_extensions_injection.*

main(): Int64 {
    let services = ServiceCollection()
    services.addApplicationServices(PackageInfo.get("demo.services")) 
    let provider = services.build()
    let aService = provider.getOrThrow<AService>()
    let bService = provider.getOrThrow<BService>()
    let cService = provider.getOrThrow<CService>()
    return 0
}

extend ServiceCollection {
    // 定义一个扩展方法,来控制注册方式和扫描策略
    // 我们可以定义注解,来实现类似spring中的方式(不推荐)
    public func addApplicationServices(packageInfo: PackageInfo) {
        for (pattern in packageInfo.typeInfos) {
            this.addSingleton(pattern, pattern)
        }
    }
}

package demo.services

public class AService {
    
}

public class BService {
    
}

public class CService {
    
}

宏注册

我们可以定义一个宏来生成工厂函数,简化工厂函数的编码,同时利用工厂函数能规避反射,进而获得巨幅提升性能。

定义注入宏

cangjie
macro package demo.macros

import std.ast.*
import std.sort.*
import std.collection.*

public macro Inject(input: Tokens): Tokens {
    if (let classDecl: ClassDecl <- parseDecl(input)) {
        let serviceTypes = getServiceTypes(classDecl)
        let funcDecl = defineFuncDecl(classDecl.identifier, serviceTypes)
        classDecl.body.decls.add(funcDecl)
        return classDecl.toTokens()
    }
    return input
}

private func getServiceTypes(classDecl: ClassDecl): Collection<FuncParam> {
    let constructors = classDecl.body.decls |> filterMap {f => f as FuncDecl} |>
        filter {f => f.keyword.kind == TokenKind.INIT} |> collectArray
    if (constructors.size == 0) {
        return []
    }
    sort(constructors, key: {x => x.funcParams.size}, descending: true)
    return constructors[0].funcParams
}

private func defineFuncDecl(identifier: Token, funcParams: Collection<FuncParam>) {
    let funcDelc = FuncDecl()
    funcDelc.keyword = Token(TokenKind.FUNC)
    funcDelc.block.lBrace = Token(TokenKind.LCURL)
    funcDelc.block.rBrace = Token(TokenKind.RCURL)
    funcDelc.modifiers.add(Modifier(Token(TokenKind.PUBLIC)))
    funcDelc.lParen = Token(TokenKind.LPAREN)
    funcDelc.rParen = Token(TokenKind.RPAREN)
    if (funcParams.size == 0) {
        funcDelc.funcParams.add(FuncParam(quote(
            _: IServiceProvider
        )))
    } else {
        funcDelc.funcParams.add(FuncParam(quote(
            sp: IServiceProvider
        )))
    }
    funcDelc.identifier = Token(TokenKind.IDENTIFIER, "createInstance")
    let params = Tokens()

    for ((index, pattern) in funcParams |> enumerate) {
        params.append(quote(
            sp.getOrThrow<$(pattern.paramType)>()
        ))
        if (index + 1 < funcParams.size) {
            params.append(quote(,))
        }
    }
    let tokens = quote(
        $(identifier)($(params))
    )
    funcDelc.block.nodes.add(CallExpr(tokens))
    return funcDelc
}

注册并解析

cangjie
main(): Int64 {
    let services = ServiceCollection()
    services.addSingleton(Logger.createInstance)
    services.addSingleton(HelloService.createInstance)
    let provider = services.build()
    let helloService = provider.getOrThrow<HelloService>()
    helloService.sayHello()
    return 0
}


@Inject
public class Logger {
    public func log(msg: String) {
        msg |> println
    }
}

@Inject
public class HelloService {
    private let _logger: Logger

    public init(logger: Logger) {
        _logger = logger
    }

    public func sayHello() {
        _logger.log("hello")
    }
}