依赖注入
依赖注入(Dependency Injection, DI)是一种重要的软件设计模式,也是控制反转(Inversion of Control, IoC)原则的一种实现方式。它通过将对象的创建和绑定过程外部化,有效管理组件间的依赖关系,提高代码的可测试性和可维护性。
快速启动
容器的基本使用步骤如下:
- 定义服务和依赖关系
- 创建服务描述集合,并注册服务
- 构建容器
- 解析服务
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 | 所有服务注册的集合 |
| 点餐过程 | 服务解析 | 请求获取服务实例 |
| 制作菜品 | 实例创建 | 容器根据描述创建服务 |
| 成品菜肴 | 服务实例 | 实际可用的对象 |
| 保质期限 | 生命周期 | 服务实例的存活策略 |
这个类比帮助初学者理解:依赖注入框架就像一个智能的餐厅系统,我们只需要"点菜"(请求服务),系统会自动处理所有的"备料"和"烹饪"过程(依赖解析和实例创建),最终将"成品菜肴"(服务实例)呈现给我们使用。
服务注册
基础注册
适用场景:简单的注册需求
// 完整描述符方式
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,它是容器协调器,支持提供未注册的服务一起完成服务解析。
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) {
}
}服务解析
依赖注入框架提供多种服务解析方式,并使用不同的内部机制来创建和管理服务实例。
解析必需服务
确保服务已注册时使用:
let services = ServiceCollection()
services.addSingleton<IDbConnection, MysqlConnection>()
let provider = services.build()
// 如果服务不存在,将抛出异常
let connection = provider.getOrThrow<IDbConnection>()解析可选服务
不确定服务是否注册时使用:
let services = ServiceCollection()
let provider = services.build()
// 如果服务解析失败返回None
let connection = provider.get<IDbConnection>()解析多实现服务
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>()解析构造器依赖
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>()协调器
解析未注册但依赖容器的服务:
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实例是不受容器托管的。即失去了生命周期管理。
生命周期
对于基于构造器解析的服务满足如下约束
- 单例服务不能依赖非单例服务
- 不能从根容器解析非单例服务
| 生命周期 | 作用范围 | 典型应用场景 |
|---|---|---|
| Singleton | 整个应用程序生命周期 | 配置服务、日志服务 |
| Scoped | 单个作用域范围内 | 数据库上下文 |
| Transient | 每次请求创建新实例 | 轻量级临时服务 |
作用域示例
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()
}===============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方法。
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
容器可以解析当前作用域的容器实例本身即自我解析。它的使用场景是服务内部需要在某个条件成立时才解析某个服务。
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() |> printlnIServiceScopeFactory
作用域工厂,用于创建作用域,使用场景很少
let services = ServiceCollection()
let provider = services.build()
let factory = provider.getOrThrow<IServiceScopeFactory>()
// 创建作用域
try(scope = factory.createScope()) {
}IServiceProviderIsService
该服务用于判断否个服务是否注册,使用场景很少
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高级功能
尝试注册
let services = ServiceCollection()
services.tryAddSingleton<IDbConnection, SqlConnection>()
services.tryAddSingleton<IDbConnection, SqlConnection>() // 如果某个ServiceType已注册那么将不再注册组件扫描
服务注册在实际开发过程中是非常繁琐的,此时我们可以使用组件扫描的方式来注册服务。我们并没有对组件扫描进行封装。开发者可以自行发挥。
比如某个package下面的服务一般都具有共同的特征,比如Services都应该注册为瞬时的。
如果需要进行更加灵活的控制,我们可以定义一个注解,扫描时根据注解信息来进行注册。
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 {
}宏注册
我们可以定义一个宏来生成工厂函数,简化工厂函数的编码,同时利用工厂函数能规避反射,进而获得巨幅提升性能。
定义注入宏
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
}注册并解析
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")
}
}