本文发布于 https://izzel.io/2020/02/12/chat-with-future/
使用 CompletableFuture 实现一个简单的对话。
关于对话
对话并不罕见,在 QuickShop 中,插件会向玩家询问“你要买几个东西”,玩家则在聊天栏中输入对应的值;在 PlotSquared 中,玩家需要不断地输入对应的命令来配置地皮生成的参数,而输入命令也是另一种形式的对话。
对话的实现,Bukkit 为开发者准备了一套 Conversations API,编程开发板块的这篇帖子有简单的介绍。
本篇将会介绍另一种实现它的方法,简单的可以概括为,在一个方法里流畅的处理对话,比如这样:
import org.bukkit.entity.Player;
public class SomeClass {
    public void ask(Player player) {
        player.sendMessage("吾与徐公孰美?");
        String answer = getAnswer(player);
        assert answer.equals("徐公不若君之美也!");
    }
}接下来的篇幅,就将讨论上文的 getAnswer 如何实现。
实现对话,无非是这种逻辑:
- 发送给玩家问题
- 监听玩家的回复
- 处理玩家的回复
- 如果还有问题,跳到 1
按照正常的方法编写,我们需要一个监听器监听 PlayerChatEvent 或者它的异步版本,需要记录玩家的状态(玩家是不是在一个对话中 / 对话进行到了哪里),如果是诸葛亮王朗量级的超长对话,那么判断状态 / 更新状态的代码量将不堪设想。你还需要考虑玩家掉线/玩家不回答的情况,这就会又引入别的监听器和定时器。
因此,我们会想用上文代码一样的方式处理,无需记录状态,但是显然,getAnswer 不可能在调用的时候就有结果,玩家这时可能还不知道他被提了一个美不美的问题,而答案有可能会在未来提供,因此我们有了 Future 接口。
关于 Future
这是 Future 接口的定义:
package java.util.concurrent;
public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}注意到上面加粗的有可能了吗?为什么会 有可能 呢?
- 你可能不想问了(cancel)
- 玩家可能并不想理睬你(get方法超时了)
- 玩家掉线了(get方法抛出了异常)
- 齐威王突然召见你进宫(get方法被中断了)
- 玩家回答:徐公不若君之美也!
可以得知,你能准确的从玩家嘴里问出有效的答案实属不易,而 Future 接口就可以表示一个可能出现异常的、会在未来得到结果的东西。Future 的泛型 V,则表示得到的值。
因此,上面的代码可以改成这样:
public class SomeClass {
    public Future<String> getAnswer(Player player) {
        return // ???
    }
    public void ask(Player player) {
        try {
            player.sendMessage("吾与徐公孰美?");
            String answer = getAnswer(player).get();
            assert answer.equals("徐公不若君之美也!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}但是,Future 从哪里来呢?本篇的答案是 CompletableFuture。
关于 CompletableFuture
望文生义,CompletableFuture 代表着可以完成的 Future,这与本篇的目的不谋而合(不然呢):玩家输入消息后,getAnswer 方法返回的 Future 就该完成了。
我们来了解一下 CompletableFuture 中比较重要的几个方法:
package java.util.concurrent;
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    public CompletableFuture() { }
    public T join();
    public boolean complete(T value);
    public boolean completeExceptionally(Throwable ex);
}- CompletableFuture实现了 Future 接口,自然有- Future接口的所有方法
- 无参构造方法得到一个崭新出厂的 Future
- join方法与- get的效果类似,但有些许不同,在使用者看来最显著的区别就是,- join并不让你强制处理异常,虽然异常永远都在
- complete和- completeExceptionally分别代表正常完成和异常完成
因此,我们不难将上面的代码改成这样:
public class SomeClass {
    public Future<String> getAnswer(Player player) {
        CompletableFuture<String> future = new CompletableFuture();
        // 在别的地方调用 future.complete()
        return future;
    }
    public void ask(Player player) {
        try {
            player.sendMessage("吾与徐公孰美?");
            String answer = getAnswer(player).get();
            assert answer.equals("徐公不若君之美也!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}于是,我们也就不难写出以下的代码:
public class SomeClass {
    public void ask(Player player); // ...
    public Future<String> getAnswer(Player player) {
        CompletableFuture<String> future = new CompletableFuture<>();
        AskLifeExperience listener = new AskLifeExperience(player.getUniqueId(), future);
        Bukkit.getPluginManager().registerEvents(listener, plugin);
        return future;
    }
    public class AskLifeExperience implements Listener {
        private final UUID uuid;
        private final CompletableFuture<String> future;
        public AskLifeExperience(UUID uuid, CompletableFuture<String> future) {
            this.uuid = uuid;
            this.future = future;
        }
        @EventHandler
        public void on(AsyncPlayerChatEvent event) {
            if (event.getPlayer().getUniqueId().equals(uuid)) {
                future.complete(event.getMessage());
                HandlerList.unregisterAll(this);
            }
        }
    }
}逻辑清晰明了,注册一个监听器,在玩家聊天的时候完成 Future。
转眼一想,既然 getAnswer 需要一定时间才会取得答案,那 ask 方法不就会消耗很多时间了吗?因此,我们要异步调用 ask。
关于 Minecraft 服务器的同步与异步
当不在主线程进行操作的时候,我们都应该想一想,这样安全吗?
从上到下看一遍,不难问出这些问题:
- sendMessage安全吗?
- 异步注册事件是安全的吗?
- CompletableFuture#complete安全吗?(不然呢)
- Future#get方法一定会返回吗?
根据一篇写的很不错的文档(这篇文档对水桶的 scheduler 有较为详细的介绍),这几个东西是线程安全的:
- sendMessage(发包)
- Bukkit 的 scheduler包
- PluginManager#callEvent(event)
因此应该将注册事件部分的代码通过 Scheduler 转移到主线程完成。
最终的完整(但不完善)的方法如下,监听器与上文相同:
public class SomeClass {
    private Plugin plugin = null;
    public void ask(Player player) {
        Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
            try {
                player.sendMessage("吾与徐公孰美?");
                String answer = getAnswer(player).get(15, TimeUnit.SECONDS);
                assert answer.equals("徐公不若君之美也!");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
    public Future<String> getAnswer(Player player) {
        CompletableFuture<String> future = new CompletableFuture<>();
        Bukkit.getScheduler().runTask(plugin, () -> {
            AskLifeExperience listener = new AskLifeExperience(player.getUniqueId(), future);
            Bukkit.getPluginManager().registerEvents(listener, plugin);
        });
        return future;
    }
}当然,代码写完,还应该问自己几个问题:
- 我们在监听器里在事件触发的时候取消注册,万一事件永远不触发呢?
- 玩家离线后,Player实例不再可用,怎么办呢?
这些问题不是本篇重点,就不说了。
总结
可以看出,优美的写一串对话,所需代码量其实并不多,寥寥数十行就可以了。
线程安全十分重要。
CompletableFuture 还有许多实用的方法,可以用于各种耗时的操作,如 获取数据库的信息后,将其应用于服务器中。希望读者能够自行多加了解。
zzzz 编写了一篇协程教程,可以写出与本篇主方法非常类似的代码,虽然背后的原理大不相同,比如它全部在主线程上运行。
tdiant 编写了一篇十分全面的水桶教程,对 Scheduler 和其他部分都有很多讲解。
来自群组: PluginsCDTribe
2021.12 数据,可能有更多内容
如何问玩家“吾与徐公孰美?”本文发布于 https://izzel.io/2020/02/12/chat-with-future/
使用 CompletableFuture 实现一个简单的对话。
## 关于对话
对话并不罕见,在 QuickShop 中,插件会向玩家询问“你要买几个东西”,玩家则在聊天栏中输入对应的值;在 PlotSquared 中,玩家需要不断地输入对应的命令来配置地皮生成的参数,而输入命令也是另一种形式的对话。
对话的实现,Bukkit 为开发者准备了一套 Conversations API,编程开发板块的[这篇帖子](https://www.mcbbs.net/thread-619632-1-1.html)有简单的介绍。
本篇将会介绍另一种实现它的方法,简单的可以概括为,在一个方法里流畅的处理对话,比如这样:
```java
import org.bukkit.entity.Player;
public class SomeClass {
public void ask(Player player) {
player.sendMessage("吾与徐公孰美?");
String answer = getAnswer(player);
assert answer.equals("徐公不若君之美也!");
}
}
```
接下来的篇幅,就将讨论上文的 `getAnswer` 如何实现。
----
实现对话,无非是这种逻辑:
1. 发送给玩家问题
2. 监听玩家的回复
3. 处理玩家的回复
4. 如果还有问题,跳到 1
按照正常的方法编写,我们需要一个监听器监听 `PlayerChatEvent` 或者它的异步版本,需要记录玩家的状态(玩家是不是在一个对话中 / 对话进行到了哪里),如果是诸葛亮王朗量级的超长对话,那么判断状态 / 更新状态的代码量将不堪设想。你还需要考虑玩家掉线/玩家不回答的情况,这就会又引入别的监听器和定时器。
因此,我们会想用上文代码一样的方式处理,无需记录状态,但是显然,`getAnswer` 不可能在调用的时候就有结果,玩家这时可能还不知道他被提了一个美不美的问题,而答案**有可能**会在未来提供,因此我们有了 `Future` 接口。
## 关于 Future
这是 Future 接口的定义:
```java
package java.util.concurrent;
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
```
注意到上面加粗的**有可能**了吗?为什么会 有可能 呢?
* 你可能不想问了(`cancel`)
* 玩家可能并不想理睬你(`get` 方法超时了)
* 玩家掉线了(`get` 方法抛出了异常)
* 齐威王突然召见你进宫(`get` 方法被中断了)
* 玩家回答:徐公不若君之美也!
可以得知,你能准确的从玩家嘴里问出有效的答案实属不易,而 `Future` 接口就可以表示一个可能出现异常的、会在未来得到结果的东西。`Future` 的泛型 `V`,则表示得到的值。
因此,上面的代码可以改成这样:
```java
public class SomeClass {
public Future<String> getAnswer(Player player) {
return // ???
}
public void ask(Player player) {
try {
player.sendMessage("吾与徐公孰美?");
String answer = getAnswer(player).get();
assert answer.equals("徐公不若君之美也!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
但是,`Future` 从哪里来呢?本篇的答案是 `CompletableFuture`。
## 关于 CompletableFuture
望文生义,`CompletableFuture` 代表着可以**完成**的 `Future`,这与本篇的目的不谋而合(不然呢):玩家输入消息后,`getAnswer` 方法返回的 `Future` 就该完成了。
我们来了解一下 `CompletableFuture` 中比较重要的几个方法:
```java
package java.util.concurrent;
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
public CompletableFuture() { }
public T join();
public boolean complete(T value);
public boolean completeExceptionally(Throwable ex);
}
```
* `CompletableFuture` 实现了 Future 接口,自然有 `Future` 接口的所有方法
* 无参构造方法得到一个崭新出厂的 `Future`
* `join` 方法与 `get` 的效果类似,但有些许不同,在使用者看来最显著的区别就是,`join` 并不让你强制处理异常,虽然异常永远都在
* `complete` 和 `completeExceptionally` 分别代表正常完成和异常完成
因此,我们不难将上面的代码改成这样:
```java
public class SomeClass {
public Future<String> getAnswer(Player player) {
CompletableFuture<String> future = new CompletableFuture();
// 在别的地方调用 future.complete()
return future;
}
public void ask(Player player) {
try {
player.sendMessage("吾与徐公孰美?");
String answer = getAnswer(player).get();
assert answer.equals("徐公不若君之美也!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
于是,我们也就不难写出以下的代码:
```java
public class SomeClass {
public void ask(Player player); // ...
public Future<String> getAnswer(Player player) {
CompletableFuture<String> future = new CompletableFuture<>();
AskLifeExperience listener = new AskLifeExperience(player.getUniqueId(), future);
Bukkit.getPluginManager().registerEvents(listener, plugin);
return future;
}
public class AskLifeExperience implements Listener {
private final UUID uuid;
private final CompletableFuture<String> future;
public AskLifeExperience(UUID uuid, CompletableFuture<String> future) {
this.uuid = uuid;
this.future = future;
}
@EventHandler
public void on(AsyncPlayerChatEvent event) {
if (event.getPlayer().getUniqueId().equals(uuid)) {
future.complete(event.getMessage());
HandlerList.unregisterAll(this);
}
}
}
}
```
逻辑清晰明了,注册一个监听器,在玩家聊天的时候完成 `Future`。
转眼一想,既然 `getAnswer` 需要一定时间才会取得答案,那 `ask` 方法不就会消耗很多时间了吗?因此,我们要异步调用 `ask`。
## 关于 Minecraft 服务器的同步与异步
当不在主线程进行操作的时候,我们都应该想一想,这样安全吗?
从上到下看一遍,不难问出这些问题:
* `sendMessage` 安全吗?
* 异步注册事件是安全的吗?
* `CompletableFuture#complete` 安全吗?(不然呢)
* `Future#get` 方法一定会返回吗?
根据[一篇写的很不错的文档](http://bdn.tdiant.net/#/brm-2-5)(这篇文档对水桶的 scheduler 有较为详细的介绍),这几个东西是线程安全的:
* `sendMessage` (发包)
* Bukkit 的 `scheduler` 包
* `PluginManager#callEvent(event)`
因此应该将注册事件部分的代码通过 Scheduler 转移到主线程完成。
最终的完整(但不完善)的方法如下,监听器与上文相同:
```java
public class SomeClass {
private Plugin plugin = null;
public void ask(Player player) {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
player.sendMessage("吾与徐公孰美?");
String answer = getAnswer(player).get(15, TimeUnit.SECONDS);
assert answer.equals("徐公不若君之美也!");
} catch (Exception e) {
e.printStackTrace();
}
});
}
public Future<String> getAnswer(Player player) {
CompletableFuture<String> future = new CompletableFuture<>();
Bukkit.getScheduler().runTask(plugin, () -> {
AskLifeExperience listener = new AskLifeExperience(player.getUniqueId(), future);
Bukkit.getPluginManager().registerEvents(listener, plugin);
});
return future;
}
}
```
当然,代码写完,还应该问自己几个问题:
* 我们在监听器里在事件触发的时候取消注册,万一事件永远不触发呢?
* 玩家离线后,`Player` 实例不再可用,怎么办呢?
这些问题不是本篇重点,就不说了。
## 总结
可以看出,优美的写一串对话,所需代码量其实并不多,寥寥数十行就可以了。
**线程安全十分重要。**
`CompletableFuture` 还有许多实用的方法,可以用于各种耗时的操作,如 `获取数据库的信息后,将其应用于服务器中`。希望读者能够自行多加了解。
zzzz 编写了[一篇协程教程](https://www.mcbbs.net/thread-932084-1-1.html),可以写出与本篇主方法非常类似的代码,虽然背后的原理大不相同,比如它全部在主线程上运行。
tdiant 编写了[一篇十分全面的水桶教程](https://www.mcbbs.net/thread-808820-1-1.html),对 Scheduler 和其他部分都有很多讲解。