Maven
排除文件
从 Maven 构建日志中可以看到,构建过程成功完成了,但存在一些警告。以下是对构建过程的解释及其中出现的警告信息的详细分析:
构建日志解析
项目构建信息:
Maven 正在构建
cc.aquaoh:AquaTeam
项目,版本为1.0-SNAPSHOT
。项目类型为
jar
(即它将生成一个.jar
文件)。
资源复制:
resources
插件成功将资源从src/main/resources
复制到target/classes
目录。日志显示复制了 4 个资源文件。
编译信息:
compiler
插件没有发现新的需要编译的类,所有类文件都已是最新状态(Nothing to compile - all classes are up to date
)。
测试资源与测试编译:
testResources
插件未找到任何测试资源目录,因此跳过了测试资源的复制。testCompile
插件表示没有需要编译的测试源代码。
测试执行:
surefire
插件没有发现要运行的测试(No tests to run
)。
打包 JAR 文件:
jar
插件成功构建了AquaTeam-1.0-SNAPSHOT.jar
文件,并将其放置在target
目录中。
Maven Shade 插件:
shade
插件用于创建一个“uber” JAR 文件,包含项目及其所有依赖项。成功将nametagedit
库包含在“shaded” JAR 文件中。创建的 JAR 文件名为
AquaTeam-1.0-SNAPSHOT-shaded.jar
,替换了原始的AquaTeam-1.0-SNAPSHOT.jar
。
警告信息
构建过程中出现了一些警告:
无效的 POM 警告:
The POM for com.nametagedit:nametagedit:jar:4.5.16 is invalid, transitive dependencies (if any) will not be available
。这个警告表示
nametagedit
依赖的 POM 文件无效,可能会导致其传递依赖(如果有的话)不可用。这通常不会影响基本的功能,但最好检查nametagedit
库的依赖树,确保它的依赖关系正确解析。
资源文件重叠警告:
maven-shade-plugin
插件检测到多个 JAR 文件(项目和nametagedit
库)中定义了重复的资源文件:META-INF/MANIFEST.MF
config.yml
plugin.yml
当多个 JAR 文件包含相同的文件时,
maven-shade-plugin
只会复制其中一个文件到最终的“uber” JAR 中。这可能导致某些配置文件丢失或被覆盖。
解决这些警告的建议
检查无效 POM 警告:
可以使用以下命令查看依赖树和详情:
mvn dependency:tree -Ddetail=true
查找和
nametagedit
相关的传递依赖并确保它们都已正确定义。
解决资源文件重叠问题:
使用
maven-shade-plugin
的<filters>
配置来排除或重命名重复的资源文件。下面是一个示例:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <filters> <filter> <artifact>com.nametagedit:nametagedit</artifact> <excludes> <exclude>META-INF/MANIFEST.MF</exclude> <exclude>config.yml</exclude> <exclude>plugin.yml</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin> </plugins> </build>
上述配置将排除
nametagedit
JAR 文件中的重复资源文件,从而避免它们在最终的 JAR 文件中被覆盖。
总结
构建过程已成功完成,但你可能需要解决一些依赖项和资源文件的问题,以确保生成的 JAR 文件在运行时没有潜在的问题。通过检查依赖树和使用 maven-shade-plugin
的过滤器配置,你可以更好地管理项目的构建和打包过程。
Java
泛型
Java 中的泛型(两万字超全详解)_java 泛型-CSDN博客
YAML
YAML语法 - 天生帅才 - 博客园 (cnblogs.com)
语法结构
YAML 基本语法规则如下。
大小写敏感
使用缩进表示层级关系,缩进时只允许使用空格,不允许使用 Tab 键
缩进的空格数目不重要,只要相同层级的元素左侧对齐即可,建议至少 2 个空格
符号 # 表示注释,从这个字符一直到行尾,都会被解析器忽略。
YAML 支持的数据结构有三种
对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
纯量(scalars):单个的、不可再分的值
YAML 对象写法
yaml 基础对象写法
一组键值对使用冒号结构隔开,冒号后面要加一个空格
key: value
实例演示:简单的示例
---
value0: 'hello World!'
value1: "hello World!"
value2: hello World!
-------------------------------
# 转为 JavaScript 格式
{ value0: 'hello World!',
value1: 'hello World!',
value2: 'hello World!' }
# 转换为 json 格式
{
"value0": "hello World!",
"value1": "hello World!",
"value2": "hello World!"
}
单行写法
使用以下样式,将所有键值对写成一个行内对象
key: { key1: value1, key2: value2, ...}
实例演示:
key: { name: zuiyoujie, age: 20 }
多行写法
使用换行和缩进的写法可以清晰展示层级关系
key:
name: zuiyoujie
age: 20
单行和多行写法结果一样,且都可以转换格式:
# 转换转为 JavaScript 格式
{ key: { name: 'zuiyoujie', age: 20 } }
# 转换为 json 格式
{
"key": {
"name": "zuiyoujie",
"age": 20
}
}
复杂对象格式的写法
使用问号加空格代表一个复杂的 key,使用一个冒号加空格代表一个复杂的 value
意思是对象的属性是一个数组 [complexkey1,complexkey2],对应的值也是一个数组 [complexvalue1,complexvalue2]
?
- complexkey1
- complexkey2
:
- complexvalue1
- complexvalue2
YAML 数组写法
yaml 单个数组写法
以连字符 - 开头的行(一组数据)表示构成一个数组:
列表中的所有成员都开始于相同的缩进级别,比如IP列表,省市列表
key: [ value1, value2, ...]
实例演示:yaml 表示一个列表
# 单行写法:
china: [ 'beijing', 'shanxi', 'hebei' ]
# 多行写法:
china:
- beijing
- shanxi
- hebei
----------------------------
# 转为 JavaScript 格式
{ china: [ 'beijing', 'shanxi', 'hebei' ] }
# 转为 json 格式
{
"china": [
"beijing",
"shanxi",
"hebei"
]
}
yaml 多数组写法
两个数组的例子:相对复杂,yaml 表示列表和字典
# 单行写法:
china: [ { beijing: 222, tianjin: 333, hebei: 444 },{ shanxi: 222, shandong: 333 } ]
# 多行写法:
china:
- beijing: 222 # 可以在连字符后直接写参数
tianjin: 333
hebei: 444
- # 也可以连字符分隔,换行写参数
shanxi: 222
shandong: 333
--------------------------
# 转换为js格式
{ china:
[ { beijing: 222, tianjin: 333, hebei: 444 },
{ shanxi: 222, shandong: 333 } ] }
# 转换为 json 格式
{
"china": [
{
"beijing": 222,
"tianjin": 333,
"hebei": 444
},
{
"shanxi": 222,
"shandong": 333
}
]
}
意思是 china 这个数组包含两个数组,数组元素又是由 beijing,tianjin,hebei 和 shanxi,shandong 两个数组组成
yaml 多维数组写法
数据结构的子成员是一个数组,则可以在该项下面缩进一个空格
china:
- beijing:
- changping: 555
- haidian: 777
- shanxi:
- xinzhou: 200
taiyuan: 300 # 同一数组数据的连字符可以省略
datong:
- hebei:
- cangzhou: 500
xiongan:
-------------------------------
# 转为 JavaScript 格式
{ china:
[ { beijing: [ { changping: 555 }, { haidian: 777 } ] },
{ shanxi: [ { xinzhou: 200, taiyuan: 300, datong: null } ] },
{ hebei: [ { cangzhou: 500, xiongan: null } ] } ] }
# 转换为 json 格式
{
"china": [
{
"beijing": [
{
"changping": 555
},
{
"haidian": 777
}
]
},
{
"shanxi": [
{
"xinzhou": 200,
"taiyuan": 300,
"datong": null
}
]
},
{
"hebei": [
{
"cangzhou": 500,
"xiongan": null
}
]
}
]
}
YAML 复合结构
数组和对象可以结合使用,形成复合结构。
languages: # 值是三个数组
- Ruby
- Perl
- Python
websites: # 值是三个键值对,也就是一个列表或者字典
YAML: yaml.org
Ruby: ruby-lang.org
Python: python.org
Perl: use.perl.org
转为 JavaScript 如下
{ languages: [ 'Ruby', 'Perl', 'Python' ],
websites:
{ YAML: 'yaml.org',
Ruby: 'ruby-lang.org',
Python: 'python.org',
Perl: 'use.perl.org' } }
数据结构
boolean:
- TRUE #true,True都可以
- FALSE #false,False都可以
float:
- 3.14
- 6.8523015e+5 #可以使用科学计数法
int:
- 123
- 0b1010_0111_0100_1010_1110 #二进制表示
null:
nodeName: 'node'
parent: ~ #使用~表示null
string:
- 哈哈
- 'Hello world' #可以使用双引号或者单引号包裹特殊字符
- newline
newline2 #字符串可以拆成多行,每一行会被转化成一个空格
date:
- 2018-02-17 #日期必须使用ISO 8601格式,即yyyy-MM-dd
datetime:
- 2018-02-17T15:02:31+08:00 #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区
-----------------------------
# 转换为 JavaScript 格式
{ boolean: [ true, false ],
float: [ 3.14, 685230.15 ],
int: [ 123, 685230 ],
null: { nodeName: 'node', parent: null },
string: [ '哈哈', 'Hello world', 'newline newline2' ],
date: [ Sat Feb 17 2018 08:00:00 GMT+0800 (中国标准时间) ],
datetime: [ Sat Feb 17 2018 15:02:31 GMT+0800 (中国标准时间) ] }
# 转换为 JSON 格式
{
"boolean": [
true,
false
],
"float": [
3.14,
685230.15
],
"int": [
123,
685230
],
"null": {
"nodeName": "node",
"parent": null
},
"string": [
"哈哈",
"Hello world",
"newline newline2"
],
"date": [
"2018-02-17T00:00:00.000Z"
],
"datetime": [
"2018-02-17T07:02:31.000Z"
]
}
处理字符串
默认字符串写法
字符串默认可以不加引号
str: 这是一行字符串
--------------------------
# 转为 JavaScript 如下
{ str: '这是一行字符串' }
# 转换为 JSON 格式
{
"str": "这是一行字符串"
}
处理特殊字符
如果字符串之中包含空格或特殊字符,需要放在引号之中
str: '内容: 字符串'
--------------------------
# 转为 JavaScript 格式
{ str: '内容: 字符串' }
# 转换为 JSON 格式
{
"str": "内容: 字符串"
}
处理引号
单引号和双引号都可以使用
单引号内的所有内容将被按字面意思处理,特殊字符不会被转义
双引号内的内容可以包含转义字符。YAML 会对一些特殊字符进行转义。
key: 'This is a string with a "quote" and a \ backslash.'
key: "This is a string with a \"quote\" and a newline\ncharacter."
path: 'C:\Program Files\Example'
message: "Line 1\nLine 2"
单引号之中如果还有单引号,必须连续使用两个单引号转义。
example: 'It''s a single-quoted string.'
处理多行字符串
字符串可以写成多行,从第二行开始,必须至少有一个空格缩进,换行符会被转为空格。
str: 这是一段
多行
字符串
-----------------------------
# 转为 JavaScript 格式
{ str: '这是一段 多行 字符串' }
# 转换为 JSON 格式
{
"str": "这是一段 多行 字符串"
}
|
:保留所有换行符,包括末尾的空行。>
:折叠换行符,将多个连续的换行符转换为一个空格。|-
:保留前面有数据的行的换行符,去掉最后的空行。|+
:保留所有换行符,包括没有数据的空行。
+
表示保留文字块末尾的换行,-
表示删除字符串末尾的换行
1. s1: |
s1: |
Foo
Bar
Noo
|
是字面(literal)块指示符,它表示要保留换行符。每一行的换行符都会被保留,包括文本结束后的空行。
在你的示例中,有 3 个文本行之后还有 3 个空行。这些空行都会保留下来。
因此,最后的 YAML 结果会有 3 个换行符(空行)。
2. s2: >
s2: >
Foo
Bar
Noo
>
是折叠(folded)块指示符,它会将换行符折叠为空格,除非前后两行之间存在一个或多个空行。所有的换行符(包括空行)都被折叠为一个空格。因此,这里不会保留任何换行符。
输出的文本会是
"Foo Bar Noo"
。
3. s3: |-
s3: |-
Foo
Bar
Noo
|-
表示保留文本行的换行符,但删除文本结尾的所有空行。因此,只保留有内容的 3 行文本的换行符,而空行将被移除。
输出结果没有额外的换行符。
4. s4: |+
s4: |+
Foo
|+
表示保留所有换行符,包括文本结尾的所有空行。在这个示例中,
Foo
后面有 3 个空行,这些空行都会被保留下来。因此,结果中会有 3 个换行符。
总结
{
"s1": "Foo\nBar\nNoo\n",
"s2": "Foo Bar Noo\n",
"s3": "Foo\nBar\nNoo",
"s4": "Foo\n\n\n"
}
Java With YAML
读取多行对象
matchMenu.yml
# 装饰边框
fill:
material: LIGHT_GRAY_STAINED_GLASS_PANE
amount: 1
直接按照节点名获取
matchMenuCfg.getString("fill.material");
Bukkit
调度分配
在 Bukkit 中,调度器(Scheduler)用于安排和执行异步或同步任务。以下是常见的 Bukkit 调度器用法及语法示例:
1. 安排同步任务(主线程执行)
// 安排一个延迟任务 Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { @Override public void run() { // 在这里执行任务代码 player.sendMessage("任务延迟后执行"); } }, 20L); // 20 ticks = 1 second // 安排一个重复任务 Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, new Runnable() { @Override public void run() { // 重复执行的任务代码 player.sendMessage("每秒发送一次消息"); } }, 0L, 20L); // 立即执行,并每隔20 ticks(1秒)重复执行
2. 安排异步任务(独立线程执行)
异步任务在独立线程中执行,不会阻塞主线程,适用于执行较长时间的任务(如数据库操作)。
// 安排一个异步任务 Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() { @Override public void run() { // 在这里执行异步任务代码 // 注意:异步任务不能直接操作 Bukkit API player.sendMessage("异步任务执行"); } });
3. 取消任务
可以通过任务 ID 或
BukkitRunnable
实例取消已安排的任务。// 获取任务ID int taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, new Runnable() { @Override public void run() { // 重复执行的任务代码 } }, 0L, 20L); // 取消任务 Bukkit.getScheduler().cancelTask(taskId); // 使用 BukkitRunnable 取消任务 BukkitRunnable task = new BukkitRunnable() { @Override public void run() { // 执行任务 } }; // 安排任务 task.runTaskTimer(plugin, 0L, 20L); // 取消任务 task.cancel();
4. 使用
BukkitRunnable
创建任务
BukkitRunnable
提供了一种更面向对象的方式创建任务,并且支持直接调用cancel()
方法来终止任务。new BukkitRunnable() { @Override public void run() { player.sendMessage("BukkitRunnable 任务执行"); } }.runTaskLater(plugin, 20L); // 延迟1秒执行
这些是使用 Bukkit 调度器的基本语法示例,可以根据你的插件需求安排和管理任务。
是的,
BukkitRunnable
也可以用于安排重复任务。你可以使用runTaskTimer
方法来安排一个重复执行的任务。这个方法允许你设置一个初始延迟和一个重复的间隔。以下是如何使用
BukkitRunnable
安排重复任务的示例:new BukkitRunnable() { @Override public void run() { player.sendMessage("BukkitRunnable 定时任务执行"); } }.runTaskTimer(plugin, 0L, 20L); // 立即执行一次,并每隔20 ticks(1秒)重复执行
参数说明:
plugin
: 你的插件实例。
0L
: 初始延迟时间(这里为 0,即立即执行)。
20L
: 重复间隔(这里是 20 ticks,即 1 秒钟)。这样,任务会在第一次立即执行,之后每隔 1 秒钟重复执行一次。如果需要取消这个重复任务,可以调用
BukkitRunnable
的cancel()
方法:取消重复任务的示例:
BukkitRunnable task = new BukkitRunnable() { @Override public void run() { player.sendMessage("BukkitRunnable 定时任务执行"); } }; // 安排任务 task.runTaskTimer(plugin, 0L, 20L); // 需要取消任务时调用 task.cancel();
这种方式方便管理定时任务的生命周期,并且可以灵活地取消任务。
cancel()
方法是BukkitRunnable
中用于取消已经安排的任务的一个关键方法。这个方法可以在任务的执行过程中或其他情况下调用,以停止任务的进一步执行。下面详细讲解cancel()
方法的作用及如何使用。1.
cancel()
方法的作用
cancel()
方法的主要功能是:
停止定时任务的进一步执行。无论是定时任务、延迟任务还是重复任务,一旦调用了
cancel()
方法,任务将不再被执行。这个方法只能由任务的执行者调用(即
BukkitRunnable
实例),不能直接在调度器上使用。2.
cancel()
的使用示例示例 1:在任务内部取消自身
你可以在任务内部的某个条件触发时调用
cancel()
方法,从而停止任务的执行。例如,假设你有一个计数器,当计数到达特定数值时,你希望停止任务:new BukkitRunnable() { int counter = 0; @Override public void run() { player.sendMessage("计数: " + counter); counter++; // 当计数达到5时,停止任务 if (counter >= 5) { this.cancel(); // 取消任务 player.sendMessage("任务已取消"); } } }.runTaskTimer(plugin, 0L, 20L); // 每秒执行一次
在这个例子中,任务会每秒执行一次,并且在计数到 5 时,通过
this.cancel()
来取消任务,停止任务的进一步执行。示例 2:在外部取消任务
你也可以在任务外部调用
cancel()
方法来取消任务。例如,如果你希望在某个条件下从外部取消任务,你可以保存任务的引用,然后调用cancel()
:// 创建并保存任务引用 BukkitRunnable task = new BukkitRunnable() { @Override public void run() { player.sendMessage("任务正在执行"); } }; // 安排任务 task.runTaskTimer(plugin, 0L, 20L); // 每秒执行一次 // 在某个时刻需要取消任务 task.cancel();
在这个示例中,任务会开始每秒执行一次,但当你调用
task.cancel()
时,任务会立即被取消,后续不会再执行。3.
cancel()
与调度器中的任务ID当你使用
BukkitRunnable
时,你可以不必手动管理任务的 ID。调度器内部会自动管理任务的 ID,并且你可以通过BukkitRunnable
的cancel()
方法来轻松取消任务,而不需要通过调度器的任务 ID 来取消。不过,如果你通过BukkitScheduler
安排任务,你可以使用任务 ID 取消任务:int taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, new Runnable() { @Override public void run() { player.sendMessage("重复任务执行"); } }, 0L, 20L); // 使用任务ID取消任务 Bukkit.getScheduler().cancelTask(taskId);
4.
cancel()
的注意事项
任务一旦取消,就不能重新开始。如果你想再次运行该任务,必须重新创建一个新的
BukkitRunnable
实例。异步任务也可以通过
cancel()
方法取消,但需要注意异步任务不能直接与 Bukkit API 交互(例如不能直接操作玩家对象),否则会导致线程安全问题。5. 常见用途
在任务达到某个状态或条件时停止执行(如计时器、循环次数)。
当插件或系统需要在某个事件发生时强制停止某个任务(例如插件卸载时)。
动态控制任务的生命周期,减少资源浪费,避免不必要的任务运行。
通过
cancel()
方法,你可以更灵活地控制BukkitRunnable
的任务执行,使你的插件在任务管理上更高效。是的,默认情况下,
BukkitRunnable
运行在主线程(也称为服务器线程)上。当你使用runTask()
,runTaskLater()
或runTaskTimer()
方法时,任务是同步执行的,意味着它会在主线程上运行。默认情况下
BukkitRunnable
是同步任务以下是一个默认同步任务的示例:
new BukkitRunnable() { @Override public void run() { player.sendMessage("这是一个同步任务,运行在主线程上"); } }.runTask(plugin); // 任务同步执行
这个任务是同步的,因为它使用了
runTask()
方法,因此它会在主线程上执行。同步任务适合那些需要与 Bukkit API 直接交互的操作,比如修改世界、与玩家交互等。异步任务
如果你希望让
BukkitRunnable
运行在异步线程(非主线程)上,你需要使用runTaskAsynchronously()
方法。这通常用于那些耗时较长且不需要直接与 Bukkit API 交互的任务,如文件读取、数据库操作等。异步任务示例:
new BukkitRunnable() { @Override public void run() { // 异步执行任务,不应直接与 Bukkit API 交互 someLongRunningTask(); } }.runTaskAsynchronously(plugin); // 任务异步执行
总结:
同步任务:默认情况下,
BukkitRunnable
是同步执行的,也就是说,它运行在主线程上,适用于与 Bukkit API 直接交互的任务。异步任务:如果需要让
BukkitRunnable
在异步线程上运行,则需要显式调用runTaskAsynchronously()
方法,适合需要在后台执行的长时间任务。需要注意的是,异步任务不应该直接操作 Bukkit API,因为这可能会导致线程安全问题。如果你需要从异步任务中修改游戏对象,可以使用
Bukkit.getScheduler().runTask()
来将部分代码调回主线程执行。在 Bukkit 中,如果你在异步线程(比如通过
runTaskAsynchronously()
启动的任务)里需要执行一个与 Bukkit API 相关的同步操作(如操作玩家对象、修改世界),你需要将该操作切换回主线程执行。为此,Bukkit 提供了一种机制,允许你在异步任务中安排同步任务。你可以使用
Bukkit.getScheduler().runTask()
方法将一个同步任务从异步任务中调度回主线程执行。示例:在异步任务中执行同步任务
new BukkitRunnable() { @Override public void run() { // 这是一个异步任务 someLongRunningTask(); // 长时间运行的任务,适合异步执行 // 在异步任务里,安排一个同步任务 Bukkit.getScheduler().runTask(plugin, new Runnable() { @Override public void run() { // 这是一个同步任务,将在主线程执行 player.sendMessage("此消息来自同步任务,运行在主线程上"); } }); } }.runTaskAsynchronously(plugin); // 启动异步任务
详细解释:
异步任务:异步任务在
runTaskAsynchronously()
内部调用,适合耗时较长的任务,如文件IO或数据库查询。同步任务:当你在异步任务中想要进行与 Bukkit API 相关的操作时,你需要使用
Bukkit.getScheduler().runTask()
。这会将任务提交到主线程(同步线程)执行,确保线程安全。总结:
异步任务适合执行不涉及 Bukkit API 的任务,因为 Bukkit API 是非线程安全的。
如果需要在异步任务中调用 Bukkit API,需要使用
Bukkit.getScheduler().runTask()
将代码切换回主线程来执行。通过这种方法,可以安全地在异步任务中执行主线程上的操作,从而避免潜在的线程安全问题。
在 Bukkit 中,你可以使用异步任务来挂起一个线程,然后通过某种机制在合适的时机唤醒它。虽然 Java 本身提供了线程的暂停和恢复机制(如
wait()
和notify()
),但是在 Bukkit 中,直接使用这些方法可能不是最好的选择,特别是因为 Bukkit 的线程管理是基于 Minecraft 服务器的架构,你需要确保不会阻塞主线程(服务器线程),以免影响游戏的运行。在 Bukkit 中挂起和恢复线程的常见方法:
异步任务与同步任务的结合
当你想让某个任务挂起并等待某种触发条件时,你可以使用 Bukkit 的异步任务来处理耗时的操作,而在异步任务中等待触发条件满足时,可以通过 Bukkit 的调度器将某些任务切换回主线程执行。
示例:使用
BukkitRunnable
和wait()/notify()
在异步任务中暂停和唤醒线程假设你有一个异步任务,它在某个触发条件之前需要暂停,直到另一个线程调用
notify()
来唤醒它。在 Bukkit 中可以这样实现:示例代码:
public class PauseResumeTask { private final Object lock = new Object(); private boolean isPaused = true; // 异步任务 - 挂起线程,直到被唤醒 public void runAsyncTask() { new BukkitRunnable() { @Override public void run() { try { synchronized (lock) { while (isPaused) { lock.wait(); // 线程挂起,等待被唤醒 } } // 继续执行任务 Bukkit.getScheduler().runTask(plugin, () -> { // 在主线程上执行某些与 Bukkit API 相关的任务 Bukkit.broadcastMessage("任务恢复并继续执行!"); }); } catch (InterruptedException e) { e.printStackTrace(); } } }.runTaskAsynchronously(plugin); // 启动异步任务 } // 调用此方法唤醒线程 public void resumeTask() { synchronized (lock) { isPaused = false; lock.notify(); // 唤醒挂起的线程 } } }
详细解释:
runAsyncTask()
:这是一个异步任务,使用runTaskAsynchronously()
启动。任务开始后进入一个while
循环并通过lock.wait()
挂起,等待另一个线程调用resumeTask()
来唤醒它。
resumeTask()
:这个方法通过调用lock.notify()
唤醒挂起的线程。在resumeTask()
中,我们将isPaused
设置为false
,从而让任务继续执行。
runTask()
:当任务被唤醒后,我们使用Bukkit.getScheduler().runTask()
在主线程上执行与 Bukkit API 相关的代码。这是因为 Minecraft 的 Bukkit API 只能在主线程上操作,异步线程不能直接修改游戏世界或与玩家交互。2. 使用
CountDownLatch
在 Bukkit 中挂起线程
CountDownLatch
是另一种控制线程等待的工具,它可以在多个线程之间设置同步点。线程在调用await()
时会挂起,直到CountDownLatch
的计数器变为 0。示例代码:
import java.util.concurrent.CountDownLatch; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; public class CountDownLatchExample extends JavaPlugin { private CountDownLatch latch = new CountDownLatch(1); // 异步任务 public void runAsyncTask() { new BukkitRunnable() { @Override public void run() { try { // 挂起线程,直到 latch 计数为0 latch.await(); // 唤醒后继续任务,切换回主线程 Bukkit.getScheduler().runTask(CountDownLatchExample.this, () -> { Bukkit.broadcastMessage("异步任务恢复并继续执行!"); }); } catch (InterruptedException e) { e.printStackTrace(); } } }.runTaskAsynchronously(this); // 启动异步任务 } // 唤醒挂起的线程 public void resumeTask() { latch.countDown(); // 减少计数,唤醒等待线程 } }
详细解释:
CountDownLatch latch
:初始化时计数为 1,当异步任务运行时,调用latch.await()
挂起任务,等待计数器归零。
latch.await()
:线程在这里挂起,等待另一个线程调用resumeTask()
,从而使计数器减为 0,唤醒任务。
resumeTask()
:通过latch.countDown()
将计数器减少到 0,唤醒正在等待的线程。注意事项:
不要在主线程中挂起:无论你使用哪种机制,不要在主线程中挂起或等待。这样会阻塞服务器线程,导致整个游戏卡顿甚至崩溃。所有的长时间任务、等待和挂起操作都应在异步线程中进行。
线程安全性:如果你需要在异步线程中修改 Bukkit API(如修改玩家状态、物品、世界块等),你必须确保这些操作在主线程中进行。可以使用
Bukkit.getScheduler().runTask()
将代码切换回主线程。总结:
在 Bukkit 中,你可以使用标准的 Java 同步机制(如
wait()/notify()
或CountDownLatch
)来挂起线程,并在合适的时候唤醒它。同时,你可以使用Bukkit.getScheduler().runTask()
方法将某些代码从异步任务中切换回主线程执行,以确保线程安全和服务器性能。跨类唤醒
在 Bukkit 中,如果你希望跨类实现线程的挂起和唤醒,可以结合 Java 的线程同步机制(如
wait()/notify()
或CountDownLatch
),通过共享对象来协调不同类之间的线程交互。在 Bukkit 的插件开发中,通常会将这些线程操作放在异步任务中,以避免阻塞服务器的主线程。使用
wait()
和notify()
跨类实现线程挂起和唤醒在这个示例中,我们将使用两个类:一个类负责启动异步任务并挂起线程,另一个类负责唤醒该线程。
示例代码:
1. TaskManager 类(负责挂起线程):
public class TaskManager { private final Object lock = new Object(); private boolean isPaused = true; private JavaPlugin plugin; public TaskManager(JavaPlugin plugin) { this.plugin = plugin; } // 启动异步任务并挂起线程 public void startAsyncTask() { new BukkitRunnable() { @Override public void run() { synchronized (lock) { while (isPaused) { try { lock.wait(); // 线程挂起,等待被唤醒 } catch (InterruptedException e) { e.printStackTrace(); } } } // 线程恢复后执行的任务 Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.broadcastMessage("异步任务恢复并继续执行!"); }); } }.runTaskAsynchronously(plugin); // 异步任务启动 } // 设置暂停标志 public void setPaused(boolean paused) { this.isPaused = paused; } // 获取锁对象 public Object getLock() { return lock; } }
2. CommandManager 类(负责唤醒线程):
public class CommandManager implements CommandExecutor { private TaskManager taskManager; public CommandManager(TaskManager taskManager) { this.taskManager = taskManager; } // 实现命令处理,唤醒线程 @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (command.getName().equalsIgnoreCase("resumeTask")) { synchronized (taskManager.getLock()) { taskManager.setPaused(false); taskManager.getLock().notify(); // 唤醒等待的线程 sender.sendMessage("任务已恢复"); } return true; } return false; } }
解释:
TaskManager
类:
这是一个管理任务的类,负责启动异步任务并通过
wait()
挂起线程。
startAsyncTask()
方法使用BukkitRunnable
来创建一个异步任务,任务开始时通过lock.wait()
挂起。当任务被唤醒时,任务的后续逻辑会通过
Bukkit.getScheduler().runTask()
切换回主线程执行与 Bukkit API 相关的操作。通过
setPaused()
和getLock()
来管理线程的状态和锁定机制。
CommandManager
类:
CommandManager
负责监听并处理命令。在玩家或管理员通过命令触发时,唤醒等待的线程。通过
synchronized
语句块锁定TaskManager
中的lock
对象,确保线程同步安全。调用
taskManager.getLock().notify()
来唤醒在TaskManager
中挂起的线程。注册命令:
你需要在
plugin.yml
文件中注册resumeTask
命令:commands: resumeTask: description: 唤醒异步任务
主插件类中的初始化:
在主插件类中初始化
TaskManager
和CommandManager
,并将命令与CommandManager
绑定:public class MyPlugin extends JavaPlugin { private TaskManager taskManager; @Override public void onEnable() { taskManager = new TaskManager(this); taskManager.startAsyncTask(); // 启动异步任务并挂起 // 注册命令 this.getCommand("resumeTask").setExecutor(new CommandManager(taskManager)); } }
流程概述:
TaskManager
启动了一个异步任务,并在任务中通过lock.wait()
挂起线程。玩家或管理员通过命令
/resumeTask
唤醒线程,命令处理由CommandManager
处理。
CommandManager
使用lock.notify()
唤醒线程,使异步任务继续执行。使用
CountDownLatch
实现另一种实现方式是使用
CountDownLatch
。相较于wait()/notify()
,CountDownLatch
更加简洁,并且不需要明确地调用notify()
,只要计数器归零,所有等待的线程都会被唤醒。示例代码:
1. TaskManager 类:
import java.util.concurrent.CountDownLatch; public class TaskManager { private final CountDownLatch latch = new CountDownLatch(1); private JavaPlugin plugin; public TaskManager(JavaPlugin plugin) { this.plugin = plugin; } // 启动异步任务并挂起线程 public void startAsyncTask() { new BukkitRunnable() { @Override public void run() { try { latch.await(); // 线程挂起,等待计数器为 0 } catch (InterruptedException e) { e.printStackTrace(); } // 线程恢复后执行的任务 Bukkit.getScheduler().runTask(plugin, () -> { Bukkit.broadcastMessage("异步任务恢复并继续执行!"); }); } }.runTaskAsynchronously(plugin); // 异步任务启动 } // 唤醒挂起的线程 public void resumeTask() { latch.countDown(); // 计数器减为 0,唤醒线程 } }
2. CommandManager 类:
public class CommandManager implements CommandExecutor { private TaskManager taskManager; public CommandManager(TaskManager taskManager) { this.taskManager = taskManager; } // 实现命令处理,唤醒线程 @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (command.getName().equalsIgnoreCase("resumeTask")) { taskManager.resumeTask(); // 唤醒线程 sender.sendMessage("任务已恢复"); return true; } return false; } }
总结:
使用
wait()/notify()
可以实现较为灵活的线程挂起与唤醒控制,但需要手动管理同步块和锁。
CountDownLatch
提供了一种更简洁的等待机制,通过计数器控制线程的挂起与唤醒,适合简单的线程同步场景。在 Bukkit 中,异步任务和主线程任务的切换需要通过
BukkitScheduler
来确保线程安全,不应该直接从异步线程调用 Bukkit API。
org.bukkit
dispatchCommand
org.bukkit.configuration
Configuration (Spigot-API 1.21-R0.1-SNAPSHOT API 中文文档) (windit.net)
常用
save(File file)
保存到指定路径
getStringList()
返回数组列表
org.bukkit.entity
HumanEntity
设置游戏模式
GameMode.SURVIVAL
: 生存模式
GameMode.CREATIVE
: 创造模式
GameMode.ADVENTURE
: 冒险模式
GameMode.SPECTATOR
: 旁观模式