事件 @AutoListener
用 @AutoListener 声明监听器、签名约束、priority/ignoreCancelled,以及 EventManager 动态监听。
用 @AutoListener 在普通方法上声明 Bukkit 事件监听器,无需手写 Listener 类、无需 registerEvents,构建期自动织入注册代码。下面带你从一个最小例子出发,把签名约束、参数语义到运行时动态监听逐一讲清。
读完本篇你能:
- 用
@AutoListener把方法变成监听器,并正确放置(object/companion object/@JvmStatic)。 - 掌握方法签名约束与
priority、ignoreCancelled的语义。 - 用
EventManager.listen/unlisten在运行时动态增删监听器。 - 理解 reload /
onDisable后框架如何自动清理,避免 handler 累积。
一个最小例子
没有 implements Listener、@EventHandler,也不必在 onEnable 里 registerEvents。构建时 Blink 的 Gradle 插件扫描注解,生成注册代码;插件 onEnable 统一注册,onDisable 统一注销。
注解定义只有两个参数(AutoListener.kt):
方法签名的约束
构建期扫描器(AnnotationScanner.kt)按字节码读取方法描述符,对签名有严格要求。
不满足约束的方法会被跳过并打印 WARNING,不会编译报错,请留意构建日志。
1. 恰好一个 Event 参数
扫描器调用 extractFirstParamType 取方法第一个参数类型作为事件类型:
由此推导出规则:
- 必须至少有一个参数,且第一个参数是引用类型(基本类型如
Int、Boolean会让extractFirstParamType返回null,方法被跳过)。 - 该类型直接用于注册,因此第一个参数必须是
org.bukkit.event.Event的子类。框架不校验,但若不是,运行时注册会失败。 - 应只声明一个参数。多参数时第一个虽仍被取出,但生成的调用 lambda 只传入事件实例,签名对不上会导致字节码不匹配。
取不到合法事件类型时,构建日志会出现:
2. 返回类型必须是 Unit
生成的调用约定是 (L事件类型;)V(见 BytecodeGenerator.kt generateEventsClass),即返回 void / Kotlin Unit。监听方法保持 Unit 即可。
3. 方法要可被静态访问到
生成器需要拿到接收者实例才能调用方法,支持三种载体(BytecodeGenerator.kt 中的分支):
| 载体 | 写法 | 生成代码如何取实例 |
|---|---|---|
顶层函数 / @JvmStatic | 静态方法 | 直接 INVOKESTATIC |
object 单例 | object Foo { ... } | 取 Foo.INSTANCE 字段 |
companion object | class Foo { companion object { ... } } | 从外层类取 companion 实例字段 |
对应到扫描器,监听器只在 isStatic、hasInstance(是 object 单例)、或 companionAccess != null(在 companion 里)三者之一成立时才注册:
不要把 @AutoListener 放在需要 new 出来的普通类的实例方法上:框架没有实例,会静默跳过。推荐放在 object 里。
priority 与 ignoreCancelled
这两个参数原样透传给 Bukkit 的 PluginManager.registerEvent。
priority
priority 用 org.bukkit.event.EventPriority,默认 NORMAL,语义同原生 Bukkit:LOWEST → LOW → NORMAL → HIGH → HIGHEST → MONITOR,数值越高越晚执行、越接近最终结果。MONITOR 用于只读观察,不要在其中修改事件。
ignoreCancelled
ignoreCancelled 默认 false。设为 true 时,若事件在你之前已被取消,你的监听器不会被调用,等价于原生 @EventHandler(ignoreCancelled = true)。
两值在构建期被读出(AutoListenerAnnotationVisitor),最终落到注册调用 register(eventClass, priority, ignoreCancelled, executor),见下节 EventManager。
EventManager:底层注册与运行时动态监听
所有静态声明(@AutoListener)和动态监听最终都走 EventManager(EventManager.kt)。它是一个 object,区分两类监听器:
- 静态监听器:一个共享的
staticListener,承载所有@AutoListener生成的注册。 - 动态监听器:按字符串
key区分,存在ConcurrentHashMap<String, Listener>里,可单独注销。
构建期生成的注册
@AutoListener 最终生成对 register(...) 的调用:
它把事件挂到共享的 staticListener 上。生成的 EventExecutor 内部还有一道 instanceof 守卫(见 BytecodeGenerator 的 generateEventsClass),事件类型不匹配时直接返回,避免父类 HandlerList 把不相关的子类事件分发进来导致 ClassCastException。
运行时动态监听:listen
在运行时临时注册监听器用 EventManager.listen,它有一个 reified 重载:
用法:
关键点:
key是身份标识,同 key 会先注销旧的再注册新的(listen第一行就调用unlisten(key)),重新注册天然幂等。- 每个动态监听用独立的匿名
Listener实例,便于按 key 单独注销。 - 内部同样有
eventClass.isInstance(event)守卫,类型不符的事件被忽略。
还有一个非内联重载,只有 Class<T> 而没有具体泛型时使用:
取消动态监听:unlisten / unlistenAll
unlisten 内部对该 key 的 Listener 调用 HandlerList.unregisterAll(listener),从 Bukkit 摘除。
查询辅助方法
reload 后的清理
Bukkit 的 /reload 或插件管理器重载会重新走 onLoad/onEnable/onDisable,手写监听器最易出的 bug 是重复注册导致每个事件被处理多次。Blink 在生成的主类里做了双重保障。
注册前先清空:生成的 BlinkGeneratedEvents.registerAll() 在注册任何监听器之前先调用一次 EventManager.unregisterAll()(BytecodeGenerator.kt generateEventsClass 顶部):
onDisable 时再清空:生成的 onDisable 也会调用 EventManager.unregisterAll()(generateOnDisable)。
unregisterAll 同时清掉静态和动态两类监听:
由此得出两条实践结论:
@AutoListener声明的监听器是托管的,reload 与插件停用都会自动清理。- 手动
listen出来的动态监听同样会在onDisable/ 重新注册时被一并清掉(走同一个unlistenAll)。
若希望某个动态监听在重载后继续存在,应在合适的生命周期回调里重新 listen,参见 生命周期 @Awake 选择 ENABLE 或 ACTIVE 时机。
小结清单
- 把方法放进
object(或 companion /@JvmStatic),加@AutoListener,参数恰好一个Event子类,返回Unit。 - 用
priority/ignoreCancelled控制时机与是否处理已取消事件,语义同原生 Bukkit。 - 运行时临时监听用
EventManager.listen(key) { ... },同 key 自动替换;用unlisten(key)摘除。 - reload 安全由框架负责:注册前与
onDisable都会unregisterAll,不会累积。
下一步
- 生命周期 @Awake:监听器的注册时机由生命周期决定。
- 命令:做一条
/track命令开关动态监听。
