前言

上次介绍了如何在 Kotlin 语言中使用 DSL 构造注册命令,这篇来写一下注册事件监听器。

注册监听器通常情况下都是用 @EventHandler 标注一个方法,写在一个实现空接口 Listener 的类中,像这样:

1
2
3
4
5
6
object DemoListener : Listener {
@EventHandler
fun onPlayJoin(event: PlayerJoinEvent) {
event.player.sendMessage(ChatColor.AQUA + "Hi!")
}
}

在插件启动时写上:

1
pluginManager.registerEvents(DemoListener, plugin)

就可以正常食用了。如果不通过注解反射实现呢?我们需要翻一下源码,看看他的底层是怎么实现的。

Bukkit 源码部分

可以在 Bukkit 这里找到它的源码。拿到源码后我们从 registerEvents 这里入手,看看它帮我们干了什么不可描述的事情。PluginManager 是个接口,需要到它的实现类 SimplePluginManager 中找。

1
2
3
4
5
6
7
8
public void registerEvents(Listener listener, Plugin plugin) {
//...省略那个插件是否启用的判断
for (Map.Entry<Class<? extends Event>, Set<RegisteredListener>> entry :
plugin.getPluginLoader().createRegisteredListeners(listener, plugin).entrySet()) {
getEventListeners(getRegistrationClass(entry.getKey()))
.registerAll(entry.getValue());
}
}

可以看到关键在于掉用了 createRegisteredListeners 这个方法,然后把 RegisteredListenersEvent 对应交给 HandlerList 处理。(createRegisteredListenersPluginLoader 接口的方法)同样,我们需要找他的实现类 JavaPluginLoader。这个方法有 87 行,这里简述一下其才做流程:

  • 拿到 Listener 实例后,用反射找里面的 Method
  • 找到 Method 后检验是否符合注册的标准,并且拿到其注解 @EventHandler 中的优先级;
  • 对于被标注 @Deprecated 并符合要求的方法要在 logger 中打印出来,提醒使用者;
  • 建立一个 EventExecutor 接口的匿名类实例,将方法 execute 重写,在其中调用反射找来的方法 (method.invoke(listener, event));
  • 根据 useTimings 分别new 出 TimedRegisteredListenerRegisteredListener,并把它们加到与 Event 对应的 Map 中。

交给 HandlerList 后,其中有一个 EnumMap<EventPriority, ArrayList<RegisteredListener>> 来保存每一个优先级对应的监听器,在 PluginManagerfireEvent 方法中调用。看到这我们大概了解了它的注册原理,EventExecutor 这个东西才是最关键的。我们回到 PluginManager,除了有 registerEvents(Listener listener, Plugin plugin) 外,还有两个注册方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void registerEvent(Class<? extends Event> event, Listener listener, EventPriority priority, EventExecutor executor, Plugin plugin) {
registerEvent(event, listener, priority, executor, plugin, false);
}

public void registerEvent(Class<? extends Event> event, Listener listener, EventPriority priority, EventExecutor executor, Plugin plugin, boolean ignoreCancelled) {
Validate.notNull(listener, "Listener cannot be null");
Validate.notNull(priority, "Priority cannot be null");
Validate.notNull(executor, "Executor cannot be null");
Validate.notNull(plugin, "Plugin cannot be null");

if (!plugin.isEnabled()) {
throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled");
}

if (useTimings) {
getEventListeners(event).register(new TimedRegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
} else {
getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
}
}

对比一下刚才的 registerEvents ,我们发现它只是少了用反射找那些 method 的部分,并且让我们传入一个 EventExecutor 实例。这就很简单了,不过我一直没想明白为啥它还要求一个 Listener 实例。。。但是这不重要,传个空的就好,因为在 RegisteredListener 构造时也没有对 Listener 做出什么不可描述的事情。试着用一下:

1
2
3
4
5
pluginManager.registerEvent(PlayerJoinEvent::class.java, object : Listener {},
EventPriority.NORMAL,
{ _: Listener, e: Event -> (e as PlayerJoinEvent).player sendMessage "你好!" },
plugin
)

这里第三个参数应该是 EventExecutor,但此接口符合函数式接口的标准,Kotlin 帮我们 SAM 转换成 (Listener,Event) -> Unit。写好后放到服务器里运行,发现插件可以正常工作,下面就可以开始写 DSL 语言结构了。

DSL 构造事件监听器

与上篇说的命令相似,我们同样需要把监听器封装一下,让他持有注册时所需要的参数:

1
2
3
4
5
6
7
8
9
10
11
data class PackingEvent<in T : Event>(private val type: Class<out Event>,
private val eventPriority: EventPriority,
private val block: (T) -> Unit) {
@Suppress("UNCHECKED_CAST")
fun register() {
pluginManager.registerEvent(type, emptyListener, eventPriority,
{ _: Listener, event ->
block(event as T)
}, plugin)
}
}

在 Kotlin 中可以用数据类进行封装,不过没获得什么好处。因为我们传进去的是空 Listener,所以没必要再把一个空的拿回来。将 event cast 成 T,这里不用担心 cast 出错,因为注册的是什么事件,传进来的一定是想要的,类型不会错。有了 PackingEvent,就可以创造 Builder 了。但我们发现,与命令不同,注册事件没有那些可有可无的东西,并且只需要优先级和一个 (T) -> Unit。这样就没有必要再写 Builder 了,直接写个 Scope:

1
2
3
4
5
class EventScope {
inline fun <reified T : Event> event(eventPriority: EventPriority = EventPriority.NORMAL, noinline block: T.() -> Unit) {
PackingEvent(T::class.java, eventPriority, block).let(EventHolder::add)
}
}

只有一个函数,为了获取泛型的类实例,要写成 inline + reified。但 block : (T) -> Unit 会被存到 PackingEvent 中,并非原地调用,不能被编译器内联优化,所以 block 要加上关键字 noinline

再把 Scope 开放出来:

1
2
fun buildEvents(block: EventScope.() -> Unit) =
EventScope().block()

然后就可以愉快的食用了,在插件启动时写上:

1
2
3
4
5
6
7
8
buildEvents {
event<PlayerJoinEvent>(EventPriority.HIGH) {
player.sendMessage(ChatColor.GREEN+"欢迎加入!~")
}
event<PlayerBedLeaveEvent> {
//...
}
}

event 函数中直接写成了 T.() -> Unit 并且 apply(block) 这个大括号的里面就相当于对应事件的类中,可以直接访问里面的公共成员。优先级上面定义了如果不声明默认是 NORMAL

总结

还是说几个问题:

  • 无法动态注册、取消注册
  • 封装性欠缺

同命令部分,大家可以自行摸索或参考一下 emerald 中的 部分