菜单

MC插件开发折腾


发布于 2024-10-28 / 4 阅读 / 0 评论 /
Maven 排除文件 从 Maven 构建日志中可以看到,构建过程成功完成了,但存在一些警告。以下是对构建过程的解释及其中出现的警告信息的详细分析: 构建日志解析

Maven

排除文件

从 Maven 构建日志中可以看到,构建过程成功完成了,但存在一些警告。以下是对构建过程的解释及其中出现的警告信息的详细分析:

构建日志解析

  1. 项目构建信息

    • Maven 正在构建 cc.aquaoh:AquaTeam 项目,版本为 1.0-SNAPSHOT

    • 项目类型为 jar(即它将生成一个 .jar 文件)。

  2. 资源复制

    • resources 插件成功将资源从 src/main/resources 复制到 target/classes 目录。日志显示复制了 4 个资源文件。

  3. 编译信息

    • compiler 插件没有发现新的需要编译的类,所有类文件都已是最新状态(Nothing to compile - all classes are up to date)。

  4. 测试资源与测试编译

    • testResources 插件未找到任何测试资源目录,因此跳过了测试资源的复制。

    • testCompile 插件表示没有需要编译的测试源代码。

  5. 测试执行

    • surefire 插件没有发现要运行的测试(No tests to run)。

  6. 打包 JAR 文件

    • jar 插件成功构建了 AquaTeam-1.0-SNAPSHOT.jar 文件,并将其放置在 target 目录中。

  7. Maven Shade 插件

    • shade 插件用于创建一个“uber” JAR 文件,包含项目及其所有依赖项。成功将 nametagedit 库包含在“shaded” JAR 文件中。

    • 创建的 JAR 文件名为 AquaTeam-1.0-SNAPSHOT-shaded.jar,替换了原始的 AquaTeam-1.0-SNAPSHOT.jar

警告信息

构建过程中出现了一些警告:

  1. 无效的 POM 警告

    • The POM for com.nametagedit:nametagedit:jar:4.5.16 is invalid, transitive dependencies (if any) will not be available

    • 这个警告表示 nametagedit 依赖的 POM 文件无效,可能会导致其传递依赖(如果有的话)不可用。这通常不会影响基本的功能,但最好检查 nametagedit 库的依赖树,确保它的依赖关系正确解析。

  2. 资源文件重叠警告

    • maven-shade-plugin 插件检测到多个 JAR 文件(项目和 nametagedit 库)中定义了重复的资源文件:

      • META-INF/MANIFEST.MF

      • config.yml

      • plugin.yml

    • 当多个 JAR 文件包含相同的文件时,maven-shade-plugin 只会复制其中一个文件到最终的“uber” JAR 中。这可能导致某些配置文件丢失或被覆盖。

解决这些警告的建议

  1. 检查无效 POM 警告

    • 可以使用以下命令查看依赖树和详情:

      mvn dependency:tree -Ddetail=true
    • 查找和 nametagedit 相关的传递依赖并确保它们都已正确定义。

  2. 解决资源文件重叠问题

    • 使用 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 入门教程 | 菜鸟教程 (runoob.com)

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 秒钟重复执行一次。如果需要取消这个重复任务,可以调用 BukkitRunnablecancel() 方法:

取消重复任务的示例:

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,并且你可以通过 BukkitRunnablecancel() 方法来轻松取消任务,而不需要通过调度器的任务 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); // 启动异步任务

详细解释:

  1. 异步任务:异步任务在 runTaskAsynchronously() 内部调用,适合耗时较长的任务,如文件IO或数据库查询。

  2. 同步任务:当你在异步任务中想要进行与 Bukkit API 相关的操作时,你需要使用 Bukkit.getScheduler().runTask()。这会将任务提交到主线程(同步线程)执行,确保线程安全。

总结:

  • 异步任务适合执行不涉及 Bukkit API 的任务,因为 Bukkit API 是非线程安全的。

  • 如果需要在异步任务中调用 Bukkit API,需要使用 Bukkit.getScheduler().runTask() 将代码切换回主线程来执行。

通过这种方法,可以安全地在异步任务中执行主线程上的操作,从而避免潜在的线程安全问题。

在 Bukkit 中,你可以使用异步任务来挂起一个线程,然后通过某种机制在合适的时机唤醒它。虽然 Java 本身提供了线程的暂停和恢复机制(如 wait()notify()),但是在 Bukkit 中,直接使用这些方法可能不是最好的选择,特别是因为 Bukkit 的线程管理是基于 Minecraft 服务器的架构,你需要确保不会阻塞主线程(服务器线程),以免影响游戏的运行。

在 Bukkit 中挂起和恢复线程的常见方法:

  1. 异步任务与同步任务的结合

    • 当你想让某个任务挂起并等待某种触发条件时,你可以使用 Bukkit 的异步任务来处理耗时的操作,而在异步任务中等待触发条件满足时,可以通过 Bukkit 的调度器将某些任务切换回主线程执行。

示例:使用 BukkitRunnablewait()/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(); // 唤醒挂起的线程
        }
    }
}

详细解释:

  1. runAsyncTask():这是一个异步任务,使用 runTaskAsynchronously() 启动。任务开始后进入一个 while 循环并通过 lock.wait() 挂起,等待另一个线程调用 resumeTask() 来唤醒它。

  2. resumeTask():这个方法通过调用 lock.notify() 唤醒挂起的线程。在 resumeTask() 中,我们将 isPaused 设置为 false,从而让任务继续执行。

  3. 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(); // 减少计数,唤醒等待线程
    }
}

详细解释:

  1. CountDownLatch latch:初始化时计数为 1,当异步任务运行时,调用 latch.await() 挂起任务,等待计数器归零。

  2. latch.await():线程在这里挂起,等待另一个线程调用 resumeTask(),从而使计数器减为 0,唤醒任务。

  3. 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;
    }
}

解释:

  1. TaskManager

    • 这是一个管理任务的类,负责启动异步任务并通过 wait() 挂起线程。

    • startAsyncTask() 方法使用 BukkitRunnable 来创建一个异步任务,任务开始时通过 lock.wait() 挂起。

    • 当任务被唤醒时,任务的后续逻辑会通过 Bukkit.getScheduler().runTask() 切换回主线程执行与 Bukkit API 相关的操作。

    • 通过 setPaused()getLock() 来管理线程的状态和锁定机制。

  2. CommandManager

    • CommandManager 负责监听并处理命令。在玩家或管理员通过命令触发时,唤醒等待的线程。

    • 通过 synchronized 语句块锁定 TaskManager 中的 lock 对象,确保线程同步安全。

    • 调用 taskManager.getLock().notify() 来唤醒在 TaskManager 中挂起的线程。

注册命令:

你需要在 plugin.yml 文件中注册 resumeTask 命令:

commands:
  resumeTask:
    description: 唤醒异步任务

主插件类中的初始化:

在主插件类中初始化 TaskManagerCommandManager,并将命令与 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));
    }
}

流程概述:

  1. TaskManager 启动了一个异步任务,并在任务中通过 lock.wait() 挂起线程。

  2. 玩家或管理员通过命令 /resumeTask 唤醒线程,命令处理由 CommandManager 处理。

  3. 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

image-20240910222853493

org.bukkit.configuration

Configuration (Spigot-API 1.21-R0.1-SNAPSHOT API 中文文档) (windit.net)

常用

  1. save(File file)

保存到指定路径

  1. getStringList()

返回数组列表

org.bukkit.entity

HumanEntity

  1. setGameMode(@NotNull GameMode mode)

设置游戏模式

GameMode.SURVIVAL: 生存模式

GameMode.CREATIVE: 创造模式

GameMode.ADVENTURE: 冒险模式

GameMode.SPECTATOR: 旁观模式

Player

纯Dev总结




是否对你有帮助?