Skip to content

依赖注入与配置

DI 模块提供 Bean 注册、创建、查找和注入,并同时承担配置读取职责。Event、Job、Controller 和应用服务都依赖它创建运行时对象,因此理解 DI 是使用其他 Rain 模块的前提。

适合使用 DI 管理的对象通常是长期存在的应用服务、基础设施和扩展实现;短生命周期值对象、领域实体和方法内部临时对象仍应直接构造。

添加依赖

普通 Rain 应用通常已经引入 application,它会传递组合 DI:

kotlin
implementation("com.IceCreamQAQ.Rain:application:1.0.0-DEV12")

只构建容器工具、不需要 Application 扫描和生命周期时,可以直接依赖 DI:

kotlin
implementation("com.IceCreamQAQ.Rain:di:1.0.0-DEV12")

最小使用闭环

在扫描包中声明普通具体类,构造器参数就是必需依赖:

kotlin
class UserRepository {
    fun find(id: Long): User? = null
}

class UserService(
    private val repository: UserRepository,
) {
    fun findUser(id: Long) = repository.find(id)
}
properties
rain.scanPackages=com.example.app

当 Loader、ApplicationService、Event Listener 或 Controller 需要 UserService 时,容器会递归创建 UserRepositoryUserService。业务代码通常不需要主动调用 DiContext.getBean()

标准注入

Rain 使用 javax.inject 注解:

kotlin
class OrderService(
    private val repository: OrderRepository,
) {
    @Inject
    @Named("audit")
    lateinit var auditLogger: Logger
}
  • Kotlin 类默认使用公开主构造器进行注入,无需标记 @Inject,也无需显式书写 constructor
  • @Inject 可用于显式选择其他构造器,也可用于字段或参数。
  • @Named 选择命名实例;默认实例名为空字符串。
  • Java 类或具有多个特殊构造路径的类型,可以使用 @Inject 明确指定注入构造器。

配置注入

使用 rain.di.Config 将配置项转换为目标类型:

kotlin
class ServerOptions {
    @Config("server.port")
    var port: Int = 8080
}

也可通过 ConfigManager 主动读取:

kotlin
val port = configManager.getConfig("server.port", Int::class.java)
val hosts = configManager.getArray("server.hosts", String::class.java)

Kotlin 委托

模块提供 inject<T>()config<T>() 属性委托。它们依赖 Rain 的 Kotlin 类增强支持,应通过完整启动器运行:

kotlin
var service by inject<UserService>()
var port by config<Int>("server.port")

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 获取容器管理实例,newBean 创建新实例,injectBean 可向已有对象注入字段。

容器如何判断 Bean

Rain 的基础判断非常简单:不是接口、不是抽象类,就具备直接创建实例的条件。容器不会要求每个类都标记 @Component。类是否真正被提前发现,则由扫描范围和 Loader 使用方式决定。

这种设计减少了标记注解,但也意味着“可创建”不等于“应该作为全局服务”。建议仍通过包结构、构造器依赖和模块边界控制 Bean 范围。

构造器选择规则

Kotlin 类:

  1. 默认选择公开主构造器。
  2. 任一构造器标记 @Inject 时,以它覆盖主构造器选择。
  3. 没有可用注入构造器时尝试无参构造器。
  4. 都不存在则创建失败。

Java 类:

  1. 优先使用标记 @Inject 的公开构造器。
  2. 否则使用公开无参构造器。
  3. 都不存在则创建失败。
kotlin
class MailService @Inject constructor(
    private val client: MailClient,
)

Rain 会在首次需要某个类型时创建并缓存对应 BeanCreator,而不是每次都重新分析构造器。

单例、新实例与外部对象

kotlin
val shared = context.getBean(Service::class.java)
val anotherShared = context.getBean(Service::class.java)
check(shared === anotherShared)

val fresh = context.newBean(Service::class.java)
check(fresh !== shared)
  • getBeanClassContext 的缓存和命名绑定,通常取得容器实例。
  • newBean 调用当前类型的 creator 创建新对象,再按上下文规则注入。
  • injectBean 使用已缓存的 injector 向现有对象注入。
  • forceInjectBean 重新构建 injector,适合类型结构在缓存后发生特殊变化的框架场景,普通业务不应依赖。

字段与 Setter 注入

字段标记 @Inject@Config 后,BeanInjector 会在对象创建后写入:

kotlin
class ReportJob {
    @Inject
    lateinit var reportService: ReportService

    @Config("report.batchSize")
    var batchSize: Int = 100
}

公开 Setter 也可标记注入注解。Kotlin Setter 的可空性与默认参数会影响空值行为:非空、无默认值且解析不到依赖时会报错;可空或可选参数允许缺省。

构造器注入仍是业务代码首选,因为对象创建完成时依赖已经齐全,测试时也无需启动容器。

命名实例

默认实例名是空字符串。@Named 可以用于类、构造器参数和字段:

kotlin
interface Clock

@Named("system")
class SystemClock : Clock

class TokenService(
    @Named("system") private val clock: Clock,
)

也可通过 API 注册运行时实例:

kotlin
context.putBean(Clock::class.java, "fixed", FixedClock())
val clock = context.getBean(Clock::class.java, "fixed")

接口本身不能直接实例化,必须由 AutoBind、显式 putBean、BeanFactory 或框架内部注册建立绑定。

集合注入

DataReaderFactory 对 List<T>Map<String, T> 有专门读取器:

kotlin
class CodecRegistry(
    val codecs: List<Codec>,
    val namedCodecs: Map<String, Codec>,
)
  • List<T> 用于取得某类型的可用实现集合。
  • Map<String, T> 同时保留命名实例名称。

集合注入常用于框架插件点,例如 SmartWeb 注入多个 TempleEngine。业务层若只需要一个实现,应注入具体依赖,避免结果顺序或候选不明确。

BeanFactory

当接口实现需要根据名称动态创建,或实例不是普通构造器 Bean 时,实现:

kotlin
class ClientFactory : BeanFactory<RemoteClient> {
    override val type = RemoteClient::class.java
    override fun isMulti() = true

    override fun createBean(name: String): RemoteClient? =
        endpoints[name]?.let(::RemoteClient)
}

AppLoader 扫描 BeanFactory<T> 的泛型参数,为 T 注册 BeanFactoryClassContext。之后 getBean(RemoteClient::class.java, name) 会委托 Factory。

isMulti() 表达 Factory 是否允许多实例/多名称语义。Factory 返回 null 表示无法提供目标名称。

配置注入路径

@Config 可用于构造器参数、字段和 Setter:

kotlin
data class HttpOptions(
    val host: String,
    val port: Int,
)

class HttpService(
    @Config("http") val options: HttpOptions,
    @Config("http.hosts") val hosts: List<String>,
)

配置底层统一保存为 ObjectNodeArrayNodeStringNode,读取时通过 RelType 转换为标量、对象、List 或 Map。因此同一套 API 可读取 Properties、YAML 和 JSON 合并后的结果。

kotlin
val options = configManager.getConfig<HttpOptions>("http")
val hosts = configManager.getArray<String>("http.hosts")
val backends = configManager.getMap<Backend>("backends")

getArray / getMap 在节点不存在时返回空集合;getConfig 返回 null

配置 Reader

ConfigManager.getConfigReader 返回一个绑定路径和类型的 Reader,DI 在创建 BeanCreator/Injector 时预先生成它。这样每次创建对象不需要重新分析注解和泛型。

getConfigWriter 当前源码仍是 TODO,不能作为可用的运行时配置写入 API。

Kotlin 委托的工作方式

完整启动器注册 YuContextKotlinInjectTransformer。Transformer 让使用 inject() / config() 的类获得访问 YuContext 的桥接能力:

kotlin
class Handler {
    val service by inject<UserService>()
    val timeout by config<Long>("request.timeout")
}

委托第一次读取时查询容器并把结果缓存到委托对象。它不是每次访问都动态查询,也不会随配置变化自动刷新。

因为该能力依赖字节码增强:

  • 必须使用 FullStackApplicationLauncherrain-test
  • 类必须由 Rain AppClassloader 加载。
  • 对依赖可见性要求高的核心业务,仍推荐构造器注入。

Rain DI 与 Spring 的差异

Rain 没有 Spring 的 BeanDefinition、Scope、条件装配和 BeanPostProcessor 大体系。它围绕 ClassContext 保存某个 JVM 类型的 creator、injector、实例和命名绑定:

text
Class<T>

ClassContext<T>
  ├─ BeanCreator<T>
  ├─ BeanInjector<T>
  ├─ 默认/命名实例
  └─ 接口绑定或 BeanFactory

实现更短、更容易沿源码调试;相应地,高级生命周期、声明式条件和成熟第三方自动配置需要应用或模块自行约定。详细设计对照见 Rain 与 Spring

常见错误

无法创建 Bean

检查是否存在公开主构造器、@Inject 构造器或公开无参构造器,以及所有非空参数能否解析。

接口注入为 null

检查接口是否建立 AutoBind、命名绑定、Factory 或手工实例。接口仅位于扫描包并不会自动选择任意实现,除非它参与 AutoBind 协议。

委托注入类型转换失败

通常表示类没有经过 Rain Transformer,或在完整启动器之前已被父类加载器加载。改用构造器注入也可直接排除此问题。

配置对象转换失败

检查配置树形状与目标类字段是否一致,尤其是 Properties 点路径、数组重复键和 Kotlin 非空字段。

基于 Apache License 2.0 发布