前言
上次介绍了如何在 Kotlin 语言中使用 DSL 构造注册命令,这篇来写一下注册事件监听器。
注册监听器通常情况下都是用 @EventHandler 标注一个方法,写在一个实现空接口 Listener 的类中,像这样:
1 | object DemoListener : Listener { |
在插件启动时写上:
1 | pluginManager.registerEvents(DemoListener, plugin) |
就可以正常食用了。如果不通过注解反射实现呢?我们需要翻一下源码,看看他的底层是怎么实现的。
Bukkit 源码部分
可以在 Bukkit 这里找到它的源码。拿到源码后我们从 registerEvents 这里入手,看看它帮我们干了什么不可描述的事情。PluginManager 是个接口,需要到它的实现类 SimplePluginManager 中找。
1 | public void registerEvents(Listener listener, Plugin plugin) { |
可以看到关键在于掉用了 createRegisteredListeners 这个方法,然后把 RegisteredListeners 和 Event 对应交给 HandlerList 处理。(createRegisteredListeners 是 PluginLoader 接口的方法)同样,我们需要找他的实现类 JavaPluginLoader。这个方法有 87 行,这里简述一下其才做流程:
- 拿到
Listener实例后,用反射找里面的Method; - 找到 Method 后检验是否符合注册的标准,并且拿到其注解
@EventHandler中的优先级; - 对于被标注
@Deprecated并符合要求的方法要在 logger 中打印出来,提醒使用者; - 建立一个
EventExecutor接口的匿名类实例,将方法execute重写,在其中调用反射找来的方法 (method.invoke(listener, event)); - 根据
useTimings分别new 出TimedRegisteredListener或RegisteredListener,并把它们加到与Event对应的 Map 中。
交给 HandlerList 后,其中有一个 EnumMap<EventPriority, ArrayList<RegisteredListener>> 来保存每一个优先级对应的监听器,在 PluginManager 的 fireEvent 方法中调用。看到这我们大概了解了它的注册原理,EventExecutor 这个东西才是最关键的。我们回到 PluginManager,除了有 registerEvents(Listener listener, Plugin plugin) 外,还有两个注册方法:
1 | public void registerEvent(Class<? extends Event> event, Listener listener, EventPriority priority, EventExecutor executor, Plugin plugin) { |
对比一下刚才的 registerEvents ,我们发现它只是少了用反射找那些 method 的部分,并且让我们传入一个 EventExecutor 实例。这就很简单了,不过我一直没想明白为啥它还要求一个 Listener 实例。。。但是这不重要,传个空的就好,因为在 RegisteredListener 构造时也没有对 Listener 做出什么不可描述的事情。试着用一下:
1 | pluginManager.registerEvent(PlayerJoinEvent::class.java, object : Listener {}, |
这里第三个参数应该是 EventExecutor,但此接口符合函数式接口的标准,Kotlin 帮我们 SAM 转换成 (Listener,Event) -> Unit。写好后放到服务器里运行,发现插件可以正常工作,下面就可以开始写 DSL 语言结构了。
DSL 构造事件监听器
与上篇说的命令相似,我们同样需要把监听器封装一下,让他持有注册时所需要的参数:
1 | data class PackingEvent<in T : Event>(private val type: Class<out Event>, |
在 Kotlin 中可以用数据类进行封装,不过没获得什么好处。因为我们传进去的是空 Listener,所以没必要再把一个空的拿回来。将 event cast 成 T,这里不用担心 cast 出错,因为注册的是什么事件,传进来的一定是想要的,类型不会错。有了 PackingEvent,就可以创造 Builder 了。但我们发现,与命令不同,注册事件没有那些可有可无的东西,并且只需要优先级和一个 (T) -> Unit。这样就没有必要再写 Builder 了,直接写个 Scope:
1 | class EventScope { |
只有一个函数,为了获取泛型的类实例,要写成 inline + reified。但 block : (T) -> Unit 会被存到 PackingEvent 中,并非原地调用,不能被编译器内联优化,所以 block 要加上关键字 noinline。
再把 Scope 开放出来:
1 | fun buildEvents(block: EventScope.() -> Unit) = |
然后就可以愉快的食用了,在插件启动时写上:
1 | buildEvents { |
event 函数中直接写成了 T.() -> Unit 并且 apply(block) 这个大括号的里面就相当于对应事件的类中,可以直接访问里面的公共成员。优先级上面定义了如果不声明默认是 NORMAL。
总结
还是说几个问题:
- 无法动态注册、取消注册
- 封装性欠缺