通过NMS序列化ItemStack 浅谈序列化复杂实体类
个人拙见,有错误请各位大佬指正
前言
对于序列化ItemStack的问题 ,板块内已经有优秀的教程了:此处+此处 。
而在此处我就简单分享一下了解的开发经验,让萌新少走弯路555~
也娇娇你遇到问题的时候以后怎么去寻根溯源~
本文将从序列化ItemStack的问题开始,讲讲如何直接序列化自己的JavaBean(实体类)。
一般讲,对于数据的存储来说Json的解析与序列化效率比Yaml更优,
而且在spigot 1.8+ 服务端内自带Google的Gson库,因此本文所讲的序列化将基于Gson库实现。你应该至少了解一下Gson的toJson()和fromJson()方法。
如何序列化ItemStack或Location?
如果只需要序列化单个ItemStcak 或者你说你可以遍历然后在yml 里直接set,那么这些可能没有任何帮助。你可以在前言中提到的教程贴了解,此处不过多赘述。
对于插件的数据存储,基本数据类型可能还好,直接new Gson()即可。 可是涉及到复杂的类型,应该如何序列化呢?
源码分析部分,不想看请跳过。
对于Gson 2.9.0 的tojson()方法,
public String toJson(Object src) {
return src == null ? this.toJson((JsonElement)JsonNull.INSTANCE) : this.toJson((Object)src, (Type)src.getClass());
}
public String toJson(Object src, Type typeOfSrc) {
StringWriter writer = new StringWriter();
this.toJson(src, typeOfSrc, (Appendable)writer);
return writer.toString();
}
public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonIOException {
TypeAdapter adapter = this.getAdapter(TypeToken.get(typeOfSrc));
boolean oldLenient = writer.isLenient();
writer.setLenient(true);
boolean oldHtmlSafe = writer.isHtmlSafe();
writer.setHtmlSafe(this.htmlSafe);
boolean oldSerializeNulls = writer.getSerializeNulls();
writer.setSerializeNulls(this.serializeNulls);
try {
adapter.write(writer, src);
} catch (IOException var13) {
throw new JsonIOException(var13);
} catch (AssertionError var14) {
throw new AssertionError("AssertionError (GSON 2.8.5): " + var14.getMessage(), var14);
} finally {
writer.setLenient(oldLenient);
writer.setHtmlSafe(oldHtmlSafe);
writer.setSerializeNulls(oldSerializeNulls);
}
}复制代码
对于getAdapter方法这里不多分析。感兴趣自行翻看源码。可定位相关字段是factories
由此看来,Gson的序列化,是获得了对应的TypeAdapter后 调用它的write()方法。 反序列化同理。
但是对与它来说,它哪认识bukkit的ItemStack啊,因此需要对TypeAdapter进行手动注册。
因此现在不能生成默认的Gson,需要使用GsonBuilder并注册TypeAdapter。
如何注册? 对于GsonBuilder的相关源码如下
public GsonBuilder registerTypeAdapter(Type type, Object typeAdapter) {
Preconditions.checkArgument(typeAdapter instanceof JsonSerializer || typeAdapter instanceof JsonDeserializer || typeAdapter instanceof InstanceCreator || typeAdapter instanceof TypeAdapter);
if (typeAdapter instanceof InstanceCreator) {
this.instanceCreators.put(type, (InstanceCreator)typeAdapter);
}
if (typeAdapter instanceof JsonSerializer || typeAdapter instanceof JsonDeserializer) {
TypeToken typeToken = TypeToken.get(type);
this.factories.add(TreeTypeAdapter.newFactoryWithMatchRawType(typeToken, typeAdapter));
}
if (typeAdapter instanceof TypeAdapter) {
this.factories.add(TypeAdapters.newFactory(TypeToken.get(type), (TypeAdapter)typeAdapter));
}
return this;
}复制代码
可知传入需要传入的类需要实现或继承: InstanceCreator JsonSerializer JsonDeserializer 和TypeAdapter
此处已有通过实现JsonSerializer JsonDeserializer详细的方案,感兴趣的请前往查看。
以下将会基于创建TypeAdapter类的实现。(实际上效果一样,只是笔者一种习惯)
那么接下来我们看看TypeAdapter是个什么东西?
public abstract class TypeAdapter {
public TypeAdapter() {
}
public abstract void write(JsonWriter var1, T var2) throws IOException;
public abstract T read(JsonReader var1) throws IOException;
}复制代码
这个抽象类拥有这两个抽象方法! 这个write和read就是在toJson/fromJson方法中被调用。
因此我们实现这两个方法即可。
那么JsonWriter和JsonReader又是什么? (恼)
这次看看doc!
这里给出了一个Message的例子:
Suppose we'd like to parse a stream of messages such as the following:
[
{
"id": 912345678901,
"text": "How do I read a JSON stream in Java?",
"geo": null,
"user": {
"name": "json_newb",
"followers_count": 41
}
},
{
"id": 912345678902,
"text": "@json_newb just use JsonReader!",
"geo": [50.454722, -104.606667],
"user": {
"name": "jesse",
"followers_count": 2
}
}
]复制代码public Message readMessage(JsonReader reader) throws IOException {
long id = -1;
String text = null;
User user = null;
List geo = null;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
if (name.equals("id")) {
id = reader.nextLong();
} else if (name.equals("text")) {
text = reader.nextString();
} else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
geo = readDoublesArray(reader);
} else if (name.equals("user")) {
user = readUser(reader);
} else {
reader.skipValue();
}
}
reader.endObject();
return new Message(id, text, user, geo);
}复制代码
这里相当于从Json变成Message,可见需要beginObject 然后 nextName (键) 和nextString (值)。 然后endObject。
那么write也是类似, 那就请读者自行分析。这里只使用结论。
总而言之就是在write中存入字段信息,然后在read中构造一个对象返回。
那么如何重写write 和read就显而易见了。
对于序列化Location :
首先我们先看看Location的结构~
public class Location implements Cloneable, ConfigurationSerializable {
private Reference world;
private double x;
private double y;
private double z;
private float pitch;
private float yaw;
}复制代码
Reference代表字段是引用。
什么 x y z pitch yaw 都好说, 那么world该怎么变成String?
此处我们可以绕路,只需要记录这个world就可以了,而不用真的把world序列化。Bukkit拥有一个Bukkit.getWorld()方法,因此我们仅需要获得world的UUID或name即可了,为了可读性,此处选择了name。
简单的得出了这样的代码:
class LocationAdapter extends TypeAdapter {
@Override
public void write(JsonWriter json, Location location) throws IOException {
json.beginObject();
json.name("world").value(Objects.requireNonNull(location.getWorld()).getName());
json.name("x").value(location.getX());
json.name("y").value(location.getY());
json.name("z").value(location.getZ());
json.name("yaw").value(location.getYaw());
json.name("pitch").value(location.getPitch());
json.endObject();
}
@Override
public Location read(JsonReader json) throws IOException {
json.beginObject();
String worldName = null;
double x = 0;
double y = 0;
double z = 0;
float yaw = 0;
float pitch = 0;
while (json.hasNext()) {
String name = json.nextName();
switch (name) {
case "world":
worldName = json.nextString();
break;
case "x":
x = json.nextDouble();
break;
case "y":
y = json.nextDouble();
break;
case "z":
z = json.nextDouble();
break;
case "yaw":
yaw = (float) json.nextDouble();
break;
case "pitch":
pitch = (float) json.nextDouble();
break;
default:
json.skipValue();
break;
}
}
json.endObject();
return new Location(Bukkit.getWorld(worldName), x, y, z, yaw, pitch);
}
}复制代码
简单的添加一些判空处理~
ps: json.peek() 就是... csgo 会peek对吧 ? 说的就是那个意思233
class LocationAdapter extends TypeAdapter {
@Override
public void write(JsonWriter json, Location location) throws IOException {
if (location == null) {
json.nullValue();
return;
}
json.beginObject();
json.name("world").value(Objects.requireNonNull(location.getWorld()).getName());
json.name("x").value(location.getX());
json.name("y").value(location.getY());
json.name("z").value(location.getZ());
json.name("yaw").value(location.getYaw());
json.name("pitch").value(location.getPitch());
json.endObject();
}
@Override
public Location read(JsonReader json) throws IOException {
if (json.peek() == null) {
return null;
}
json.beginObject();
String worldName = null;
double x = 0;
double y = 0;
double z = 0;
float yaw = 0;
float pitch = 0;
while (json.hasNext()) {
String name = json.nextName();
switch (name) {
case "world":
worldName = json.nextString();
break;
case "x":
x = json.nextDouble();
break;
case "y":
y = json.nextDouble();
break;
case "z":
z = json.nextDouble();
break;
case "yaw":
yaw = (float) json.nextDouble();
break;
case "pitch":
pitch = (float) json.nextDouble();
break;
default:
json.skipValue();
break;
}
}
json.endObject();
if (worldName != null) {
return new Location(Bukkit.getWorld(worldName), x, y, z, yaw, pitch);
}
return null;
}
}复制代码
此时你想序列化一个Location的时候,就不用存什么x y z 然后world给设置到yml里面了。
为什么不讲讲ItemStack呢!
当然你也可以像上面一样...查看字段然后这样这样那样那样的。
但它对特殊nbt的兼容很差!因此本文将使用NMS进行这些操作。
如何找到自己想要的NMS内容?
此处基于1.16.5
你应该大致了解 NMS-OBC-Bukkit 的关系!
大概很多萌新对于NMS的内容了解却不知道如何使用。
就像以上面的问题为例, 如何使用NMS 把ItemStack变成nmsItemStack后变为String保存?如何找到相关方法?
首先我们需要找到自己想要的类,例如ItemStack, 类名在NMS里面大概也没多少变化。
那么可找到net.minecraft.server.v1_16_R3.ItemStack (在1.17+为net.minecraft.world.item.ItemStack其余自行查阅)
而位于两者之间的OBC 则为org.bukkit.craftbukkit.v1_16_R3.inventory.CraftItemStack 可以看到它继承了bukkit的ItemStack。
字段与方法如下:
net.minecraft.server.v1_16_R3.ItemStack handle;
public static net.minecraft.server.v1_16_R3.ItemStack asNMSCopy(ItemStack original) {
if (original instanceof CraftItemStack) {
CraftItemStack stack = (CraftItemStack)original;
return stack.handle == null ? net.minecraft.server.v1_16_R3.ItemStack.b : stack.handle.cloneItemStack();
} else if (original != null && original.getType() != Material.AIR) {
Item item = CraftMagicNumbers.getItem(original.getType(), original.getDurability());
if (item == null) {
return net.minecraft.server.v1_16_R3.ItemStack.b;
} else {
net.minecraft.server.v1_16_R3.ItemStack stack = new net.minecraft.server.v1_16_R3.ItemStack(item, original.getAmount());
if (original.hasItemMeta()) {
setItemMeta(stack, original.getItemMeta());
}
return stack;
}
} else {
return net.minecraft.server.v1_16_R3.ItemStack.b;
}
}
public static ItemStack asBukkitCopy(net.minecraft.server.v1_16_R3.ItemStack original) {
if (original.isEmpty()) {
return new ItemStack(Material.AIR);
} else {
ItemStack stack = new ItemStack(CraftMagicNumbers.getMaterial(original.getItem()), original.getCount());
if (hasItemMeta(original)) {
stack.setItemMeta(getItemMeta(original));
}
return stack;
}
}
public static CraftItemStack asCraftMirror(net.minecraft.server.v1_16_R3.ItemStack original) {
return new CraftItemStack(original != null && !original.isEmpty() ? original : null);
}复制代码
此处由bukkit ItemStack变为nms ItemStack无疑是使用:asNMSCopy
那么反过来应该用哪个呢? 是asBukkitCopy吗? 并不是! 因为这个方法中使用的是bukkit ItemStack的构造方法,所以某些情况下bukkit它不认识物品的特殊tag! 于是我们在这里选择asCraftMirror 由于返回值为CraftItemStack 它继承了bukkit的ItemStack ,不是吗! 所以很多情况你可直接把它当作bukkit的 ItemStack进行操作。
(实际上你在插件里获得到游戏中的ItemStack实际上是CraftItemStack,不信你在游戏中getItemInMainHand后输出 getClass试试?)
那么你完成了第一步,转换。转换后又该如何使用呢?
nms.NBTTagCompound! 都说nbt nbt 它到底是什么呢?NBT(Named Binary Tag)它是一种Mojang设计的 用于存储和传输数据的二进制格式。
所以我们要保存物品nbt 肯定就是依赖它了!
我们可以在nms.ItemStack中发现这些方法:
private void load(NBTTagCompound nbttagcompound) {
this.item = (Item)IRegistry.ITEM.get(new MinecraftKey(nbttagcompound.getString("id")));
this.count = nbttagcompound.getByte("Count");
if (nbttagcompound.hasKeyOfType("tag", 10)) {
this.tag = nbttagcompound.getCompound("tag").clone();
this.getItem().b(this.tag);
}
if (this.getItem().usesDurability()) {
this.setDamage(this.getDamage());
}
}
private ItemStack(NBTTagCompound nbttagcompound) {
this.load(nbttagcompound);
this.checkEmpty();
}
public static ItemStack a(NBTTagCompound nbttagcompound) {
try {
return new ItemStack(nbttagcompound);
} catch (RuntimeException var2) {
LOGGER.debug("Tried to load invalid item: {}", nbttagcompound, var2);
return b;
}
}
public NBTTagCompound save(NBTTagCompound nbttagcompound) {
MinecraftKey minecraftkey = IRegistry.ITEM.getKey(this.getItem());
nbttagcompound.setString("id", minecraftkey == null ? "minecraft:air" : minecraftkey.toString());
nbttagcompound.setByte("Count", (byte)this.count);
if (this.tag != null) {
nbttagcompound.set("tag", this.tag.clone());
}
return nbttagcompound;
}
复制代码
可通过 save(NBTTagCompound nbttagcompound) 和 a(NBTTagCompound nbttagcompound) 这两个方法通过NBTTagCompound来实现序列化与反序列化。
如何让NBTTagCompound 和String转换呢? 只差这一步了!
答案是toString和 MojangsonParser (JsonParser? Mojangson好像可以理解为特殊的Json吧?
它拥有一个静态方法
public static NBTTagCompound parse(String var0) throws CommandSyntaxException {
return (new MojangsonParser(new StringReader(var0))).a();
}复制代码
传入String 输出NBTTagCompound
至此!我们把ItemStack通过NMS 与String转换的路捋清了!
即为ItemStack (CraftItemStack) nmsItemStack nbtTagCompound String
都用NMS了 你总不会不了解反射吧!
相关实现代码样例 (代码丑勿喷555)
public class NMSUtils {
private static Class nbtTagCompound;
private static Class itemStack;
private static Class MojangsonParser;
private static Class CraftItemStack;
private static Method parse;
private static Method a; //nbtTagCompound->nmsItemStack
private static Method asCraftMirror;
private static Method asNMSCopy;
private static Method save;
private static Method toString;
private static String NMS_PACKAGE = "";
private static String OBC_PACKAGE = "";
private final static String version;
public static int versionToInt;
static
{
String packet = Bukkit.getServer().getClass().getPackage().getName();
version = packet.substring(packet.lastIndexOf('.') + 1);
String nmsHead = "net.minecraft.server.";
versionToInt = Integer.parseInt(version.split("_")[1]);
try {
if(versionToInt
Class.forName(nmsHead + version +".ItemStack");
else
Class.forName("net.minecraft.world.item.ItemStack");
NMS_PACKAGE = nmsHead + version;
OBC_PACKAGE = "org.bukkit.craftbukkit." + version;
} catch (ClassNotFoundException ignored){
}
try {
CraftItemStack = Class.forName(OBC_PACKAGE+".inventory.CraftItemStack");
if(versionToInt
nbtTagCompound = Class.forName(NMS_PACKAGE+ ".NBTTagCompound");
itemStack = Class.forName(NMS_PACKAGE+".ItemStack");
MojangsonParser = Class.forName(NMS_PACKAGE+".MojangsonParser");
parse = MojangsonParser.getMethod("parse",String.class);
save = itemStack.getMethod("save",nbtTagCompound);
}else {
nbtTagCompound = Class.forName("net.minecraft.nbt.NBTTagCompound");
itemStack = Class.forName("net.minecraft.world.item.ItemStack");
MojangsonParser = Class.forName("net.minecraft.nbt.MojangsonParser");
parse = MojangsonParser.getMethod("a",String.class);
save = itemStack.getMethod("b",nbtTagCompound);
}
if(versionToInt>12)
a = itemStack.getMethod("a",nbtTagCompound);
asCraftMirror = CraftItemStack.getMethod("asCraftMirror",itemStack);
asNMSCopy = CraftItemStack.getMethod("asNMSCopy", ItemStack.class);
toString = nbtTagCompound.getMethod("toString");
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
}
}
public static ItemStack toItem(String NBTString){
Object nbt; //(nbtTagCompound)
ItemStack item = null;
Object nmsItemStack;
try {
nbt = parse.invoke(MojangsonParser,NBTString);
if(versionToInt>12){
nmsItemStack = a.invoke(itemStack,nbt);
}else {
if(versionToInt>9) {
Constructor constructor = itemStack.getConstructor(nbtTagCompound);
nmsItemStack = constructor.newInstance(nbt);
}
else {
//1.7.10
Method createStack = itemStack.getMethod("createStack", nbtTagCompound);
nmsItemStack = createStack.invoke(itemStack,nbt);
}
}
item = (ItemStack) asCraftMirror.invoke(CraftItemStack,nmsItemStack);
} catch (Exception e) {
e.printStackTrace();
}
return item;
}
public static String toNBTString(ItemStack itemStack){
Object nbt;
Object str = null;
try {
Object nmsItemStack = asNMSCopy.invoke(CraftItemStack,itemStack);
Constructor constructor = nbtTagCompound.getConstructor();
nbt = constructor.newInstance();
save.invoke(nmsItemStack,nbt);
str = toString.invoke(nbt);
}catch (Exception e){
e.printStackTrace();
}
return (String)str;
}
}复制代码
扯了好多233 那么到这里,你总会如何创建一个ItemStack的 TypeAdapter了吧!
class ItemAdapter extends TypeAdapter{
@Override
public void write(JsonWriter json, ItemStack item) throws IOException {
if(item == null){
json.nullValue();
return;
}
json.beginObject();
json.name("item").value(NMSUtils.toNBTString(item));
json.endObject();
}
@Override
public ItemStack read(JsonReader json) throws IOException {
if (json.peek() == null) {
return null;
}
json.beginObject();
ItemStack item = null;
while (json.hasNext()) {
String name = json.nextName();
if ("item".equals(name)) {
item = NMSUtils.toItem(json.nextString());
} else {
json.skipValue();
}
}
json.endObject();
if(item==null){
item = new ItemStack(Material.STONE);
}
return item;
}
}复制代码
具体使用就是。
public static String ItemToJson(ItemStack item){
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(ItemStack.class, new ItemAdapter())
.create();
return gson.toJson(item);
}复制代码
出来的String不是想存哪存哪?
当然上面的序列化并不是只能传入单一的ItemStack!
如何序列化复杂、嵌套的JavaBean?
JavaBean的概念...
JavaBean 是一种JAVA语言写成的可重用组件。为写成JavaBean,类必须是具体的和公共的,并且具有无参数的构造器。JavaBean 通过提供符合一致性设计模式的公共方法将内部域暴露成员属性,set和get方法获取。众所周知,属性名称符合这种模式,其他Java 类可以通过自省机制(反射机制)发现和操作这些JavaBean 的属性。
比如我现在有一个类MyPlayer 如下 我希望序列化与反序列化它。
@AllArgsConstructor
@NoArgsConstructor
@Data
public class MyPlayer {
private String name;
private int age;
}复制代码
此时直接使用new Gson()即可,对吧?
那么添加一个ItemStack字段呢?
@AllArgsConstructor
@NoArgsConstructor
@Data
public class MyPlayer {
private String name;
private int age;
private ItemStack item;
}复制代码
用上文的ItemAdapter即可实现。
同时Gson的序列化对于数组、List与Map,也无需太大改变即可直接序列化。
Ps 昨天通过群u Vincen 的尝试Map的key必须为String。 当然为了避免内存泄漏,请勿以实体类当Key!
可是当我希望序列化一个如下的类:
public class Group{
private List players;
private int size;
}复制代码
此时并不可以直接
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(ItemStack.class, new ItemAdapter())
.create();复制代码
其实此时的MyPlayer已经类似于ItemStack了,你仍需要给与它的TypeAdapter
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(ItemStack.class, new ItemAdapter())
.registerTypeAdapter(MyPlayer.class, new MyPlayerAdapter())
.create();复制代码
至此 完结撒花~
个人拙见,有错误请各位大佬指正
前言
对于序列化ItemStack的问题 ,板块内已经有优秀的教程了:此处+此处 。
而在此处我就简单分享一下了解的开发经验,让萌新少走弯路555~
也娇娇你遇到问题的时候以后怎么去寻根溯源~
本文将从序列化ItemStack的问题开始,讲讲如何直接序列化自己的JavaBean(实体类)。
一般讲,对于数据的存储来说Json的解析与序列化效率比Yaml更优,
而且在spigot 1.8+ 服务端内自带Google的Gson库,因此本文所讲的序列化将基于Gson库实现。你应该至少了解一下Gson的toJson()和fromJson()方法。
如何序列化ItemStack或Location?
如果只需要序列化单个ItemStcak 或者你说你可以遍历然后在yml 里直接set,那么这些可能没有任何帮助。你可以在前言中提到的教程贴了解,此处不过多赘述。
对于插件的数据存储,基本数据类型可能还好,直接new Gson()即可。 可是涉及到复杂的类型,应该如何序列化呢?
源码分析部分,不想看请跳过。
对于Gson 2.9.0 的tojson()方法,
public String toJson(Object src) {
return src == null ? this.toJson((JsonElement)JsonNull.INSTANCE) : this.toJson((Object)src, (Type)src.getClass());
}
public String toJson(Object src, Type typeOfSrc) {
StringWriter writer = new StringWriter();
this.toJson(src, typeOfSrc, (Appendable)writer);
return writer.toString();
}
public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonIOException {
TypeAdapter adapter = this.getAdapter(TypeToken.get(typeOfSrc));
boolean oldLenient = writer.isLenient();
writer.setLenient(true);
boolean oldHtmlSafe = writer.isHtmlSafe();
writer.setHtmlSafe(this.htmlSafe);
boolean oldSerializeNulls = writer.getSerializeNulls();
writer.setSerializeNulls(this.serializeNulls);
try {
adapter.write(writer, src);
} catch (IOException var13) {
throw new JsonIOException(var13);
} catch (AssertionError var14) {
throw new AssertionError("AssertionError (GSON 2.8.5): " + var14.getMessage(), var14);
} finally {
writer.setLenient(oldLenient);
writer.setHtmlSafe(oldHtmlSafe);
writer.setSerializeNulls(oldSerializeNulls);
}
}复制代码
对于getAdapter方法这里不多分析。感兴趣自行翻看源码。可定位相关字段是factories
由此看来,Gson的序列化,是获得了对应的TypeAdapter后 调用它的write()方法。 反序列化同理。
但是对与它来说,它哪认识bukkit的ItemStack啊,因此需要对TypeAdapter进行手动注册。
因此现在不能生成默认的Gson,需要使用GsonBuilder并注册TypeAdapter。
如何注册? 对于GsonBuilder的相关源码如下
public GsonBuilder registerTypeAdapter(Type type, Object typeAdapter) {
Preconditions.checkArgument(typeAdapter instanceof JsonSerializer || typeAdapter instanceof JsonDeserializer || typeAdapter instanceof InstanceCreator || typeAdapter instanceof TypeAdapter);
if (typeAdapter instanceof InstanceCreator) {
this.instanceCreators.put(type, (InstanceCreator)typeAdapter);
}
if (typeAdapter instanceof JsonSerializer || typeAdapter instanceof JsonDeserializer) {
TypeToken typeToken = TypeToken.get(type);
this.factories.add(TreeTypeAdapter.newFactoryWithMatchRawType(typeToken, typeAdapter));
}
if (typeAdapter instanceof TypeAdapter) {
this.factories.add(TypeAdapters.newFactory(TypeToken.get(type), (TypeAdapter)typeAdapter));
}
return this;
}复制代码
可知传入需要传入的类需要实现或继承: InstanceCreator JsonSerializer JsonDeserializer 和TypeAdapter
此处已有通过实现JsonSerializer JsonDeserializer详细的方案,感兴趣的请前往查看。
以下将会基于创建TypeAdapter类的实现。(实际上效果一样,只是笔者一种习惯)
那么接下来我们看看TypeAdapter是个什么东西?
public abstract class TypeAdapter {
public TypeAdapter() {
}
public abstract void write(JsonWriter var1, T var2) throws IOException;
public abstract T read(JsonReader var1) throws IOException;
}复制代码
这个抽象类拥有这两个抽象方法! 这个write和read就是在toJson/fromJson方法中被调用。
因此我们实现这两个方法即可。
那么JsonWriter和JsonReader又是什么? (恼)
这次看看doc!
这里给出了一个Message的例子:
Suppose we'd like to parse a stream of messages such as the following:
[
{
"id": 912345678901,
"text": "How do I read a JSON stream in Java?",
"geo": null,
"user": {
"name": "json_newb",
"followers_count": 41
}
},
{
"id": 912345678902,
"text": "@json_newb just use JsonReader!",
"geo": [50.454722, -104.606667],
"user": {
"name": "jesse",
"followers_count": 2
}
}
]复制代码public Message readMessage(JsonReader reader) throws IOException {
long id = -1;
String text = null;
User user = null;
List geo = null;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
if (name.equals("id")) {
id = reader.nextLong();
} else if (name.equals("text")) {
text = reader.nextString();
} else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
geo = readDoublesArray(reader);
} else if (name.equals("user")) {
user = readUser(reader);
} else {
reader.skipValue();
}
}
reader.endObject();
return new Message(id, text, user, geo);
}复制代码
这里相当于从Json变成Message,可见需要beginObject 然后 nextName (键) 和nextString (值)。 然后endObject。
那么write也是类似, 那就请读者自行分析。这里只使用结论。
总而言之就是在write中存入字段信息,然后在read中构造一个对象返回。
那么如何重写write 和read就显而易见了。
对于序列化Location :
首先我们先看看Location的结构~
public class Location implements Cloneable, ConfigurationSerializable {
private Reference world;
private double x;
private double y;
private double z;
private float pitch;
private float yaw;
}复制代码
Reference代表字段是引用。
什么 x y z pitch yaw 都好说, 那么world该怎么变成String?
此处我们可以绕路,只需要记录这个world就可以了,而不用真的把world序列化。Bukkit拥有一个Bukkit.getWorld()方法,因此我们仅需要获得world的UUID或name即可了,为了可读性,此处选择了name。
简单的得出了这样的代码:
class LocationAdapter extends TypeAdapter {
@Override
public void write(JsonWriter json, Location location) throws IOException {
json.beginObject();
json.name("world").value(Objects.requireNonNull(location.getWorld()).getName());
json.name("x").value(location.getX());
json.name("y").value(location.getY());
json.name("z").value(location.getZ());
json.name("yaw").value(location.getYaw());
json.name("pitch").value(location.getPitch());
json.endObject();
}
@Override
public Location read(JsonReader json) throws IOException {
json.beginObject();
String worldName = null;
double x = 0;
double y = 0;
double z = 0;
float yaw = 0;
float pitch = 0;
while (json.hasNext()) {
String name = json.nextName();
switch (name) {
case "world":
worldName = json.nextString();
break;
case "x":
x = json.nextDouble();
break;
case "y":
y = json.nextDouble();
break;
case "z":
z = json.nextDouble();
break;
case "yaw":
yaw = (float) json.nextDouble();
break;
case "pitch":
pitch = (float) json.nextDouble();
break;
default:
json.skipValue();
break;
}
}
json.endObject();
return new Location(Bukkit.getWorld(worldName), x, y, z, yaw, pitch);
}
}复制代码
简单的添加一些判空处理~
ps: json.peek() 就是... csgo 会peek对吧 ? 说的就是那个意思233
class LocationAdapter extends TypeAdapter {
@Override
public void write(JsonWriter json, Location location) throws IOException {
if (location == null) {
json.nullValue();
return;
}
json.beginObject();
json.name("world").value(Objects.requireNonNull(location.getWorld()).getName());
json.name("x").value(location.getX());
json.name("y").value(location.getY());
json.name("z").value(location.getZ());
json.name("yaw").value(location.getYaw());
json.name("pitch").value(location.getPitch());
json.endObject();
}
@Override
public Location read(JsonReader json) throws IOException {
if (json.peek() == null) {
return null;
}
json.beginObject();
String worldName = null;
double x = 0;
double y = 0;
double z = 0;
float yaw = 0;
float pitch = 0;
while (json.hasNext()) {
String name = json.nextName();
switch (name) {
case "world":
worldName = json.nextString();
break;
case "x":
x = json.nextDouble();
break;
case "y":
y = json.nextDouble();
break;
case "z":
z = json.nextDouble();
break;
case "yaw":
yaw = (float) json.nextDouble();
break;
case "pitch":
pitch = (float) json.nextDouble();
break;
default:
json.skipValue();
break;
}
}
json.endObject();
if (worldName != null) {
return new Location(Bukkit.getWorld(worldName), x, y, z, yaw, pitch);
}
return null;
}
}复制代码
此时你想序列化一个Location的时候,就不用存什么x y z 然后world给设置到yml里面了。
为什么不讲讲ItemStack呢!
当然你也可以像上面一样...查看字段然后这样这样那样那样的。
但它对特殊nbt的兼容很差!因此本文将使用NMS进行这些操作。
如何找到自己想要的NMS内容?
此处基于1.16.5
你应该大致了解 NMS-OBC-Bukkit 的关系!
大概很多萌新对于NMS的内容了解却不知道如何使用。
就像以上面的问题为例, 如何使用NMS 把ItemStack变成nmsItemStack后变为String保存?如何找到相关方法?
首先我们需要找到自己想要的类,例如ItemStack, 类名在NMS里面大概也没多少变化。
那么可找到net.minecraft.server.v1_16_R3.ItemStack (在1.17+为net.minecraft.world.item.ItemStack其余自行查阅)
而位于两者之间的OBC 则为org.bukkit.craftbukkit.v1_16_R3.inventory.CraftItemStack 可以看到它继承了bukkit的ItemStack。
字段与方法如下:
net.minecraft.server.v1_16_R3.ItemStack handle;
public static net.minecraft.server.v1_16_R3.ItemStack asNMSCopy(ItemStack original) {
if (original instanceof CraftItemStack) {
CraftItemStack stack = (CraftItemStack)original;
return stack.handle == null ? net.minecraft.server.v1_16_R3.ItemStack.b : stack.handle.cloneItemStack();
} else if (original != null && original.getType() != Material.AIR) {
Item item = CraftMagicNumbers.getItem(original.getType(), original.getDurability());
if (item == null) {
return net.minecraft.server.v1_16_R3.ItemStack.b;
} else {
net.minecraft.server.v1_16_R3.ItemStack stack = new net.minecraft.server.v1_16_R3.ItemStack(item, original.getAmount());
if (original.hasItemMeta()) {
setItemMeta(stack, original.getItemMeta());
}
return stack;
}
} else {
return net.minecraft.server.v1_16_R3.ItemStack.b;
}
}
public static ItemStack asBukkitCopy(net.minecraft.server.v1_16_R3.ItemStack original) {
if (original.isEmpty()) {
return new ItemStack(Material.AIR);
} else {
ItemStack stack = new ItemStack(CraftMagicNumbers.getMaterial(original.getItem()), original.getCount());
if (hasItemMeta(original)) {
stack.setItemMeta(getItemMeta(original));
}
return stack;
}
}
public static CraftItemStack asCraftMirror(net.minecraft.server.v1_16_R3.ItemStack original) {
return new CraftItemStack(original != null && !original.isEmpty() ? original : null);
}复制代码
此处由bukkit ItemStack变为nms ItemStack无疑是使用:asNMSCopy
那么反过来应该用哪个呢? 是asBukkitCopy吗? 并不是! 因为这个方法中使用的是bukkit ItemStack的构造方法,所以某些情况下bukkit它不认识物品的特殊tag! 于是我们在这里选择asCraftMirror 由于返回值为CraftItemStack 它继承了bukkit的ItemStack ,不是吗! 所以很多情况你可直接把它当作bukkit的 ItemStack进行操作。
(实际上你在插件里获得到游戏中的ItemStack实际上是CraftItemStack,不信你在游戏中getItemInMainHand后输出 getClass试试?)
那么你完成了第一步,转换。转换后又该如何使用呢?
nms.NBTTagCompound! 都说nbt nbt 它到底是什么呢?NBT(Named Binary Tag)它是一种Mojang设计的 用于存储和传输数据的二进制格式。
所以我们要保存物品nbt 肯定就是依赖它了!
我们可以在nms.ItemStack中发现这些方法:
private void load(NBTTagCompound nbttagcompound) {
this.item = (Item)IRegistry.ITEM.get(new MinecraftKey(nbttagcompound.getString("id")));
this.count = nbttagcompound.getByte("Count");
if (nbttagcompound.hasKeyOfType("tag", 10)) {
this.tag = nbttagcompound.getCompound("tag").clone();
this.getItem().b(this.tag);
}
if (this.getItem().usesDurability()) {
this.setDamage(this.getDamage());
}
}
private ItemStack(NBTTagCompound nbttagcompound) {
this.load(nbttagcompound);
this.checkEmpty();
}
public static ItemStack a(NBTTagCompound nbttagcompound) {
try {
return new ItemStack(nbttagcompound);
} catch (RuntimeException var2) {
LOGGER.debug("Tried to load invalid item: {}", nbttagcompound, var2);
return b;
}
}
public NBTTagCompound save(NBTTagCompound nbttagcompound) {
MinecraftKey minecraftkey = IRegistry.ITEM.getKey(this.getItem());
nbttagcompound.setString("id", minecraftkey == null ? "minecraft:air" : minecraftkey.toString());
nbttagcompound.setByte("Count", (byte)this.count);
if (this.tag != null) {
nbttagcompound.set("tag", this.tag.clone());
}
return nbttagcompound;
}
复制代码
可通过 save(NBTTagCompound nbttagcompound) 和 a(NBTTagCompound nbttagcompound) 这两个方法通过NBTTagCompound来实现序列化与反序列化。
如何让NBTTagCompound 和String转换呢? 只差这一步了!
答案是toString和 MojangsonParser (JsonParser? Mojangson好像可以理解为特殊的Json吧?
它拥有一个静态方法
public static NBTTagCompound parse(String var0) throws CommandSyntaxException {
return (new MojangsonParser(new StringReader(var0))).a();
}复制代码
传入String 输出NBTTagCompound
至此!我们把ItemStack通过NMS 与String转换的路捋清了!
即为ItemStack (CraftItemStack) nmsItemStack nbtTagCompound String
都用NMS了 你总不会不了解反射吧!
相关实现代码样例 (代码丑勿喷555)
public class NMSUtils {
private static Class nbtTagCompound;
private static Class itemStack;
private static Class MojangsonParser;
private static Class CraftItemStack;
private static Method parse;
private static Method a; //nbtTagCompound->nmsItemStack
private static Method asCraftMirror;
private static Method asNMSCopy;
private static Method save;
private static Method toString;
private static String NMS_PACKAGE = "";
private static String OBC_PACKAGE = "";
private final static String version;
public static int versionToInt;
static
{
String packet = Bukkit.getServer().getClass().getPackage().getName();
version = packet.substring(packet.lastIndexOf('.') + 1);
String nmsHead = "net.minecraft.server.";
versionToInt = Integer.parseInt(version.split("_")[1]);
try {
if(versionToInt
Class.forName(nmsHead + version +".ItemStack");
else
Class.forName("net.minecraft.world.item.ItemStack");
NMS_PACKAGE = nmsHead + version;
OBC_PACKAGE = "org.bukkit.craftbukkit." + version;
} catch (ClassNotFoundException ignored){
}
try {
CraftItemStack = Class.forName(OBC_PACKAGE+".inventory.CraftItemStack");
if(versionToInt
nbtTagCompound = Class.forName(NMS_PACKAGE+ ".NBTTagCompound");
itemStack = Class.forName(NMS_PACKAGE+".ItemStack");
MojangsonParser = Class.forName(NMS_PACKAGE+".MojangsonParser");
parse = MojangsonParser.getMethod("parse",String.class);
save = itemStack.getMethod("save",nbtTagCompound);
}else {
nbtTagCompound = Class.forName("net.minecraft.nbt.NBTTagCompound");
itemStack = Class.forName("net.minecraft.world.item.ItemStack");
MojangsonParser = Class.forName("net.minecraft.nbt.MojangsonParser");
parse = MojangsonParser.getMethod("a",String.class);
save = itemStack.getMethod("b",nbtTagCompound);
}
if(versionToInt>12)
a = itemStack.getMethod("a",nbtTagCompound);
asCraftMirror = CraftItemStack.getMethod("asCraftMirror",itemStack);
asNMSCopy = CraftItemStack.getMethod("asNMSCopy", ItemStack.class);
toString = nbtTagCompound.getMethod("toString");
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
}
}
public static ItemStack toItem(String NBTString){
Object nbt; //(nbtTagCompound)
ItemStack item = null;
Object nmsItemStack;
try {
nbt = parse.invoke(MojangsonParser,NBTString);
if(versionToInt>12){
nmsItemStack = a.invoke(itemStack,nbt);
}else {
if(versionToInt>9) {
Constructor constructor = itemStack.getConstructor(nbtTagCompound);
nmsItemStack = constructor.newInstance(nbt);
}
else {
//1.7.10
Method createStack = itemStack.getMethod("createStack", nbtTagCompound);
nmsItemStack = createStack.invoke(itemStack,nbt);
}
}
item = (ItemStack) asCraftMirror.invoke(CraftItemStack,nmsItemStack);
} catch (Exception e) {
e.printStackTrace();
}
return item;
}
public static String toNBTString(ItemStack itemStack){
Object nbt;
Object str = null;
try {
Object nmsItemStack = asNMSCopy.invoke(CraftItemStack,itemStack);
Constructor constructor = nbtTagCompound.getConstructor();
nbt = constructor.newInstance();
save.invoke(nmsItemStack,nbt);
str = toString.invoke(nbt);
}catch (Exception e){
e.printStackTrace();
}
return (String)str;
}
}复制代码
扯了好多233 那么到这里,你总会如何创建一个ItemStack的 TypeAdapter了吧!
class ItemAdapter extends TypeAdapter{
@Override
public void write(JsonWriter json, ItemStack item) throws IOException {
if(item == null){
json.nullValue();
return;
}
json.beginObject();
json.name("item").value(NMSUtils.toNBTString(item));
json.endObject();
}
@Override
public ItemStack read(JsonReader json) throws IOException {
if (json.peek() == null) {
return null;
}
json.beginObject();
ItemStack item = null;
while (json.hasNext()) {
String name = json.nextName();
if ("item".equals(name)) {
item = NMSUtils.toItem(json.nextString());
} else {
json.skipValue();
}
}
json.endObject();
if(item==null){
item = new ItemStack(Material.STONE);
}
return item;
}
}复制代码
具体使用就是。
public static String ItemToJson(ItemStack item){
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(ItemStack.class, new ItemAdapter())
.create();
return gson.toJson(item);
}复制代码
出来的String不是想存哪存哪?
当然上面的序列化并不是只能传入单一的ItemStack!
如何序列化复杂、嵌套的JavaBean?
JavaBean的概念...
JavaBean 是一种JAVA语言写成的可重用组件。为写成JavaBean,类必须是具体的和公共的,并且具有无参数的构造器。JavaBean 通过提供符合一致性设计模式的公共方法将内部域暴露成员属性,set和get方法获取。众所周知,属性名称符合这种模式,其他Java 类可以通过自省机制(反射机制)发现和操作这些JavaBean 的属性。
比如我现在有一个类MyPlayer 如下 我希望序列化与反序列化它。
@AllArgsConstructor
@NoArgsConstructor
@Data
public class MyPlayer {
private String name;
private int age;
}复制代码
此时直接使用new Gson()即可,对吧?
那么添加一个ItemStack字段呢?
@AllArgsConstructor
@NoArgsConstructor
@Data
public class MyPlayer {
private String name;
private int age;
private ItemStack item;
}复制代码
用上文的ItemAdapter即可实现。
同时Gson的序列化对于数组、List与Map,也无需太大改变即可直接序列化。
Ps 昨天通过群u Vincen 的尝试Map的key必须为String。 当然为了避免内存泄漏,请勿以实体类当Key!
可是当我希望序列化一个如下的类:
public class Group{
private List players;
private int size;
}复制代码
此时并不可以直接
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(ItemStack.class, new ItemAdapter())
.create();复制代码
其实此时的MyPlayer已经类似于ItemStack了,你仍需要给与它的TypeAdapter
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(ItemStack.class, new ItemAdapter())
.registerTypeAdapter(MyPlayer.class, new MyPlayerAdapter())
.create();复制代码
至此 完结撒花~