Application 模块
application 是 Rain 的运行时编排模块。它不实现具体业务能力,而是负责把配置、DI、类扫描、Loader、ApplicationService、事件和 JVM 关闭钩子组织成一条确定的启动链。
如果目标是开发 Rain 上层框架,建议先阅读扩展点总览,再结合本页理解 Loader 与生命周期的源码细节。完整模块范例见框架模块开发实战。
引入依赖
implementation("com.IceCreamQAQ.Rain:application:1.0.0-DEV12")该模块依赖 DI、classloader 与 hook。Event、Job、Controller 等业务模块仍需按需加入。
两种启动器
FullStackApplicationLauncher
import rain.application.FullStackApplicationLauncher
fun main() {
FullStackApplicationLauncher.launch()
}完整启动器是普通应用的默认选择。它会:
- 推断调用包并写入
rain.launchPackage。 - 创建
AppClassloader,设置为线程上下文类加载器。 - 注册
EnchantManager、Hook Transformer 和 Kotlin 注入 Transformer。 - 通过新的类加载器重新加载
rain.application.Application。 - 反射调用
Application.start()。
为什么要重新加载 Application?因为需要保证应用类和 Rain 增强组件从 AppClassloader 进入 JVM,Transformer 才能在类定义前修改字节码。直接在原始类加载器中创建 Application 会绕过这条增强链。
可以显式指定扫描启动包:
FullStackApplicationLauncher.launch("com.example.app")BasicApplicationLauncher
import rain.application.BasicApplicationLauncher
fun main() {
BasicApplicationLauncher.launch()
}基础启动器直接执行 Application().start(),不建立增强类加载器。适合:
- 明确不需要 Hook、Kotlin
by inject等增强。 - 在受限类加载环境中嵌入 Rain。
- 调试配置和基础 DI,不希望引入字节码转换变量。
如果模块依赖 Rain Hook(例如 SmartAccess 的声明式事务),不要使用基础启动器。
Application.start 执行顺序
源码中的主流程可以概括为:
确定 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 按以下顺序确定运行模式:
- JVM 系统属性
rain.runMode。 - 当前目录存在
pom.xml、build.gradle或build.gradle.kts时使用dev。 - 否则不在 Application 层指定,由配置实现按其规则处理。
显式指定生产模式:
java -Drain.runMode=prod -jar app.jar完整配置加载与覆盖规则见配置参考。
类扫描
AppLoader 从 rain.scanPackages 读取包列表:
rain.scanPackages=[com.example.app, com.example.shared]它通过应用类加载器扫描包内类。扫描范围直接决定:
- 哪些类能成为 Bean。
- 哪些
@LoadBy标记会被发现。 - 哪些
ApplicationService会启动。 - 哪些 Event Listener、Job、Controller 会注册。
- 哪些
BeanFactory和@AutoBind实现会进入容器。
不要扫描整个第三方依赖根包。范围过大会增加启动成本,也可能把不应进入容器的实现注册进来。
Loader 扩展协议
Loader 是 Rain 最核心的模块发现协议:
interface Loader : Comparable<Loader> {
fun priority(): Int = 10
fun load(items: Collection<LoadItem>)
}一个注解、接口或父类通过 @LoadBy 指向 Loader:
@LoadBy(CommandLoader::class)
annotation class Command应用类:
@Command
class RefreshCacheCommandLoader:
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
interface ApplicationService {
fun priority() = 10
fun start() {}
fun stop() {}
}实现类位于扫描包且能成为 Bean 时,会被 ApplicationServiceLoader 收集:
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 之前。
@EventListener
class WarmupListener {
@SubscribeEvent
fun AppStatusEvent.AppStarted.warmup() {
// 此时应用服务已经启动
}
}AppStopping 适合发出业务层停止通知,但关键资源仍应在 ApplicationService.stop() 释放,因为 EventBus 是可选依赖。
Module.onLoad
rain.modules 可以列出实现 Module 的类:
rain.modules=com.example.feature.SearchModuleclass SearchModule : Module {
override fun onLoad() {
// 在类扫描之前进行模块初始化
}
}Module 在扫描应用类之前执行。它是低层启动扩展,当前接口只有 onLoad(),没有上下文参数和对应卸载回调。一般业务模块优先使用 Loader + ApplicationService;只有确实需要在扫描前动作时才使用 Module。
ClassRegister
通过 rain.classRegisters 配置 ClassRegister Bean:
rain.classRegisters=com.example.IndexRegisterAppLoader 对每个扫描类调用:
interface ClassRegister {
fun register(clazz: Class<*>)
}它适合构建类索引或做与具体 @LoadBy 无关的统一扫描。因为它会看到扫描范围内每个类,逻辑必须轻量,并避免在这里启动外部资源。
AutoBind
给接口标记 @AutoBind 后,AppLoader 会收集扫描范围中的实现,并按 @Named 名称建立接口绑定:
@AutoBind
interface Storage
@Named("local")
class LocalStorage : Storage
@Named("s3")
class S3Storage : Storage注入时通过名称选择实现。AutoBind 也会沿接口和父类关系检查,适合框架扩展点;普通业务的一对一依赖可以直接按实现类注入,避免不必要抽象。
自定义启动与嵌入
如果需要在宿主程序中控制 Application 实例:
val application = Application()
application.start()
// ...
application.stop()这条方式等价于基础启动,不自动建立 AppClassloader。需要完整增强时应调用 FullStackApplicationLauncher,当前公开启动器不会返回 Application 或 DiContext;测试场景应使用 rain-test。
故障排查
Loader 没有执行
检查:
- 目标类是否位于
rain.scanPackages。 @LoadBy是否标在正确注解、接口或父类上。- 目标类是否满足
mastBean和 Bean 判定要求。 - Loader 本身能否从 DI 容器创建。
ApplicationService 没有启动
检查类是否位于扫描范围、是否能成为 Bean、是否确实实现 ApplicationService。若构造器依赖无法解析,容器创建会失败。
完整启动器下出现类型转换异常
同名类若分别由父类加载器和 AppClassloader 加载,在 JVM 中是不同类型。不要提前在系统类加载器中主动加载需要增强的应用类;线程池也应继承或显式设置正确的 context classloader。
源码阅读入口
按以下顺序阅读可以快速理解整个模块:
FullStackApplicationLauncher:增强类加载器建立。Application.start:运行时总入口。ConfigImpl与ContextImpl:配置和容器初始化。AppLoader.load:扫描、绑定与 Loader 调度。ApplicationServiceLoader:服务生命周期。