Skip to content

模块事件总线 ​

自 v6.2.2 起

模块事件总线从 UltiTools-API v6.2.2 开始可用。

UltiTools 提供了一套解耦的发布/订阅事件系统,用于模块间通信。模块可以发布事件并订阅来自其他模块的事件,而无需模块之间的直接依赖。

定义事件 ​

通过继承 ModuleEvent 创建自定义事件:

java
package com.ultikits.docs.eventbus;

import com.ultikits.ultitools.events.ModuleEvent;
import lombok.Getter;

import java.util.UUID;

public class BalanceChangeEvent extends ModuleEvent {
    @Getter private final UUID player;
    @Getter private final double amount;

    public BalanceChangeEvent(UUID player, double amount) {
        this.player = player;
        this.amount = amount;
    }
}

每个 ModuleEvent 自动携带:

  • sourceModule — 发布事件的模块名称(通过 setSourceModule() 设置)
  • timestamp — 事件创建的时间戳(System.currentTimeMillis())

注解方式处理器 ​

在任何托管 Bean 中使用 @ModuleEventHandler 标记方法。框架会自动发现并注册:

java
package com.ultikits.docs.eventbus;

import com.ultikits.ultitools.annotations.ModuleEventHandler;
import com.ultikits.ultitools.annotations.Service;
import org.bukkit.Bukkit;

@Service
public class AuditService {

    @ModuleEventHandler
    public void onBalanceChange(BalanceChangeEvent event) {
        Bukkit.getLogger().info(
            event.getSourceModule() + " changed balance for "
            + event.getPlayer() + " by " + event.getAmount()
        );
    }
}

方法必须是 public 的,且必须有且仅有一个继承 ModuleEvent 的参数。

注解属性 ​

属性类型默认值说明
priorityEventPriorityNORMAL执行顺序 — LOWEST 最先执行,MONITOR 最后执行
ignoreCancelledbooleanfalse如果为 true,当事件被取消后跳过此处理器
java
@ModuleEventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onHighPriority(BalanceChangeEvent event) {
    // 在 NORMAL 处理器之后执行,如果事件被取消则跳过
}

优先级顺序 ​

处理器按优先级从低到高顺序执行:

LOWEST → LOW → NORMAL → HIGH → HIGHEST → MONITOR

MONITOR 优先级

使用 MONITOR 进行只读观察(日志、统计)。在此优先级避免修改事件状态。

编程式订阅 ​

对于动态订阅(如临时监听器),可以直接使用 EventBus API:

java
EventBus eventBus = UltiTools.getInstance().getEventBus();

// 简单订阅
Subscription sub = eventBus.subscribe(BalanceChangeEvent.class, event -> {
    player.sendMessage("余额变动: " + event.getAmount());
});

// 稍后不再需要时取消订阅
sub.unsubscribe();

也支持完整选项:

java
Subscription sub = eventBus.subscribe(
    BalanceChangeEvent.class,
    EventPriority.HIGH,
    true,       // ignoreCancelled
    "MyModule", // 所属模块名
    event -> { /* 处理器 */ }
);

返回的 Subscription 提供:

  • unsubscribe() — 停止接收事件(可安全多次调用)
  • isActive() — 检查订阅是否仍然活跃

发布事件 ​

同步发布 ​

处理器在调用线程上按优先级顺序执行:

java
EventBus eventBus = UltiTools.getInstance().getEventBus();

BalanceChangeEvent event = new BalanceChangeEvent(player, 100.0);
event.setSourceModule("UltiEconomy");
eventBus.publish(event);

异步发布 ​

处理器在后台线程池上执行:

java
eventBus.publishAsync(event);

异步限制

  • 可取消事件不能异步发布 — 框架会抛出 IllegalArgumentException
  • 异步处理器不能调用大多数 Bukkit API 方法(使用 Bukkit.getScheduler().runTask() 切回主线程)

可取消事件 ​

实现 Cancellable 接口允许处理器阻止后续处理:

java
package com.ultikits.docs.eventbus;

import com.ultikits.ultitools.events.Cancellable;
import com.ultikits.ultitools.events.ModuleEvent;

public class PlayerTradeEvent extends ModuleEvent implements Cancellable {
    private boolean cancelled;

    @Override
    public boolean isCancelled() { return cancelled; }

    @Override
    public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }

    // ... your event data
}

处理器可以取消事件:

java
@ModuleEventHandler(priority = EventPriority.LOW)
public void onTrade(PlayerTradeEvent event) {
    if (isBanned(event.getTrader())) {
        event.setCancelled(true); // 设置 ignoreCancelled=true 的后续处理器将被跳过
    }
}

设置了 ignoreCancelled = true 的处理器在事件被取消后会被跳过。

父类匹配 ​

注册到父事件类型的处理器也会接收子事件:

java
// 捕获所有 ModuleEvent
@ModuleEventHandler
public void onAnyEvent(ModuleEvent event) {
    Bukkit.getLogger().info("事件来自 " + event.getSourceModule());
}

注册到 BalanceChangeEvent 的处理器不会接收其他 ModuleEvent 子类型。

错误隔离 ​

如果某个处理器抛出异常,该异常会被记录,其他处理器继续正常执行。一个有问题的处理器不会影响整个事件链。

自动生命周期 ​

  • 注册:@Service Bean 中的 @ModuleEventHandler 方法在插件模块加载时被发现并注册
  • 清理:当模块卸载时,该模块拥有的所有处理器会被自动注销

对于注解方式的处理器,你不需要手动管理生命周期。

完整示例 ​

经济模块在余额变化时发布事件:

java
// 在 UltiEconomy 模块中
public class BalanceChangeEvent extends ModuleEvent {
    @Getter private final UUID player;
    @Getter private final double oldBalance;
    @Getter private final double newBalance;

    public BalanceChangeEvent(UUID player, double oldBalance, double newBalance) {
        this.player = player;
        this.oldBalance = oldBalance;
        this.newBalance = newBalance;
    }
}

@Service
public class EconomyService {
    @Autowired
    private UltiToolsPlugin plugin;

    public void setBalance(UUID player, double amount) {
        double old = getBalance(player);
        // ... 更新数据库 ...

        BalanceChangeEvent event = new BalanceChangeEvent(player, old, amount);
        event.setSourceModule(plugin.getPluginName());
        UltiTools.getInstance().getEventBus().publish(event);
    }
}

独立的审计模块无需依赖经济模块即可接收事件:

java
package com.ultikits.docs.eventbus.complete;

import com.ultikits.ultitools.annotations.ModuleEventHandler;
import com.ultikits.ultitools.annotations.Service;
import org.bukkit.Bukkit;

// In a separate audit module
@Service
public class AuditLogService {

    @ModuleEventHandler
    public void onBalanceChange(BalanceChangeEvent event) {
        double delta = event.getNewBalance() - event.getOldBalance();
        Bukkit.getLogger().info(String.format(
            "[Audit] %s balance: %.2f -> %.2f (delta: %+.2f) from %s",
            event.getPlayer(), event.getOldBalance(),
            event.getNewBalance(), delta, event.getSourceModule()
        ));
    }
}

跨模块通信

EventBus 非常适合模块需要对彼此的操作做出反应但又不想紧耦合的场景。 常见例子包括经济变化触发审计日志或通知、玩家传送事件触发区域检查、以及自定义游戏事件协调多个模块。

贡献者

The avatar of contributor named as Ling Bao Ling Bao
The avatar of contributor named as Claude Opus 5.5 (1M context) Claude Opus 5.5 (1M context)

基于 MIT 许可发布