插件基础结构
在把插件丢进真实服务端之前,先搞清楚 Nukkit-MOT 是如何识别、加载并启用插件的。
Nukkit-MOT 如何加载插件
当你把一个 jar 放进 plugins/ 目录后,整体流程大致如下:
PluginManager扫描plugins/目录下的.jar文件JavaPluginLoader从 jar 根目录读取plugin.yml- 加载器根据
main字段找到入口类 - 将这个类实例化为
PluginBase - 调用
onLoad() - 随后在启用阶段调用
onEnable() - 当服务端停止或插件被禁用时调用
onDisable()
如果 depend 中声明的硬依赖缺失,插件不会被加载。
最小工程结构
一个普通 Java 插件建议保持下面这个结构:
hello-world-plugin/
├─ pom.xml
└─ src/
└─ main/
├─ java/
│ └─ com/example/helloworld/HelloWorldPlugin.java
└─ resources/
└─ plugin.yml
对初学者来说,最重要的两条规则是:
- Java 入口类放在
src/main/java/ plugin.yml放在src/main/resources/
plugin.yml 必填字段
根据当前 PluginDescription 的实现,下面这些字段是必填项:
| 字段 | 是否必填 | 作用 |
|---|---|---|
name | 是 | 插件名,同时也会用于插件数据目录名 |
main | 是 | 插件入口类的完整类名,例如 com.example.helloworld.HelloWorldPlugin |
version | 是 | 插件版本字符串,像 "1.0.0" 这样的值建议加引号 |
api | 是 | 支持的 Nukkit API 版本,可以是字符串,也可以是字符串列表。入门阶段推荐写成 ["1.0.0"] |
- 你的主类必须继承
PluginBase - 主类不应该放在
cn.nukkit.*包下 - 打包后
plugin.yml必须位于 jar 根目录
常见可选字段
这些字段对最小 HelloWorld 插件不是必需的,但很快你就会用到:
| 字段 | 用途 |
|---|---|
author / authors | 声明作者或作者列表 |
description | 插件简介 |
website | 项目主页、文档或仓库地址 |
prefix | 插件日志前缀 |
load | 加载时机:STARTUP 或 POSTWORLD |
depend | 硬依赖,缺失时插件无法加载 |
softdepend | 软依赖,缺失时插件仍可继续加载 |
loadbefore | 提示当前插件应先于其他插件加载 |
libraries | 可选。 外部 Maven 坐标(groupId:artifactId:version),Nukkit-MOT 会自动下载并按插件隔离加载。只应列出你的插件代码实际 import 的库。详见自动解析 Maven 库。 |
repositories | 可选。 解析 libraries 时优先于内置默认仓库尝试的额外仓库 URL,建议每个以 / 结尾。仅在用私有 Nexus/Artifactory 或非 Maven Central 的源时才需要;公共坐标无需配置即可解析。 |
commands | 通过 plugin.yml 注册命令元数据 |
permissions | 通过 plugin.yml 注册权限节点 |
load 的默认值是 POSTWORLD。只有在确实需要在世界或某些注册表初始化完成前就运行逻辑时,才使用 STARTUP。
一个更完整的 plugin.yml 示例
前一页那个 HelloWorld 插件实际上只需要前四个字段,但真实项目里常见的写法通常更接近下面这样:
name: HelloWorldPlugin
main: com.example.helloworld.HelloWorldPlugin
version: "1.0.0"
api: ["1.0.0"]
author: YourName
description: A minimal example plugin for Nukkit-MOT
website: https://example.com
prefix: HelloWorld
load: POSTWORLD
softdepend:
- SomeOptionalPlugin
libraries:
- "com.squareup.okhttp3:okhttp:4.12.0"
- "org.xerial:sqlite-jdbc:3.45.1.0"
commands:
helloworld:
description: Send a hello message
usage: "/helloworld"
permission: helloworld.command
permissions:
helloworld.command:
description: Allows the player to use /helloworld
default: true
如果你现在还没有实现命令和权限系统,可以先把 commands 和 permissions 两段删掉。
主类与生命周期
插件入口类通常继承 PluginBase,并覆写一个或多个生命周期方法:
package com.example.helloworld;
import cn.nukkit.plugin.PluginBase;
public final class HelloWorldPlugin extends PluginBase {
@Override
public void onLoad() {
this.getLogger().info("Plugin is loading");
}
@Override
public void onEnable() {
this.getLogger().info("Plugin is enabled");
}
@Override
public void onDisable() {
this.getLogger().info("Plugin is disabled");
}
}
它们通常这样使用:
onLoad():做较早、较轻量的初始化onEnable():做正常启动逻辑,例如注册监听器、命令、任务onDisable():保存状态、清理资源
不要把插件启动逻辑塞进构造方法里,应该让服务端来控制生命周期。
数据目录与资源文件
Nukkit-MOT 会根据插件的 name 创建数据目录,通常是:
plugins/HelloWorldPlugin/
src/main/resources/ 里的文件会被一起打进 jar,所以 plugin.yml 要放这里。后续如果你要加入默认 config.yml,也应该放在这里。
自动解析 Maven 库
与其把大型第三方库 shade(打进)插件 jar 里让体积膨胀,不如直接在 plugin.yml 里声明它们的 Maven 坐标。Nukkit-MOT 会把每个库下载到服务端数据目录下的共享 libraries/ 目录,并在加载时挂到你插件自己的 ClassLoader 上。
libraries 是可选字段。每个声明的坐标(连同传递依赖)都会在插件加载时被下载和解析,无论你的代码是否用到都会拖慢启动、占用磁盘。不要把构建文件里的整个依赖列表直接复制过来 —— 只有当你的插件代码 import 了某个库时,才把它的坐标加进来。
libraries:
- "com.squareup.okhttp3:okhttp:4.12.0"
- "org.xerial:sqlite-jdbc:3.45.1.0"
repositories:
- "https://maven.my-company.com/repository/public/"
- "https://jitpack.io"
解析流程
对每个 groupId:artifactId:version 条目,服务端会:
- 校验坐标,拒绝任何可能逃出
libraries/目录的写法(路径穿越、首尾点、反斜杠、控制字符)。 - 按
repositories顺序依次尝试,全部失败再用内置兜底仓库(Maven Central 和repo.lanink.cn),命中即停止。 - 把
.jar下载到libraries/<group 路径>/<artifact>/<version>/<artifact>-<version>.jar,先写到.tmp临时文件再原子移动到位,避免半成品文件。 - 读取对应的
.pom并递归解析传递依赖,采用nearest-wins(最近优先)策略:同一个groupId:artifactId以第一次见到的版本为准,冲突版本直接跳过而非升级。
已下载的 artifact 会在插件之间复用,所以每个版本的成本只会付出一次。
ClassLoader 隔离
每个插件都有自己独立的 PluginClassLoader。当你的插件加载一个类时:
- 先查插件自己的 URL —— 包括主 jar 和所有
libraries声明的 jar。 - 自己的 URL 找不到时,再回退到全局扫描,因此原有的
depend/softdepend机制依然有效。
也就是说,你在 libraries 里声明的版本会优先于其他插件 shade 进来的同名类。这是版本优先的隔离,不是访问控制 —— 插件之间仍能通过全局回退互相看到对方的类。
支持与不支持的范围
支持:
- 根
<dependencies>中字面量 version、scope 为compile或runtime、且optional != true的条目。 - 通过这些受支持依赖进行的传递解析。
- 通过
repositories声明的自定义仓库。
不支持(POM 解析器刻意保持最小化):
parent继承、dependencyManagement、BOM 导入、exclusions、relocations。- 版本区间、classifier、以及 POM
<version>里的${...}属性占位符。 provided、test、system、import这些 scope —— 一律被过滤掉。
如果某个库需要上述任意一项,请在 libraries 里显式补充坐标,或继续把那个依赖 shade 进你的 jar。
失败行为
- 坐标格式不合法(不是恰好三段以
:分隔,或包含非法字符)会抛出LibraryLoadException,插件不会加载。 - 所有仓库都无法提供 jar 时,插件不会加载。
.pom解析失败不是致命错误:jar 仍然可用,只是丢掉它的传递依赖(会打印一条 warning)。- XML 解析器针对 XXE(外部实体、DTD、schema)做了加固 —— 恶意 POM 无法通过解析器访问网络或读取本地文件。
对于 Maven Central 上的热门库,优先用 libraries 而非 shade。repositories 留给私有的 Nexus/Artifactory 地址即可,公共坐标通常无需配置即可解析。
继续之前
- 初学者最常见的加载失败原因就是
plugin.yml缺失,或者main类名写错 - 当
libraries解析失败时,服务端日志会打印出错的坐标以及尝试过的仓库 —— 在改回 shade 方案之前,先检查 URL 和网络 - 如果结构和元数据都已经确认无误,就继续阅读运行并调试你的插件