开发中的数据存储

这篇文章是来源于这个问题
在回答这个问题之后我思考了一下,怎样存储数据才算优雅又高效,因此有了这篇文章。

本文的代码是 Groovy,你可以看做是没有行末分号、异常不用捕获的 Java。
本文代码以 MIT License 开源。

那么开始。

配置文件

配置文件是最易用也最常用的方法之一,
但是这不是本文的重点,因为配置文件的使用教程实在是太多了。

尽管配置文件的本意是给使用者自定义你的插件/Mod的行为,但是用来存储数据也是可以的。

为方便读者,这里给出一些其他人的教程。

数据库

数据库是个挺好的话题,给出一个 MySQL 教程

这篇文章不会讲解数据库,因为数据库也不是本文重点。但是额外提醒一点,用数据库注意阻塞和线程安全。

手动写二进制数据

存配置文件看起来很不优雅,还占空间;而存数据库对于一般插件好像又有点多此一举了,那么有没有又快又省空间的方法呢?

这节的标题就是,这一节也是真正的重点。

例子,我们要做一个玩家属性插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
class PlayerData {
int hp = 20
double strength = 0
boolean haveJob
Job job
}

class Job {
String name
int level
int exp
String prefix // 假设我们的插件甚至还要搞聊天前缀
}

然后把这个实例变成二进制数据。

模仿 Bukkit 的那个 Serializable,利用 Netty Buffer 库,我们可以搞这么一个二进制数据序列化/反序列化的系统出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import com.google.common.collect.Maps
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import org.bukkit.plugin.java.JavaPlugin

import java.nio.charset.StandardCharsets
import java.util.function.Function

interface Serializable {
void serialize(ByteBuf buf)
}

class PlayerData implements Serializable {
int hp = 20
double strength = 0
boolean haveJob = false
Job job

@Override
void serialize(ByteBuf buf) {
buf.writeInt(hp)
buf.writeDouble(strength)
buf.writeBoolean(haveJob)
if (haveJob) {
job.serialize(buf)
}
}

static PlayerData deserialize(ByteBuf buf) {
PlayerData data = new PlayerData()
data.hp = buf.readInt()
data.strength = buf.readDouble()
data.haveJob = buf.readBoolean()
if (data.haveJob) {
data.job = Job.deserialize(buf)
}
return data
}
}

class Job implements Serializable {
String name
int level
int exp
String prefix // 假设我们的插件甚至还要搞聊天前缀

@Override
void serialize(ByteBuf buf) {
Util.writeString(buf, name)
buf.writeInt(level)
buf.writeInt(exp)
Util.writeString(buf, prefix)
}

static Job deserialize(ByteBuf buf) {
Job job = new Job()
job.name = Util.readString(buf)
job.level = buf.readInt()
job.exp = buf.readInt()
job.prefix = Util.readString(buf)
return job
}
}

class Util {
static Map<Class<?>, Function<ByteBuf, ?>> deserializers = Maps.newHashMap()
static <T> void registerDeserializer(Class<T> cl, Function<ByteBuf, T> function) {
deserializers.put(cl ,function)
}
static <T> T deserialize(Class<T> cl) {
Function<ByteBuf, ?> function = deserializers.get(cl)
if (function != null) {
return (T) function.apply(Unpooled.buffer())
}
throw new Exception("Not registered class $cl")
}
static byte[] serialize(Object obj) {
if (obj instanceof Serializable) {
ByteBuf buf = Unpooled.buffer()
obj.serialize(buf)
return buf.array()
}
return null
}
static void writeString(ByteBuf buf, String value) {
if (value == null) {
buf.writeInt(0)
return
}
byte[] arr = value.getBytes(StandardCharsets.UTF_8)
buf.writeInt(arr.length)
buf.writeBytes(arr)
}
static String readString(ByteBuf buf) {
int len = buf.readInt()
if (len == 0) return null
byte[] arr = new byte[len]
buf.readBytes(arr)
return new String(arr, StandardCharsets.UTF_8)
}
}

class Plugin extends JavaPlugin {
@Override
void onEnable() {
Util.registerDeserializer(PlayerData, PlayerData::deserialize)
Util.registerDeserializer(Job, Job::deserialize)
}
}