本帖最后由 桃源村服主 于 2023-10-4 16:45 编辑
一、前言
前段时间我在某插件开发交流群中看到这样一个问题:
为什么用Bukkit.getPluginManager().registerEvent监听ItemSpawnEvent,结果会一并监听到CreatureSpawnEvent啊
随即想到我在上一个帖子[插件开发教程] 监听“玩家放置末影水晶”的方式中同样使用了Bukkit.getPluginManager().registerEvent,当玩家放置末影水晶时,后台会打印4次“EntitySpawnEvent”,这似乎也存在问题。
对于这些问题,我作了一番探究,在此分享给大家。
二、问题复现
首先我们需要尝试复现这个问题。
在Bukkit中文文档中,对PluginManager#registerEvent方法介绍如下:
data/myattachment/forum/202310/04/105130ppiuqkwvqbqzkqzb.png
那么我们可以写出这样的代码,使用Bukkit.getPluginManager().registerEvent注册一个监听器,监听到ItemSpawnEvent就全服播报事件的名称。
代码如下:
    public static Plugin plugin;
    @Override
    public void onEnable() {
        plugin = this;
        Bukkit.getPluginManager().registerEvent(ItemSpawnEvent.class,new MyListener(), EventPriority.NORMAL,new MyEventExecutor(),plugin);
    }
    static class MyListener implements Listener {
        MyListener() {
        }
    }
    static class MyEventExecutor implements EventExecutor {
        public void execute(Listener listener, Event event) {
            Bukkit.broadcastMessage(event.getEventName());
        }
    }复制代码
看起来没有问题,但是很快你会发现,服务器被CreatureSpawnEvent刷屏了,甚至还夹杂着几个EntitySpawnEvent。
data/myattachment/forum/202310/04/104349vpsp99p1o1vxozxz.png
随即我尝试了更为“常规”的方法,使用PluginManager#registerEvents注册监听器。结果一切正常,看来PluginManager#registerEvent这一方法确实暗藏玄机。
三、深入探究
既然javadoc里没写,那我们只能通过源码研究如何正确使用PluginManager#registerEvent了。
3.1 PluginManager#registerEvents
我们先来看看registerEvents是如何实现的。
首先找到org.bukkit.plugin包下的类PluginManager,发现它是一个接口,spigot-api中其唯一的实现类是org.bukkit.plugin.SimplePluginManager。
SimplePluginManager#registerEvents
    @Override
    public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) {
        if (!plugin.isEnabled()) {
            throw new IllegalPluginAccessException("Plugin attempted to register " + listener + " while not enabled");
        }
        for (Map.Entry, Set> entry : plugin.getPluginLoader().createRegisteredListeners(listener, plugin).entrySet()) {
            getEventListeners(getRegistrationClass(entry.getKey())).registerAll(entry.getValue());
        }
    }复制代码
JavaPluginLoader#createRegisteredListeners
    @Override
    @NotNull
    public Map, Set> createRegisteredListeners(@NotNull Listener listener, @NotNull final Plugin plugin) {
        Validate.notNull(plugin, "Plugin can not be null");
        Validate.notNull(listener, "Listener can not be null");
        boolean useTimings = server.getPluginManager().useTimings();
        Map, Set> ret = new HashMap, Set>();
        Set methods;
        try {
            Method[] publicMethods = listener.getClass().getMethods();
            Method[] privateMethods = listener.getClass().getDeclaredMethods();
            methods = new HashSet(publicMethods.length + privateMethods.length, 1.0f);
            for (Method method : publicMethods) {
                methods.add(method);
            }
            for (Method method : privateMethods) {
                methods.add(method);
            }
        } catch (NoClassDefFoundError e) {
            plugin.getLogger().severe("Plugin " + plugin.getDescription().getFullName() + " has failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist.");
            return ret;
        }
        for (final Method method : methods) {
            final EventHandler eh = method.getAnnotation(EventHandler.class);
            if (eh == null) continue;
            // Do not register bridge or synthetic methods to avoid event duplication
            // Fixes SPIGOT-893
            if (method.isBridge() || method.isSynthetic()) {
                continue;
            }
            final Class checkClass;
            if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) {
                plugin.getLogger().severe(plugin.getDescription().getFullName() + " attempted to register an invalid EventHandler method signature "" + method.toGenericString() + "" in " + listener.getClass());
                continue;
            }
            final Class eventClass = checkClass.asSubclass(Event.class);
            method.setAccessible(true);
            Set eventSet = ret.get(eventClass);
            if (eventSet == null) {
                eventSet = new HashSet();
                ret.put(eventClass, eventSet);
            }
            for (Class clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
                // This loop checks for extending deprecated events
                if (clazz.getAnnotation(Deprecated.class) != null) {
                    Warning warning = clazz.getAnnotation(Warning.class);
                    WarningState warningState = server.getWarningState();
                    if (!warningState.printFor(warning)) {
                        break;
                    }
                    plugin.getLogger().log(
                            Level.WARNING,
                            String.format(
                                    ""%s" has registered a listener for %s on method "%s", but the event is Deprecated. "%s"; please notify the authors %s.",
                                    plugin.getDescription().getFullName(),
                                    clazz.getName(),
                                    method.toGenericString(),
                                    (warning != null && warning.reason().length() != 0) ? warning.reason() : "Server performance will be affected",
                                    Arrays.toString(plugin.getDescription().getAuthors().toArray())),
                            warningState == WarningState.ON ? new AuthorNagException(null) : null);
                    break;
                }
            }
            final CustomTimingsHandler timings = new CustomTimingsHandler("Plugin: " + plugin.getDescription().getFullName() + " Event: " + listener.getClass().getName() + "::" + method.getName() + "(" + eventClass.getSimpleName() + ")", pluginParentTimer); // Spigot
            EventExecutor executor = new EventExecutor() {
                @Override
                public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException {
                    try {
                        if (!eventClass.isAssignableFrom(event.getClass())) {
                            return;
                        }
                        // Spigot start
                        boolean isAsync = event.isAsynchronous();
                        if (!isAsync) timings.startTiming();
                        method.invoke(listener, event);
                        if (!isAsync) timings.stopTiming();
                        // Spigot end
                    } catch (InvocationTargetException ex) {
                        throw new EventException(ex.getCause());
                    } catch (Throwable t) {
                        throw new EventException(t);
                    }
                }
            };
            if (false) { // Spigot - RL handles useTimings check now
                eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
            } else {
                eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
            }
        }
        return ret;
    }复制代码
SimplePluginManager#getEventListeners
data/myattachment/forum/202310/04/113843eafggwftx0j0mtjt.png
SimplePluginManager#getRegistrationClass
data/myattachment/forum/202310/04/121937ym6q375f7eqe13yk.png
HandlerList#registerAll、HandlerList#register
data/myattachment/forum/202310/04/113715lz7r8lzep79njr1e.png
根据这些追溯我们可以得知,使用registerEvents注册监听器经历了以下步骤:
(1)JavaPluginLoader#createRegisteredListeners
获取传入的Listener实现类内的所有方法,将每个方法传入的第一个参数类型作为要监听的事件类根据事件类获取已注册的监听器集合Set对这一事件类及其所有父类(到Event.class为止)进行是否过时(Deprecated)检测新建一个EventExecutor对象,并使用它来新建TimedRegisteredListener或RegisteredListener,再将其放入先前获取的集合中返回一个Map, Set>对象,存储着各个事件的所有监听器
(2)遍历(1)中获取的Map对象的所有元素
获取元素的键(事件类),调用getRegistrationClass函数,检查该事件类或者其任意一个父类是否有getHandlerList方法调用getEventListeners函数,执行事件类的getHandlerList方法,获得HandlerList对象
(3)执行HandlerList#registerAll,传入这一事件类在Map中对应的Set,把集合(Set)里的RegisteredListener全都注册进HandlerList对象。
3.2 PluginManager#registerEvent
下图为registerEvent函数的实现,传入参数过长未截全
data/myattachment/forum/202310/04/124256sv6y0bp1s3ydcbbv.png
我们很容易发现,其实现过程实质上和registerEvents一样,只不过需要用户自行构建Listener、EventExecutor对象并传入。
那么我们应该如何构建Listener、EventExecutor对象呢?
跳转到org.bukkit.event.Listener,接口申明如下:
package org.bukkit.event;
/
* Simple interface for tagging all EventListeners
*/
public interface Listener {}
复制代码
我们发现Listener里空空如也,没什么需要我们实现的。
再跳转到org.bukkit.plugin.EventExecutor,接口申明如下:
package org.bukkit.plugin;
import org.bukkit.event.Event;
import org.bukkit.event.EventException;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
/

* Interface which defines the class for event call backs to plugins
*/
public interface EventExecutor {
    public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException;
}复制代码
可以看到,我们需要在EventExecutor实现类中实现execute函数。
JavaPluginLoader#createRegisteredListeners函数内是这样写的:
data/myattachment/forum/202310/04/130416egyaye2jlzgduqi3.png
再对比我们先前的代码:
    static class MyEventExecutor implements EventExecutor {
        public void execute(Listener listener, Event event) {
            Bukkit.broadcastMessage(event.getEventName());
        }
    }复制代码
很明显,这句多出来的判断十有八.九是问题的关键了:
if (!eventClass.isAssignableFrom(event.getClass())) {
    return;
}复制代码
我喜欢把isAssignableFrom翻译成“由...装换而来”,A.isAssignableFrom(B)相当于判断A是否是B的父类。(父类可以由子类装换而来)
为了便于理解,再举3个例子:
Object.class.isAssignableFrom(String.class)  ->  true
String.class.isAssignableFrom(String.class)   ->  true
String.class.isAssignableFrom(Object.class)  ->  false
也就是说,实际执行的时候,需要判断当前发生的事件的类是不是EventExecutor先前记录的eventClass的子类。
而我们的写法中没有作这个判断,而因此EntitySpawnEvent(ItemSpawnEvent的父类)、CreatureSpawnEvent(ItemSpawnEvent的兄弟类)发生时也执行了execute函数中的代码。
在前面代码的基础上稍作修改可以得到:
    public static Plugin plugin;
    @Override
    public void onEnable() {
        plugin = this;
        Bukkit.getPluginManager().registerEvent(ItemSpawnEvent.class,
                new MyListener(),
                EventPriority.NORMAL,
                new EventExecutor(){// 可替换成lambda表达式,这里为了方便理解不作简写
                    @Override
                    public void execute(Listener listener, Event event) {
                        if(!ItemSpawnEvent.class.isAssignableFrom(event.getClass())) return;
                        Bukkit.broadcastMessage(event.getEventName());
                    }
                },
                plugin);
    }
    static class MyListener implements Listener {
        MyListener() {
        }
    }复制代码
这次运行起来确实没有问题。
3.3 不同事件为何会串线
根据上述分析,我们知道了使用PluginManager#registerEvent注册监听器时,在EventExecutor里加上判断就没问题,而PluginManager#registerEvents也正是这样做的。
那么问题来了:为什么需要在这里加判断呢?
根据前面对代码的分析,注册监听器时会根据事件获取HandlerList,然后执行HandlerList#register。
再看事件发生时执行的代码:
SimplePluginManager#callEvent
data/myattachment/forum/202310/04/141318pfebt4yoy5ao28hz.png
SimplePluginManager#fireEvent
data/myattachment/forum/202310/04/141541qw738lw7w2pqz79p.png
RegisteredListener#callEvent
data/myattachment/forum/202310/04/142453xh854llzjw5o4jj4.png
根据这些追溯我们可以得知,触发一个事件经历了以下步骤:
判断事件是否为异步事件、当前线程是否为主线程、是否有线程锁等等调用函数fireEvent根据事件获取HandlerList,并获取这个HandlerList下注册的所有监听器遍历所有监听器,若监听器所属插件被加载就调用其callEvent函数若事件没被取消,就执行executor成员的execute函数
也就是说,触发事件的时候,并不是直接按照事件类来调用监听器,而是先获取事件的HandlerList,再根据HandlerList获取监听器。
当我们跳转到ItemSpawnEvent类就会发现,这个事件没有重写getHandlerList方法。
data/myattachment/forum/202310/04/143304bkrwx0u6hiho5hop.png
因此注册ItemSpawnEvent的监听器时,实际上把监听器塞在了其父类EntitySpawnEvent的HandlerList里,它的兄弟类CreatureSpawnEvent亦是如此。
如果不在EventExecutor的execute函数内加以区分,就会导致这几个事件一并串线。
3.4 为什么“EntitySpawnEvent”会被打印4次
在此基础上,本帖前言的问题就不难解释了。
EntitySpawnEvent共有4个未重写getHandleList方法的子类,给这5个类均注册一次监听器,就相当于给EntitySpawnEvent注册了5个监听器。而ItemSpawnEvent恰巧在黑名单内,因此触发一次EntitySpawnEvent将执行4次EventExecutor的execute函数,也就打印了4次“EntitySpawnEvent”。
至此,问题全都圆满解决。
四、总结
由于注册好的监听器是存储在事件类的HandleList中,而EntitySpawnEvent的4个子类均未重写getHandleList方法,导致这5个事件的监听器实际上都存储在一起。
因此,如果不在EventExecutor的execute函数内加以区分,就会导致这几个事件一并串线。
最后,感谢@Neige在“不同事件为何会串线”这一问题上对我错误见解的纠正。