Skip to content

Application 模块

application 是 Rain 的运行时编排模块。它不实现具体业务能力,而是负责把配置、DI、类扫描、Loader、ApplicationService、事件和 JVM 关闭钩子组织成一条确定的启动链。

如果目标是开发 Rain 上层框架,建议先阅读扩展点总览,再结合本页理解 Loader 与生命周期的源码细节。完整模块范例见框架模块开发实战

引入依赖

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

该模块依赖 DI、classloader 与 hook。Event、Job、Controller 等业务模块仍需按需加入。

两种启动器

FullStackApplicationLauncher

kotlin
import rain.application.FullStackApplicationLauncher

fun main() {
    FullStackApplicationLauncher.launch()
}

完整启动器是普通应用的默认选择。它会:

  1. 推断调用包并写入 rain.launchPackage
  2. 创建 AppClassloader,设置为线程上下文类加载器。
  3. 注册 EnchantManager、Hook Transformer 和 Kotlin 注入 Transformer。
  4. 通过新的类加载器重新加载 rain.application.Application
  5. 反射调用 Application.start()

为什么要重新加载 Application?因为需要保证应用类和 Rain 增强组件从 AppClassloader 进入 JVM,Transformer 才能在类定义前修改字节码。直接在原始类加载器中创建 Application 会绕过这条增强链。

可以显式指定扫描启动包:

kotlin
FullStackApplicationLauncher.launch("com.example.app")

BasicApplicationLauncher

kotlin
import rain.application.BasicApplicationLauncher

fun main() {
    BasicApplicationLauncher.launch()
}

基础启动器直接执行 Application().start(),不建立增强类加载器。适合:

  • 明确不需要 Hook、Kotlin by inject 等增强。
  • 在受限类加载环境中嵌入 Rain。
  • 调试配置和基础 DI,不希望引入字节码转换变量。

如果模块依赖 Rain Hook(例如 SmartAccess 的声明式事务),不要使用基础启动器。

Application.start 执行顺序

源码中的主流程可以概括为:

text
确定 runMode 与 launchPackage

ConfigImpl.init()

ContextImpl.init()

向 DI 注册 IRainClassLoader(完整启动器)

AppLoader.load()
  ├─ 执行 Module.onLoad
  ├─ 扫描 rain.scanPackages
  ├─ 建立 @AutoBind 映射
  ├─ 注册 BeanFactory
  ├─ 调用 ClassRegister
  ├─ 发现 @LoadBy
  └─ 按优先级执行 Loader

ApplicationServiceLoader.start()

发布 AppStatusEvent.AppStarted

注册 JVM shutdown hook

这个顺序解释了几个重要约束:

  • 配置和 DI 在所有 Loader 之前可用。
  • Loader 在 ApplicationService.start() 之前完成元数据扫描和注册。
  • AppStarted 事件在全部应用服务启动后发布。
  • 需要监听 AppStarted 的 Event Loader 必须已经在此前完成监听器注册。

运行模式

Application 按以下顺序确定运行模式:

  1. JVM 系统属性 rain.runMode
  2. 当前目录存在 pom.xmlbuild.gradlebuild.gradle.kts 时使用 dev
  3. 否则不在 Application 层指定,由配置实现按其规则处理。

显式指定生产模式:

bash
java -Drain.runMode=prod -jar app.jar

完整配置加载与覆盖规则见配置参考

类扫描

AppLoaderrain.scanPackages 读取包列表:

properties
rain.scanPackages=[com.example.app, com.example.shared]

它通过应用类加载器扫描包内类。扫描范围直接决定:

  • 哪些类能成为 Bean。
  • 哪些 @LoadBy 标记会被发现。
  • 哪些 ApplicationService 会启动。
  • 哪些 Event Listener、Job、Controller 会注册。
  • 哪些 BeanFactory@AutoBind 实现会进入容器。

不要扫描整个第三方依赖根包。范围过大会增加启动成本,也可能把不应进入容器的实现注册进来。

Loader 扩展协议

Loader 是 Rain 最核心的模块发现协议:

kotlin
interface Loader : Comparable<Loader> {
    fun priority(): Int = 10
    fun load(items: Collection<LoadItem>)
}

一个注解、接口或父类通过 @LoadBy 指向 Loader:

kotlin
@LoadBy(CommandLoader::class)
annotation class Command

应用类:

kotlin
@Command
class RefreshCacheCommand

Loader:

kotlin
class CommandLoader(
    private val context: DiContext,
) : Loader {
    override fun priority() = 20

    override fun load(items: Collection<LoadItem>) {
        items.forEach { item ->
            val command = context.getBean(item.clazz)
            // 注册 command
        }
    }
}

LoadItem 包含:

属性含义
clazz实际被扫描到的类
target触发 @LoadBy 的注解、接口或父类
annotation通过元注解触发时的实际注解实例
loadByAnnotation是否由注解触发

Rain 不只检查类上的直接注解,也沿接口、父类和注解元数据寻找 @LoadBy。因此一个稳定接口也可以成为插件发现入口。

Loader 顺序

AppLoader 从 DI 容器取得所有涉及的 Loader,按 priority() 从小到大执行。依赖其他 Loader 产物的 Loader 应使用更大的值,并把顺序约束写进模块文档。

Loader 适合做启动期元数据工作,不适合启动后台线程;后台资源应交给 ApplicationService

ApplicationService

kotlin
interface ApplicationService {
    fun priority() = 10
    fun start() {}
    fun stop() {}
}

实现类位于扫描包且能成为 Bean 时,会被 ApplicationServiceLoader 收集:

kotlin
class MessageConsumerService(
    private val consumer: MessageConsumer,
) : ApplicationService {
    override fun priority() = 50

    override fun start() {
        consumer.connect()
    }

    override fun stop() {
        consumer.close()
    }
}

启动前 Rain 会再次对实例执行字段注入,然后按 priority 升序调用 start()

停止顺序

当前源码的 stop() 仍按与启动相同的升序遍历,而不是常见的逆序关闭。若服务 B 依赖服务 A 且 B 必须先停止,不能假设 priority 会自动形成栈式释放;应在现版本中显式协调关闭关系。

任一服务启动或停止抛出异常时,Loader 记录日志并继续向外抛出,不会自动回滚已经启动的服务。

应用状态事件

如果引入 Event 模块,Application 会发布:

  • AppStatusEvent.AppStarted:全部 ApplicationService 启动完成后。
  • AppStatusEvent.AppStopping:调用服务 stop 之前。
kotlin
@EventListener
class WarmupListener {
    @SubscribeEvent
    fun AppStatusEvent.AppStarted.warmup() {
        // 此时应用服务已经启动
    }
}

AppStopping 适合发出业务层停止通知,但关键资源仍应在 ApplicationService.stop() 释放,因为 EventBus 是可选依赖。

Module.onLoad

rain.modules 可以列出实现 Module 的类:

properties
rain.modules=com.example.feature.SearchModule
kotlin
class SearchModule : Module {
    override fun onLoad() {
        // 在类扫描之前进行模块初始化
    }
}

Module 在扫描应用类之前执行。它是低层启动扩展,当前接口只有 onLoad(),没有上下文参数和对应卸载回调。一般业务模块优先使用 Loader + ApplicationService;只有确实需要在扫描前动作时才使用 Module。

ClassRegister

通过 rain.classRegisters 配置 ClassRegister Bean:

properties
rain.classRegisters=com.example.IndexRegister

AppLoader 对每个扫描类调用:

kotlin
interface ClassRegister {
    fun register(clazz: Class<*>)
}

它适合构建类索引或做与具体 @LoadBy 无关的统一扫描。因为它会看到扫描范围内每个类,逻辑必须轻量,并避免在这里启动外部资源。

AutoBind

给接口标记 @AutoBind 后,AppLoader 会收集扫描范围中的实现,并按 @Named 名称建立接口绑定:

kotlin
@AutoBind
interface Storage

@Named("local")
class LocalStorage : Storage

@Named("s3")
class S3Storage : Storage

注入时通过名称选择实现。AutoBind 也会沿接口和父类关系检查,适合框架扩展点;普通业务的一对一依赖可以直接按实现类注入,避免不必要抽象。

自定义启动与嵌入

如果需要在宿主程序中控制 Application 实例:

kotlin
val application = Application()
application.start()
// ...
application.stop()

这条方式等价于基础启动,不自动建立 AppClassloader。需要完整增强时应调用 FullStackApplicationLauncher,当前公开启动器不会返回 Application 或 DiContext;测试场景应使用 rain-test

故障排查

Loader 没有执行

检查:

  1. 目标类是否位于 rain.scanPackages
  2. @LoadBy 是否标在正确注解、接口或父类上。
  3. 目标类是否满足 mastBean 和 Bean 判定要求。
  4. Loader 本身能否从 DI 容器创建。

ApplicationService 没有启动

检查类是否位于扫描范围、是否能成为 Bean、是否确实实现 ApplicationService。若构造器依赖无法解析,容器创建会失败。

完整启动器下出现类型转换异常

同名类若分别由父类加载器和 AppClassloader 加载,在 JVM 中是不同类型。不要提前在系统类加载器中主动加载需要增强的应用类;线程池也应继承或显式设置正确的 context classloader。

源码阅读入口

按以下顺序阅读可以快速理解整个模块:

  1. FullStackApplicationLauncher:增强类加载器建立。
  2. Application.start:运行时总入口。
  3. ConfigImplContextImpl:配置和容器初始化。
  4. AppLoader.load:扫描、绑定与 Loader 调度。
  5. ApplicationServiceLoader:服务生命周期。

基于 Apache License 2.0 发布