生命周期 @Awake
@Awake 的函数约束、LOAD/ENABLE/ACTIVE/DISABLE 四阶段触发时机与 priority 排序。
Blink 用注解 @Awake 把初始化/清理逻辑挂到插件生命周期上,编译期扫描、运行期按优先级触发,无需继承 JavaPlugin,也不用手写 onEnable。跟着本篇读,你就能把开机、关机要做的事安排到正确的阶段。
读完本篇你能:
- 在
object或顶层函数上正确使用@Awake。 - 区分
LOAD/ENABLE/ACTIVE/DISABLE四个阶段的触发时机。 - 用
priority控制同阶段执行顺序。 - 理解
LifeCycleManager如何注册、触发、清理,以及 reload 后 handler 为何不重复累积。
1. 最小示例
@Awake 标在无参、无返回值的函数上,传入目标阶段:
无需 extends JavaPlugin,无需手动注册。构建时 Blink 扫描所有编译产物里的 @Awake,生成调用桥接,运行期由插件主类在对应阶段触发。
日志 API(BlinkLog.info/warn/error)见 日志。
2. 注解定义
注解只有两个参数(来源:Awake.kt):
value:必填,目标阶段,取LifeCycle枚举之一。priority:可选,同阶段内执行顺序,默认0。
阶段枚举(来源:LifeCycle.kt):
3. 函数约束
Blink 在编译期用 ASM 扫描字节码(AnnotationScanner.kt),生成的桥接代码对被标注函数有硬性要求。
不满足约束时,该函数会被静默跳过或打印警告,不会编译报错。所以请留意构建日志,别让钩子悄悄失效。
3.1 必须无参、无返回值
扫描器要求方法描述符严格为 ()V,否则跳过并告警:
需要插件实例时,不要加参数,改用 Blink 的全局访问入口,或在函数体内自取。
3.2 接收者必须是可静态访问的单例
生成的 lambda 调用你的函数时,需要一个能在静态上下文拿到的接收者。扫描器据此把方法归为三类(来源:BytecodeGenerator.kt 的 generateLifeCycleClass):
| 写法 | 字节码接收者 | 是否支持 |
|---|---|---|
顶层函数(编译进 XxxKt,方法为 static) | INVOKESTATIC | 支持 |
object 单例(有 INSTANCE 静态字段) | GETSTATIC INSTANCE 后 INVOKEVIRTUAL | 支持 |
companion object 内的函数 | 从外层类的 companion 字段取实例 | 支持 |
普通 class 的实例方法 | 无法静态获取实例 | 不支持,被跳过 |
判定逻辑(generateLifeCycleClass):
不是静态、不是 object 单例、也不是 companion,就被跳过。
把生命周期函数放进 object,或写成顶层函数,这两种最稳妥:
顶层函数会被 Kotlin 编译进文件级类 文件名Kt,方法是 static,天然满足要求。
4. 四个阶段的触发时机
四个阶段由生成的插件主类 BlinkGeneratedMain 触发,时机固定(来源:BytecodeGenerator.kt 的 generateOnLoad / generateOnEnable / generateOnDisable):
4.1 LOAD — 服务器加载阶段(onLoad)
对应 Bukkit 的 JavaPlugin.onLoad(),在所有插件 onEnable 之前。此时:
- 依赖已加载、
BlinkLog已初始化、脚本/NMS 等子系统已init。 @Awake的注册刚刚完成(registerAll()在trigger(LOAD)之前一行),所以LOAD自身能被触发。- 还不能注册命令、监听事件,世界也尚未加载。
适合:注册 WorldGuard flag 之类要求在 onLoad 完成的早期 hook、预读配置、声明只读静态资源。
4.2 ENABLE — 插件启用阶段(onEnable)
对应 JavaPlugin.onEnable()。事件监听(@AutoListener)已在本阶段触发前注册完成。
适合:绝大多数初始化——打开数据库连接、加载配置、注册调度任务、初始化管理器单例。这是默认且最常用的阶段。
@AutoListener 见 事件。
4.3 ACTIVE — 服务器就绪后的第一个 tick
ACTIVE 不在 onEnable 同步执行,而是通过 server.scheduler.runTask(...) 调度到启用后的第一个服务器 tick(见 generateOnEnable 里的 lambda$onEnable$0)。
该 tick 到来时,所有插件都已 onEnable 完毕、世界已加载,因此 ACTIVE 是做跨插件交互和访问已加载世界/在线实体的安全点。
适合:读取/对接其他插件提供的服务、扫描已加载世界中的方块或实体、依赖"所有插件都已就绪"的逻辑。
4.4 DISABLE — 插件卸载阶段(onDisable)
对应 JavaPlugin.onDisable(),且是 onDisable 中最先执行的步骤——在事件注销和 LifeCycleManager.clear() 之前。所以 DISABLE 钩子运行时,事件监听和其它生命周期注册仍然有效。
适合:保存数据、关闭连接、取消自己创建的调度任务、释放外部资源。
5. priority:控制同阶段顺序
同一阶段内,多个 @Awake 按 priority 升序执行(数字小的先跑)。排序逻辑在 LifeCycleManager.trigger 里:
要点:
- 越小越早,需要某段逻辑先跑就给它更小的
priority(可用负数)。 priority只在同一阶段内比较,不跨阶段;阶段先后由四个枚举的固定顺序决定。- 未指定时为
0;相同priority的相对顺序不保证稳定,不要依赖。
6. 运行期:LifeCycleManager
LifeCycleManager 是 object 单例(LifeCycleManager.kt),负责注册、触发、清理。日常开发不需要直接调用它——所有注册由生成代码完成;以下说明其行为,便于排查。
6.1 注册
每个有效的 @Awake 会被生成代码翻译成一次 register 调用:
action 是封装了你函数调用的 Runnable,name 形如 类名#方法名(生成代码用 ${entry.ownerInternal.substringAfterLast('/')}#${entry.methodName} 拼出),仅用于出错时定位。注册发生在 onLoad 里的 BlinkGeneratedLifeCycle.registerAll()。
6.2 触发与排序缓存
两个关键行为:
- 排序结果缓存在
sortedCache,首次触发某阶段时排一次序,之后复用。 - 每个 handler 单独 try/catch:一个
@Awake抛异常不会中断同阶段其余 handler,只打一条带类名#方法名的错误日志。某模块初始化失败,其它模块仍正常初始化。
6.3 清理与幂等
clear() 在两处被调用:
BlinkGeneratedLifeCycle.registerAll()开头先clear()一次(幂等保障)。onDisable触发完DISABLE后调用clear()。
这保证服务器 reload(重新走 onLoad/onEnable)时,旧 handler 先清空再重新注册,不会重复累积。事件侧由 EventManager.unregisterAll() 兜底。
7. 排错清单
@Awake 没生效时,按顺序自查:
- 构建日志里有没有
[Blink] 扫描到 N 个 @Awake?数字是否对得上钩子数。 - 函数是否无参无返回(
()V)?有参/有返回会被跳过,留意 stderr 里的expected ()V — skipping警告。 - 函数是否在
object/ 顶层 /companion object里?普通class的实例方法会被静默跳过。 - 阶段选对了吗?需要其它插件就绪请用
ACTIVE而非ENABLE。 - 同阶段顺序不对?检查
priority(越小越先)。 - 运行期某钩子报错但其它正常?这是设计行为,去日志里找
@Awake(阶段) 类名#方法名 执行出错。
