Repository navigation
Annotations
Kz edited this page Oct 17, 2026
·
13 revisions
用注解声明命令、配置、事件、任务和菜单,在启动处显式注册一次,生命周期跟随插件或组件。
插件只在启动处安装需要的模块,日常业务不再逐次传 Scope:
public final class ExamplePlugin extends KPlugin {
@Override
protected void setup() {
ConfigModule.install(this);
CommandModule.install(this);
ConfigDocument<Settings> settings = configs().load(Settings.class);
commands().register(new PlayerCommands(settings), new AdminCommands(settings));
events().register(new PlayerListener());
tasks().register(new Maintenance());
}
}-
commands()、configs()、events()、tasks()、components()绑定当前插件实例的生命周期。 - 没有全局当前 Scope、线程变量、类路径扫描或自动依赖注入。依赖通过构造器传入。
-
context()在 setup 期间可用,插件构造阶段不可用。 - 业务资源重建后需重新构造依赖对象,不能继续使用旧服务入口。
- 命令只在启动时声明,配置重载不重建含命令的根作用域或组件。
- 现有
setup(Scope)仍可用;新代码选择无参 setup,不要同时覆盖两种入口。 - 高级集成可通过
context().scope()访问底层 Scope。
public final class DungeonFeature extends KComponent {
@OnStart
public void start() { /* 初始化业务资源 */ }
@Override
protected void setup() {
commands().register(new DungeonCommands());
events().register(new DungeonListener());
tasks().register(new Maintenance());
}
@OnStop
public void stop() { /* 关闭业务资源 */ }
}ComponentHandle feature = components().install(new DungeonFeature());
feature.close();| 阶段 | 顺序 |
|---|---|
| 安装 | 检查生命周期方法签名 → attach 上下文 → 调用 @OnStart → 执行 setup |
| 关闭 | 逆序释放 setup 中的注册资源 → 调用 @OnStop
|
| 初始化中途失败 | 走与关闭相同的清理路径 |
-
@OnStop必须能处理只初始化了一部分的状态。 - 每类最多一个有效
@OnStart和一个@OnStop,均为 public、非 static、无参 void。 - 普通对象也可仅通过生命周期注解安装,不必继承 KComponent。
- 同一个 KComponent 实例不能重复安装;重新开启不含命令声明的业务组件应创建新实例。
- 含命令的组件必须在插件启动时安装。关闭只保证命令立即逻辑停用,不支持通过运行时重新安装组件恢复命令。
- 组件不会自动扫描、注册字段里的对象;命令、事件和任务仍在 setup 中显式注册一次。
- 可随配置重建的事件、任务等业务资源,应与启动时声明的命令分开管理。
注解包为 me.kzheart.klib.command.annotation。@Command 与 Bukkit 的 Command 同名,建议显式 import。
- 根名、别名与路由在启动时一次声明。配置重载只更新处理器读取的配置、语言和业务状态。
- 作用域关闭立即停用执行、补全和权限检查;物理节点可能保留到服务端生命周期重建或重启。
- 同一插件不得将相同标签交给 Klib 与其他注册器共同管理。完整规则见 Command 模块。
@Command(value = "dungeonbridge", aliases = {"dbe"}, help = true)
@Permission("bamdungeon.use")
@Check("ready")
public final class DungeonCommands {
@Route({"open", "ui"})
@Description("打开副本菜单")
public void ui(Player player) { /* 打开菜单 */ }
@Route("search <query>")
@Description("查询副本")
public void search(Player player, @Param("query") @Greedy String query) { }
@Route("select <dungeon> <difficulty>")
@Description("选择副本和难度")
public void select(Player player,
@Param("dungeon") String dungeon,
@Param("difficulty") @Suggest("difficulty") String difficulty) { }
@Suggestions("difficulty")
public List<String> difficulties(SuggestionContext context) {
String dungeon = context.get("dungeon", String.class);
return Collections.singletonList(dungeon + "-easy");
}
@CheckHandler("ready")
public void ready() {
// 不可进入时抛出 new CommandRejectedException("数据服务尚未就绪")。
}
}| 方法参数 | 含义 |
|---|---|
Player(无 @Param) |
发送者,自动限制为玩家入口 |
@Param Player |
在线目标玩家 |
CommandSender、CommandContext、CommandCall
|
直接注入 |
| 其他业务参数 | 必须显式写 @Param,不依赖 -parameters
|
- 支持的参数类型:String、int/Integer、long/Long、double/Double、boolean/Boolean、UUID、枚举、在线 Player。double 必须有限。
- 贪婪参数必须是末尾 String。
- 可选入口用不同的
@Route方法表达,例如分别声明search和search <query>。 - 共享参数路径必须保持名称、类型和补全声明一致;绑定到不同对象的补全器不能占用同一个共享参数节点。
- 类级
@Permission为默认值,方法级@Permission替换默认值。 - 类级和方法级
@Check累加,按声明顺序去重。 -
@CheckHandler可无参或接收一个CommandCall,只在执行命令时调用,不在补全和帮助中执行。 -
CommandRejectedException是可直接展示的业务拒绝;其他异常仍由命令错误边界记录并提示内部错误。 - 权限用于服务端执行、帮助、Tab 补全和可用的 Brigadier 客户端树。补全不代替业务对象授权。
- 同根命令的多个处理类必须放在同一次
register(playerCommands, adminCommands)中;所有别名共享合并后的完整树。 -
MountedCommand.of("main", questCommands, "quest", "q")把一个处理类的全部路由再挂到/main quest ...与/main q ...下:- 处理类自身的根命令照常注册(同一实例同时传入即可);
- 子命令可写多级,如
"quest data"; - 挂载忽略处理类的
@Command名称、别名与类级@Description,权限、Check 与补全照常生效; - 挂载项须与目标根的处理类放在同一次
register(...)中,不支持嵌套挂载。
- 所有声明方法须 public、非 static。路由须返回 void。
- 继承方法按正常 Java override 规则处理,不隐式继承父类方法注解。
- 注册前校验整个批次:重复路由、缺失参数、未知补全器、错误签名会在触碰 Bukkit 前失败。
- 注册桥接中途失败,会逻辑关闭本批次已成功绑定的命令和别名,不保证物理节点立即清理或恢复原标签。
- 旧 Bukkit 启动注册中,单个尚未成功的注册尝试会清理自己的部分写入。
@Command(value = "mail", help = true) 这类声明会安装自动分页帮助:
-
@Command的help默认是false。设置help = true后,Klib 在合并路由后安装help [page]。 - 同根命令任一处理类启用即可,别名共享合并后的帮助。
- 帮助按发送者权限与玩家限制过滤,不执行业务
@CheckHandler。 -
@Description提供每条命令的用途。
上面的 DungeonCommands 没有空字符串根路由,因此:
| 输入 | 结果 |
|---|---|
/dungeonbridge、/dbe
|
帮助第一页 |
/dbe help 1 |
显式指定第一页 |
/dbe open、/dbe ui
|
打开菜单 |
- 原来用
@Route({"", "open"})打开 GUI 的,改成@Route("open"),移除空字符串路由。 - 启用自动帮助会保留已有根处理器,裸主命令仍会执行该处理器。
- 已显式声明的
@Route("help")也会保留,Klib 不覆盖它,也不另外生成分页入口。 - 挂载处理器的
@Command(help = true)不会替目标根安装帮助。应在目标根的处理类启用,或在程序化根声明中安装CommandBuiltins。程序化写法见 Command 模块。
用一个明确的注册调用同时设置成功和失败回调,避免链式设置期间已经完成的 Future 丢失失败处理器:
@Route("reviews")
public void reviews(CommandCall call) {
call.await(repository.reviews(),
rows -> call.reply("待核对会话: " + rows.size()),
failure -> call.reply("读取失败"));
}- 两种回调均回到主线程,注册所属功能关闭后不再执行。
-
reply对已离线玩家不发送消息。 - 不会撤销已提交的数据库事务,也不会强行终止外部
CompletionStage。
启动时构建的程序化命令也可平铺声明。route 返回路径末尾节点,单参数版本 argument 返回追加的参数节点:
Arg<String> token = Arguments.string("token");
commands().register("dbe", root -> {
root.route("action start").argument(token)
.executes(context -> start(context.get(token)));
root.route("reload").permission("bamdungeon.admin")
.executes(this::reload);
});- 程序化自定义参数用
Arguments.contextual(name, (input, context) -> value, suggestions),可读取发送者和已解析的前置参数。 - 注解内置类型之外的领域解析目前使用此程序化入口,不会自动把任意业务类型当作字符串解析。
ConfigFile 位于 me.kzheart.klib.config.annotation(core 契约),其他配置注解由 config 模块提供。
@ConfigFile("config.yml")
public final class Settings {
@Key("max-party-size")
@Range(min = 1, max = 16)
@Comment("副本允许的最大队伍人数")
public int maxPartySize = 4;
@Validate
public void validate() {
// 跨字段校验,失败时抛出 ConfigValidationException。
}
}-
configs().load(Settings.class)仍返回ConfigDocument;值不偷偷注入业务字段。每次业务操作取一次value()快照。
| 注解 | 规则 |
|---|---|
@Key |
当前对象的一个 YAML 键,不是点分路径;重名映射拒绝加载 |
@Range |
支持数值字段,包括 YAML 缺失时的 Java 默认值;拒绝 null、NaN、无穷和超界值 |
@Validate |
在 POJO 映射完成后执行 |
@Comment |
供显式 ConfigDefaults.yaml(Settings.class) 生成默认 YAML,不会改写正在使用的配置 |
- 映射、范围或业务校验失败,都不会发布新配置,也不会提交文件源的待定修改。
- 普通安装仍从
src/main/resources/defaults/config.yml加载默认文件,不会自动改用注解生成。 - 默认生成支持标量、嵌套 POJO、标量集合和 Map;包含 POJO 的集合默认值请用 YAML 资源描述。
事件复用 Bukkit @EventHandler,通过 events().register(listener) 注册:
- 方法必须 public、非 static、void,且只有一个 Event 子类型参数。
- 保留 priority 和 ignoreCancelled。
- 事件不自动切线程,仍在 Bukkit 派发线程处理。
public final class Maintenance {
@Every(ticks = 20)
public void tick() { /* 主线程 Bukkit 操作 */ }
@Every(ticks = 1200, thread = TaskThread.ASYNC)
public void flush() { /* 不访问 Bukkit 状态 */ }
}tasks().register(instance) 注册固定周期任务:
- ticks 必须大于零,方法为 public、非 static、无参 void。
- 同一任务不重叠执行:异步上次运行未结束时跳过本次触发。
- 功能关闭时取消后续调度;已运行的 I/O 是否响应中断由业务实现决定。
- 动态周期或临时任务继续使用
tasks().every、after、async、thenSync。 - 直接调用带注解的方法仍是普通 Java 调用,不会切线程或启动定时器。
Menus menus = Menus.install(this); // KPlugin 的 setup 中安装一次
menus.open(player, new DungeonMenu());组件内需要独立关闭菜单时用 Menus.install(context(), plugin),不要复用插件根上下文创建的服务。
@Menu(title = "副本列表", layout = {"dd nn"})
public final class DungeonMenu {
private int page;
@Entries('d')
public List<MenuEntry> entries() { return pageEntries(page); }
@Button('n')
public MenuEntry nextButton() { return nextPageButton(); }
@Click('n')
public void next(MenuClick click) {
page++;
click.refresh();
}
}| 注解 / 方法 | 规则 |
|---|---|
@Menu 布局 |
1..6 行,每行 9 字符;空格是空槽,其他字符必须有唯一 Button 或 Entries 提供器 |
@Button |
返回非 null MenuEntry
|
@Click |
接收 MenuClick、返回 void,替代该按钮条目原有的动作 |
@Entries |
返回 List<MenuEntry>,按行优先填充区域;少于容量时留空槽,超出容量时报错,调用方应先分页 |
refresh() |
重新计算模型并渲染原物品栏,不创建新会话、不重复注册事件;标题和尺寸不能改变 |
- 每次打开创建一个对象;同一服务拒绝同时复用一个活动菜单对象。
- 动态条目保留自己的动作,不再给 Entries 区域额外绑定 Click。
- 模型编译失败时保留原模型;业务对象中已经修改的 page 等字段不会自动回滚。
- 不允许新模型覆盖投放区。菜单关闭后不能再替换模型,会话任务随关闭清理。
注解路由注册、权限与别名、自动帮助、根处理器及自定义帮助的测试见 AnnotatedCommandsTest.java。
- 不引入全包扫描、自动注入、事务代理或通用
@Async。 - 数据事务、物品 Builder、语言消息和外部插件能力继续使用现有显式 API。
- 语言接口注解是可选后续方向,目前没有
@MessageKey或@Placeholder接口代理。