Rain 容器模型
Rain 的 DI 上下文不是简单的 Map<Class<*>, Any>。它围绕类型建立上下文,再由类型上下文负责创建、注入、保存和绑定实例。
先手写一个最小容器
在看 Rain 源码前,可以先想象一个最简单的容器:
class TinyContainer {
private val objects = mutableMapOf<Class<*>, Any>()
fun <T : Any> put(type: Class<T>, value: T) {
objects[type] = value
}
fun <T : Any> get(type: Class<T>): T? {
return objects[type] as? T
}
}它能保存和查找对象,但很快就会遇到问题:
- 对象不存在时,能否自动创建?
- 创建时需要的构造器参数从哪里来?
- 接口应该返回哪个实现?
- 同一类型有两个实例怎么办?
- Kotlin
object是否应该重复创建? - 已有对象怎样执行字段注入?
List<Processor>如何收集全部实现?
Rain 的容器模型,就是对这些问题逐层回答后的结果。
思考:为什么不能只保存对象?
因为容器不仅要记住“已经有什么”,还要知道“没有时怎样创建”“创建后怎样注入”“接口如何找到实现”。所以 Rain 保存的不只是对象,还保存每个类型的处理规则。
DiContext
└─ Class<?> -> ClassContext<?>
├─ BeanCreator
├─ BeanInjector
├─ 默认实例
├─ 命名实例
└─ 接口到实现的绑定什么是 Bean
在广义上,Bean 是交给容器管理或由容器提供的对象。不同框架对“哪些类属于 Bean”有不同规则。
Rain 当前判断非常直接:只要一个类不是接口,也不是抽象类,它就是可实例化的 Bean 类型。
AppLoader 扫描配置包中的类。扫描到具体类后,容器可以为它建立 InstanceAbleClassContext:
class UserRepository
class UserService(
private val repository: UserRepository,
)这些类通常不需要额外标记组件注解。前提是它们位于 rain.scanPackages 中,并且构造条件可以满足。
这意味着 Rain 的扫描范围不只是性能配置,也是容器信任边界。扫描包过大时,一些原本只想作为普通数据类型的具体类也具备被容器创建的可能。
DiContext
DiContext 提供容器的基本操作:
val service = context.getBean(UserService::class.java)
context.putBean(Cache::class.java, "local", localCache)
val fresh = context.newBean(TaskHandler::class.java)
context.injectBean(existingObject)getBean获取默认或命名实例;putBean放入外部创建的实例;newBean强制创建新的对象;injectBean向已有对象执行属性注入;registerClass注册类型上下文。
普通业务代码应尽量接收依赖本身。主动访问 DiContext 主要适用于框架边界、动态工厂和无法在编译时确定类型的基础设施。
ClassContext
每个类型对应一个 ClassContext,描述:
- 类型能否直接实例化;
- 如何使用
BeanCreator创建对象; - 如何使用
BeanInjector注入对象; - 默认和命名实例保存在哪里;
- 接口或父类型绑定到哪些实现上下文;
- 是否通过
BeanFactory提供对象。
Rain 会为不同类型选择不同上下文,例如普通可实例化类、不可直接实例化的接口、外部本地实例、Kotlin object、伴生对象和工厂提供的类型。
BeanCreator 与 BeanInjector
创建和注入在 Rain 中是两个步骤。
BeanCreator 负责选择构造器并准备参数。对于 Kotlin 类,Rain 默认尝试使用公开主构造器,因此普通主构造器注入既不需要 @Inject,也不需要显式写出 constructor 关键字。可空类型、默认参数和显式标记 @Inject 的其他构造器也会参与创建规则。
BeanInjector 负责对象创建后的字段与属性注入,也可以被 injectBean() 用于已有对象。
这种拆分让 Rain 可以同时处理:
- 构造器注入;
- 字段与 Setter 注入;
- Kotlin
object的既有实例; - 应用外部创建并放入容器的实例。
默认实例与新实例
不指定名称时,Rain 使用空字符串作为默认实例名:
val service = context.getBean(UserService::class.java)普通类型第一次获取默认实例时会按需创建并注入,之后复用保存的实例。这个体验接近单例 Bean,但它只是相对于当前 Rain 容器的单例。
val another = context.newBean(UserService::class.java)newBean() 仍可以创建不同对象;创建另一个 DiContext 也会产生独立实例集合。因此不要把容器单例理解成 JVM 全局单例。
命名实例
同一类型需要多个对象时,可以使用名称区分:
@Inject
@Named("audit")
lateinit var logger: Logger也可以主动注册:
context.putBean(Cache::class.java, "local", localCache)
context.putBean(Cache::class.java, "remote", remoteCache)Rain 还允许名称写成 {some.config.key},先从配置中解析真实名称。这让部署配置可以参与实现选择,但也增加了动态性;排查问题时需要同时检查配置值和容器绑定。
接口自动绑定
接口标记 @AutoBind 后,AppLoader 会寻找扫描范围内的具体实现,并按实现类的 @Named 名称建立绑定:
@AutoBind
interface PaymentGateway
@Named("stripe")
class StripeGateway : PaymentGatewayRain 需要接口显式允许自动绑定。实现类是否可用,则由扫描范围、是否为具体类以及构造条件共同决定。
集合注入
DataReaderFactory 支持按泛型类型读取 List<T> 和 Map<String, T>:
class ProcessorChain(
private val processors: List<Processor>,
private val namedProcessors: Map<String, Processor>,
)它适合策略集合、处理链和插件列表。集合顺序取决于容器收集到的实例和绑定结构;没有显式排序协议时,不应依赖偶然顺序。
BeanFactory
Rain 的 BeanFactory<T> 表达“由某个工厂按名称创建目标类型”:
interface BeanFactory<T> {
val type: Class<T>
fun isMulti(): Boolean
fun createBean(name: String): T?
}它适合目标对象无法由普通构造器创建,或创建过程需要外部资源的场景。不要仅凭名称把它等同于 Spring 的 BeanFactory;Spring 的 BeanFactory 是整个容器体系的基础接口,职责范围远大于 Rain 的同名接口。
对象图
容器真正维护的是一张对象依赖图:
UserController
└─ UserService
├─ UserRepository
│ └─ Database
└─ EventBus
└─ EventListenerInfo...创建顶层对象时,容器需要递归找到创建方式、解析依赖、选择绑定、创建下一层对象、执行属性注入并保存可复用实例。
局部的一行注入声明可能触发很长的运行时创建链。这带来组合能力,也使循环依赖、缺失 Bean 和错误配置更可能在启动或首次访问时暴露。
跟着一次 getBean 走
假设应用请求:
context.getBean(UserService::class.java)可以把容器内部过程想象成:
- 在
contextMap中寻找UserService的ClassContext; - 如果还没有,根据类型特征创建合适的上下文;
- 检查默认实例是否已经存在;
- 如果不存在,由
BeanCreator选择构造器; - 读取构造器参数类型,例如
UserRepository; - 递归获取或创建
UserRepository; - 调用构造器得到
UserService; - 由
BeanInjector处理额外字段和属性; - 保存为默认实例;
- 返回对象。
这说明 getBean() 并不只是查 Map。它可能触发整张对象图的创建。
观察 Rain 的不同上下文
尝试解释为什么这些类型不能用完全相同的创建规则:
| 类型 | 特点 | Rain 的处理方向 |
|---|---|---|
| 普通具体类 | 可以调用构造器 | 创建并保存实例 |
| 接口或抽象类 | 不能直接构造 | 保存到具体实现的绑定 |
Kotlin object | 语言已经提供唯一实例 | 获取现有对象并注入 |
| 外部放入的对象 | 已经创建完成 | 直接作为本地实例使用 |
| 工厂提供的类型 | 创建过程不由普通构造器决定 | 委托 BeanFactory |
ClassContext 的意义就在这里:不同种类的类型共享同一访问接口,但拥有不同的创建和保存策略。
新人自测
- 为什么容器需要保存“类型规则”,而不只保存对象?
getBean()和newBean()为什么不应该完全相同?- 接口无法构造,Rain 如何让它返回具体对象?
- 为什么容器单例不等于 JVM 全局单例?
List<Processor>注入说明容器掌握了哪些额外信息?