Skip to content

Rain 容器模型

Rain 的 DI 上下文不是简单的 Map<Class<*>, Any>。它围绕类型建立上下文,再由类型上下文负责创建、注入、保存和绑定实例。

先手写一个最小容器

在看 Rain 源码前,可以先想象一个最简单的容器:

kotlin
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 保存的不只是对象,还保存每个类型的处理规则。

text
DiContext
└─ Class<?> -> ClassContext<?>
                  ├─ BeanCreator
                  ├─ BeanInjector
                  ├─ 默认实例
                  ├─ 命名实例
                  └─ 接口到实现的绑定

什么是 Bean

在广义上,Bean 是交给容器管理或由容器提供的对象。不同框架对“哪些类属于 Bean”有不同规则。

Rain 当前判断非常直接:只要一个类不是接口,也不是抽象类,它就是可实例化的 Bean 类型。

AppLoader 扫描配置包中的类。扫描到具体类后,容器可以为它建立 InstanceAbleClassContext

kotlin
class UserRepository

class UserService(
    private val repository: UserRepository,
)

这些类通常不需要额外标记组件注解。前提是它们位于 rain.scanPackages 中,并且构造条件可以满足。

这意味着 Rain 的扫描范围不只是性能配置,也是容器信任边界。扫描包过大时,一些原本只想作为普通数据类型的具体类也具备被容器创建的可能。

DiContext

DiContext 提供容器的基本操作:

kotlin
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 使用空字符串作为默认实例名:

kotlin
val service = context.getBean(UserService::class.java)

普通类型第一次获取默认实例时会按需创建并注入,之后复用保存的实例。这个体验接近单例 Bean,但它只是相对于当前 Rain 容器的单例

kotlin
val another = context.newBean(UserService::class.java)

newBean() 仍可以创建不同对象;创建另一个 DiContext 也会产生独立实例集合。因此不要把容器单例理解成 JVM 全局单例。

命名实例

同一类型需要多个对象时,可以使用名称区分:

kotlin
@Inject
@Named("audit")
lateinit var logger: Logger

也可以主动注册:

kotlin
context.putBean(Cache::class.java, "local", localCache)
context.putBean(Cache::class.java, "remote", remoteCache)

Rain 还允许名称写成 {some.config.key},先从配置中解析真实名称。这让部署配置可以参与实现选择,但也增加了动态性;排查问题时需要同时检查配置值和容器绑定。

接口自动绑定

接口标记 @AutoBind 后,AppLoader 会寻找扫描范围内的具体实现,并按实现类的 @Named 名称建立绑定:

kotlin
@AutoBind
interface PaymentGateway

@Named("stripe")
class StripeGateway : PaymentGateway

Rain 需要接口显式允许自动绑定。实现类是否可用,则由扫描范围、是否为具体类以及构造条件共同决定。

集合注入

DataReaderFactory 支持按泛型类型读取 List<T>Map<String, T>

kotlin
class ProcessorChain(
    private val processors: List<Processor>,
    private val namedProcessors: Map<String, Processor>,
)

它适合策略集合、处理链和插件列表。集合顺序取决于容器收集到的实例和绑定结构;没有显式排序协议时,不应依赖偶然顺序。

BeanFactory

Rain 的 BeanFactory<T> 表达“由某个工厂按名称创建目标类型”:

kotlin
interface BeanFactory<T> {
    val type: Class<T>
    fun isMulti(): Boolean
    fun createBean(name: String): T?
}

它适合目标对象无法由普通构造器创建,或创建过程需要外部资源的场景。不要仅凭名称把它等同于 Spring 的 BeanFactory;Spring 的 BeanFactory 是整个容器体系的基础接口,职责范围远大于 Rain 的同名接口。

对象图

容器真正维护的是一张对象依赖图:

text
UserController
└─ UserService
   ├─ UserRepository
   │  └─ Database
   └─ EventBus
      └─ EventListenerInfo...

创建顶层对象时,容器需要递归找到创建方式、解析依赖、选择绑定、创建下一层对象、执行属性注入并保存可复用实例。

局部的一行注入声明可能触发很长的运行时创建链。这带来组合能力,也使循环依赖、缺失 Bean 和错误配置更可能在启动或首次访问时暴露。

跟着一次 getBean 走

假设应用请求:

kotlin
context.getBean(UserService::class.java)

可以把容器内部过程想象成:

  1. contextMap 中寻找 UserServiceClassContext
  2. 如果还没有,根据类型特征创建合适的上下文;
  3. 检查默认实例是否已经存在;
  4. 如果不存在,由 BeanCreator 选择构造器;
  5. 读取构造器参数类型,例如 UserRepository
  6. 递归获取或创建 UserRepository
  7. 调用构造器得到 UserService
  8. BeanInjector 处理额外字段和属性;
  9. 保存为默认实例;
  10. 返回对象。

这说明 getBean() 并不只是查 Map。它可能触发整张对象图的创建。

观察 Rain 的不同上下文

尝试解释为什么这些类型不能用完全相同的创建规则:

类型特点Rain 的处理方向
普通具体类可以调用构造器创建并保存实例
接口或抽象类不能直接构造保存到具体实现的绑定
Kotlin object语言已经提供唯一实例获取现有对象并注入
外部放入的对象已经创建完成直接作为本地实例使用
工厂提供的类型创建过程不由普通构造器决定委托 BeanFactory

ClassContext 的意义就在这里:不同种类的类型共享同一访问接口,但拥有不同的创建和保存策略。

新人自测

  1. 为什么容器需要保存“类型规则”,而不只保存对象?
  2. getBean()newBean() 为什么不应该完全相同?
  3. 接口无法构造,Rain 如何让它返回具体对象?
  4. 为什么容器单例不等于 JVM 全局单例?
  5. List<Processor> 注入说明容器掌握了哪些额外信息?

基于 Apache License 2.0 发布