Bukkit 为我们提供了一些事件,虽然对于原版而言是够用了,但对于插件的二次开发,这还是不够用的,举个例子,在登录插件中,玩家登录成功事件,怎么监听?如果采用各自定义的 API,就会使得开发变得极其麻烦。
有鉴于此,插件开发社区达成了一个共识:对于插件内有价值的事件,采用自定义事件进行触发。
自定义事件实际上和药水效果很像,都是通过「暴力地」直接继承某个 Bukkit 的对象。
如果你忘了事件处理的相关内容,请重新看一看 2-2。
所有的事件都继承了 Event
类,Event
类下还有 PlayerEvent
、InventoryEvent
等抽象类和 PlayerInteractEvent
这样的具体可监听的类。
要进行自定义事件,我们需要找一个最近似的类进行继承。比如,如果你的事件与玩家有关,那就继承 PlayerEvent
,更进一步,如果你的事件与玩家登录都有关,那就继承 PlayerLoginEvent
。
下面我们就以 HarmonyAuth SMART 中的「登录成功」事件为例,演示自定义事件的方法。
首先创建一个类用于描述这个事件,虽然这个事件叫做「登录成功」,但它和 PlayerLoginEvent
却没什么关联,因此我们不继承它,而只继承 PlayerEvent
这个抽象类。
package rarityeg.harmonyauthsmart;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import javax.annotation.Nonnull;
public class HASPlayerLoginEvent extends PlayerEvent implements Cancellable {
// 是个可取消事件,需要实现 Cancellable
private static final HandlerList handlers = new HandlerList();
// handlers 是与这个类挂钩的处理器们
private boolean isCancelled = false;
// 是否取消标志位
protected final boolean isAutoLogin;
// 我们自己实现的功能——是否是自动登录
@Override
@Nonnull
public HandlerList getHandlers() {
// 事件对象的「获取处理器」方法
return handlers;
}
public static HandlerList getHandlerList() {
// 事件类的「获取处理器」方法
return handlers;
}
public HASPlayerLoginEvent(Player p) {
// 构造方法
super(p, true);
}
@Override
public boolean isCancelled() {
// Cancellable 需要
return isCancelled;
}
@Override
public void setCancelled(boolean b) {
// Cancellable 需要
isCancelled = b;
}
public boolean isAutoLogin() {
// 自己添加的功能
return isAutoLogin;
}
}
这里的 HandlerList
需要进行一些说明。
getHandlers
,getHandlerList
(静态)是必不可少的方法,Bukkit 利用这个表来记录这个事件类有哪些处理器。getHandlers
是成员方法,用于在事件实例上获取这张表,而 getHandlerList
是静态方法,用于从这个类本身获取这张表。
另外请记住,有 getHandlerList
这个静态方法的类才可以被监听。
?> 到底怎么回事?
当有事件发生时,Bukkit 或者插件会创建这个事件的一个实例,之后 Bukkit 看着 getHandlers
方法决定要将这个事件实例分配给哪些处理器,这就是 getHandlers
的由来。
那为什么需要 getHandlerList
这个静态方法呢?
回想一下我们监听事件时,我对事件处理函数的描述:「(事件处理函数的)函数名不重要,重要的是参数类型」。
在注册事件处理器时,事件实例还没有出现(因为事件还没有发生),但是此时事件处理器急着要注册,Bukkit 必须找个地方记录那些处理函数,Bukkit 的选择就是在事件所属的类中进行记录,对应的方法就是 getHandlerList
。
由于「该变量(handlers
)不属于哪个对象,属于整个类共有」(参见 2-1),因此所有的该类事件实例都共享这个表,那么它们自然就具有了相同的事件处理器(即,每个事件都被公平对待)。
这样就很明白啦,有 getHandlerList
方法的类,Bukkit 可以将事件处理函数写在其中,那也就是可以监听嘛,反之,如果没有这个方法,那 Bukkit 没办法记住这个类有哪些处理函数,那不就是不可监听嘛。
如果看不懂以上内容也没关系,总之,记住,要自定义事件,getHandlers
和 getHandlerList
是必须的,当作固定格式来写就好啦。
另外,事件的取消并不是在事件中完成的,而是由事件的分发者查看取消标志来决定后续的处理,下面就会看到。
我们还为这个事件添加了属于我们的一些功能,例如 isAutoLogin
来查看这次登录是否是自动登录。自定义自定义嘛,有额外的功能也很重要。
接下来我们需要在我们的代码中考虑什么时候触发这个事件。想一想,我们的插件中,哪个时机适合触发「登录成功」事件?自然是玩家切实登录成功的适合嘛。
触发事件时一定要遵循实事求是的原则。
HarmonyAuth SMART 中只有三个地方判定玩家切实登录,一个是登录命令,一个是注册命令,还有一个是自动登录。我们的修改如下:
// 登录命令节选
if (idm.getPasswordHash(id).equals(Util.calculateMD5(args[0]))) {
// 当玩家密码正确时,登录
HASPlayerLoginEvent event = new HASPlayerLoginEvent(player, false);
// 创建事件实例,不是自动登录,填 false
Bukkit.getPluginManager().callEvent(event);
// 让 Bukkit 分发事件
if (event.isCancelled()) {
// 看看,如果这个事件被取消了,就不执行后面的玩家登录操作
sti(id);
return;
}
// 后略
}
// 注册命令节选
if (args.length < 2 || !args[0].equals(args[1])) {
player.sendMessage(Util.getAndTranslate("msg.register-failed"));
sti(id);
return;
}
// 密码一致,注册成功
HASPlayerLoginEvent event = new HASPlayerLoginEvent(player, false);
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) {
sti(id);
return;
}
// 自动登录的处理
if (seconds <= HarmonyAuthSMART.instance.getConfig().getInt("auto-login")) {
HASPlayerLoginEvent event = new HASPlayerLoginEvent(e.getPlayer(), true);
// 这次是自动登录
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) {
return;
}
// ... 后略
}
这样我们的自定义事件就做好啦!以后就可以通过 @EventHandler
来监听它了啦~
要注意的一点是,callEvent
方法应当尽可能的开新线程处理(runTask
),对于不可取消事件(即丢出去就不用再管的)更是应当如此。
因为谁也不知道这个事件上面挂了些什么处理函数,要是有一个函数阻塞了(如等待数据库),你的 callEvent
就可能会卡在那里,虽然 Bukkit 具有识别这种情况的能力,但这个风险还是尽量避免。
那上面的例子中为什么没有这样做呢?是因为「查询数据库」这一操作本来就已经在异步执行了,并且我们还需要判断事件有没有取消来决定让不让玩家登录,因此我们只好等等。
异步处理的本质就像「你慢慢弄,我来不及了,我先走了,一会再见」,而同步处理就像「快点快点,我等着你在」。在上面的例子中,如果要取消玩家的登录,就必须等待事件处理完成后查看有没有被取消,用异步实现就没有必要了。
通常情况下我们希望能够让其他开发者能够使用我们的事件,因此我们希望将它包括到 API 中。
不过这个事件是一个类,不是接口啊,看上去也不好注册什么服务,怎么打包呢?
直接将这个类移动到 API 中(本体中需要删除,然后以 API 为依赖,重新导入这个类),编译即可。
不需要注册服务,不需要进行任何其它的操作。
为什么呢?
首先,我们创建了这个事件,这个类没有使用本体中的类(仅仅调用了 Bukkit API)。
然后,这个事件在 HarmonyAuth SMART 将来的开发中,相当一段时间内都不会改变。
因此,我们可以直接将这个类打包到 API 中,因为监听事件本质上也不需要知道这个事件怎么触发,只需要知道这个事件的类就行了,我们就将它提供给开发者。
再说说服务和接口的意义吧,AC-2 中的 API 实际上本质就是通过接口和服务将「不变的部分」从「可变的部分」中「解放」出来,既然这个类本身就是「不变的」,并且也和「可变的部分」没有什么关联,那么我们也不需要「解放」,直接打包就可以了。
当然,如果你的事件类中包含了本体中其它的类(或者乃至其它插件的 API),那么那些类中「可变的」部分需要和 AC-2 中一样,抽象成接口,用本体实现,注册服务等等;「不变的」自然也可以直接打包。
实际上也就是「要解放我,先解放我依赖的那些」,就像移植树木需要先把根挖出来一样。
这就是自定义事件的方法了,在后面的插件开发中我们还会见到它的。