这篇文章是来源于这个问题,
在回答这个问题之后我思考了一下,怎样存储数据才算优雅又高效,
因此有了这篇文章。
开发中的数据存储
本文的代码是 Groovy,你可以看做是没有行末分号、异常不用捕获的 Java。
本文代码以 MIT License 开源。
那么开始。
配置文件
配置文件是最易用也最常用的方法之一,
但是这不是本文的重点,因为配置文件的使用教程实在是太多了。
尽管配置文件的本意是给使用者自定义你的插件/Mod的行为,但是用来存储数据也是可以的。
为方便读者,这里给出一些其他人的教程。
- tdiant 的教程
- ustc_zzz 的 Forge 配置文件 教程
- Sponge 配置文件 教程
数据库
数据库是个挺好的话题,给出一个 MySQL 教程,
这篇文章不会讲解数据库,因为数据库也不是本文重点。但是额外提醒一点,用数据库注意阻塞和线程安全。
手动写二进制数据
存配置文件看起来很不优雅,还占空间;而存数据库对于一般插件好像又有点多此一举了,那么有没有又快又省空间的方法呢?
这节的标题就是,这一节也是真正的重点。
例子,我们要做一个玩家属性插件:
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 库,我们可以搞这么一个二进制数据序列化/反序列化的系统出来:
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)
}
}