如何使用 Gson 在 java 中使用非本机字段序列化 class?

How to serialize a class with non native fields in java with Gson?

我目前正在为 Minecraft 中的 Spigot 插件编写自己的 NPC class。

import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import net.minecraft.server.v1_16_R1.*;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_16_R1.CraftServer;
import org.bukkit.craftbukkit.v1_16_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_16_R1.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_16_R1.util.CraftChatMessage;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class NPC extends EntityPlayer implements Serializable {

    private final int entityId;
    private Location location;
    private GameProfile gameprofile;

    private final List<Player> recipients;
    private Recipient recipient_type;

    private String display_name;
    private String tablist_name;
    private final DataWatcher dataWatcher;
    private final DataWatcherObject<Byte> object_entity_state;
    private final DataWatcherObject<String> object_customName;
    private final DataWatcherObject<Boolean> object_isSilent;
    private final DataWatcherObject<Boolean> object_hasGravity;
    private final DataWatcherObject<Boolean> object_isCustomNameVisible;

    private boolean isDestroyed;
    private final Plugin plugin;

    public NPC(String name, Location location, UUID uuid, WorldServer worldServer, Plugin plugin) {
        super(((CraftServer) Bukkit.getServer()).getServer(), worldServer, new GameProfile(uuid, name), new PlayerInteractManager(worldServer));
        this.plugin = plugin;
        this.display_name = name;
        this.tablist_name = name;
        this.recipient_type = Recipient.ALL;
        this.recipients = new ArrayList<>();
        this.entityId = (int) Math.ceil(Math.random() * 1000);
        this.gameprofile = new GameProfile(uuid, display_name);
        this.location = location;
        this.dataWatcher = new DataWatcher(null);
        this.dataWatcher.register(object_entity_state = new DataWatcherObject<>(0, DataWatcherRegistry.a), (byte) 0);
        this.dataWatcher.register(new DataWatcherObject<>(1, DataWatcherRegistry.b), 300);
        this.dataWatcher.register(object_customName = new DataWatcherObject<>(2, DataWatcherRegistry.d), "");
        this.dataWatcher.register(object_isCustomNameVisible = new DataWatcherObject<>(3, DataWatcherRegistry.i),false);
        this.dataWatcher.register(object_isSilent = new DataWatcherObject<>(4, DataWatcherRegistry.i), false);
        this.dataWatcher.register(object_hasGravity = new DataWatcherObject<>(5, DataWatcherRegistry.i), false);
        this.dataWatcher.register(new DataWatcherObject<>(9, DataWatcherRegistry.i), false);
        this.dataWatcher.register(new DataWatcherObject<>(6, DataWatcherRegistry.a), (byte) 0);
        this.dataWatcher.register(new DataWatcherObject<>(7, DataWatcherRegistry.c), 20.0F);
        this.dataWatcher.register(new DataWatcherObject<>(8, DataWatcherRegistry.b), 0);
        this.dataWatcher.register(new DataWatcherObject<>(10, DataWatcherRegistry.b), 0);
        this.dataWatcher.register(new DataWatcherObject<>(11, DataWatcherRegistry.c), 0.0F);
        this.dataWatcher.register(new DataWatcherObject<>(12, DataWatcherRegistry.b), 20);
        this.dataWatcher.register(new DataWatcherObject<>(13, DataWatcherRegistry.a), (byte) 127);
        this.dataWatcher.register(new DataWatcherObject<>(14, DataWatcherRegistry.a), (byte) 1);
        this.dataWatcher.register(new DataWatcherObject<>(15, DataWatcherRegistry.p), new NBTTagCompound());
        this.dataWatcher.register(new DataWatcherObject<>(16, DataWatcherRegistry.p), new NBTTagCompound());
    }

    public NPC(String name, Location location, Plugin plugin) {
        this(name, location, UUID.randomUUID(), ((CraftWorld) Bukkit.getWorlds().get(0)).getHandle(), plugin);
    }

    public NPC(String name, Location location, UUID uuid, Plugin plugin) {
        this(name, location, uuid, ((CraftWorld) Bukkit.getWorlds().get(0)).getHandle(), plugin);
    }

//    public Map<String, Object> serialize() {
//        Map<String, Object> result = new HashMap<>();
//        BeanInfo info = null;
//        try {
//            info = Introspector.getBeanInfo(this.getClass());
//        } catch (IntrospectionException e) {
//            e.printStackTrace();
//        }
//        for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
//            Method reader = pd.getReadMethod();
//            if (reader != null) {
//                try {
//                    result.put(pd.getName(), reader.invoke(this));
//                } catch (IllegalAccessException | InvocationTargetException e) {
//                    e.printStackTrace();
//                }
//            }
//        }
//        return result;
//    }

    /*
     *  view all the packet receivers
     */
    public List<Player> getRecipients() {
        return this.recipients;
    }


    /*
     *  get the signature and skin from the gameprofile
     */
    public Property getSkin() {
        if (this.gameprofile.getProperties().isEmpty())
            return null;
        return (Property) this.gameprofile.getProperties().get("textures").toArray()[0];
    }

    /*
     * get npc id
     */
    public int getEntityId() {
        return entityId;
    }


    /*
     * get npc location
     */
    public Location getLocation() {
        return location;
    }


    /*
     * get npc gameprofile
     */
    public GameProfile getGameprofile() {
        return gameprofile;
    }

    /*
     * get all the packet receivers
     */
    public Recipient getRecipient_type() {
        return recipient_type;
    }


    /*
     * get npc displayname above head
     */
    public String getDisplay_name() {
        return display_name;
    }

    /*
     * get npc displayname in tablist
     */
    public String getTablist_name() {
        return tablist_name;
    }

    /*
     * check if npc is deleted
     */
    public boolean isDestroyed() {
        return isDestroyed;
    }

    /*
     * get plugin
     */
    public Plugin getPlugin() {
        return plugin;
    }


    /*
     *  add Player so it receives the update packets
     */
    public void addRecipient(Player p) {
        this.recipients.add(p);
    }


    /*
     *  Remove Player that receives the update packets
     */
    public void removeRecipient(Player p) {
        this.recipients.remove(p);
    }


    /*
     *  toggle if the packet receivers are all the players online or a specicif group.
     *  If set to ALL (online) then the set/get recipient will have no effect
     */
    public void setRecipientType(Recipient recipient_type) {
        this.recipient_type = recipient_type;
    }


    /*
     *  spawn the npc, this should be the last function after init.
     */
    public void spawn(boolean tablist, boolean fix_head) {
        PacketPlayOutNamedEntitySpawn packet = new PacketPlayOutNamedEntitySpawn();
        this.setField(packet, "a", this.entityId);
        this.setField(packet, "b", this.gameprofile.getId());
        this.setField(packet, "c", location.getX());
        this.setField(packet, "d", location.getY());
        this.setField(packet, "e", location.getZ());
        this.setField(packet, "f", fix_head ? (byte) ((int) location.getYaw() * 256.0F / 360.0F) : 0);
        this.setField(packet, "g", fix_head ? (byte) ((int) location.getPitch() * 256.0F / 360.0F) : 0);
//        this.setField(packet, "i", this.dataWatcher);
        this.addToTabList();
        this.sendPacket(packet);
        this.isDestroyed = false;


        //Delay is required to commit the tablist changes
        new BukkitRunnable() {

            @Override
            public void run() {
                if (!tablist) removeFromTabList();
            }
        }.runTaskLater(this.plugin, 5);

    }


    /*
     *  put items in inventory, see https://www.google.nl/search?q=bukkit+inventory+slots&source=lnms&tbm=isch&sa=X&ved=0ahUKEwjr9v_FxvjaAhUFMuwKHQi7ALUQ_AUICigB&biw=1920&bih=974#imgrc=QUECAbUohgZxbM:
     *  for more info
     */
    public void setEquipment(EnumItemSlot slot, ItemStack item) {
        PacketPlayOutEntityEquipment packet = new PacketPlayOutEntityEquipment();
        this.setField(packet, "a", this.entityId);
        this.setField(packet, "b", slot);
        this.setField(packet, "c", item);
        this.sendPacket(packet);
    }


    /*
     *  set the name above the player
     */
    public void setDisplayNameAboveHead(String name) throws IOException {
        if(name.length() > 16) throw new IOException("Name cannot be longer than 16 chatacters.");
        this.display_name = name;
        this.reloadNpc();
    }


    /*
     *  set the name above the player and tablist.
     */
    public void setDisplayName(String name) throws IOException {
        this.setDisplayNameAboveHead(name);
        this.setTablistName(name);
    }


    /*
     *  set custom name in tablist
     */
    public void setTablistName(String name) {
        this.tablist_name = name;
        this.updateToTabList();
    }


    /*
     *  respawn the npc and refresh all comitted changes
     */
    public void reloadNpc() {
        this.updateProfile();
        if(!this.isDestroyed) {
            PacketPlayOutEntityDestroy packet = new PacketPlayOutEntityDestroy(this.entityId);
            this.sendPacket(packet);
            this.spawn(true, true);
        }
    }


    /*
     *  Update/Refresh the gameprofile that contains UUID, Name, Skin.
     */
    private void updateProfile() {
        Property skin = this.getSkin();
        this.gameprofile = new GameProfile(this.gameprofile.getId(), this.display_name);
        if (skin != null)
            this.setSkin(skin.getValue(), skin.getSignature());
    }


    /*
     *  set the texture and signature in the gameprofile, to submit it you must reload player.
     */
    public void setSkin(String texture, String signature) {
        this.gameprofile.getProperties().put("textures", new Property("textures", texture, signature));
    }



    /*
     *  remove npc from the recipient's tablist
     */
    public void removeFromTabList() {
        PacketPlayOutPlayerInfo packet = new PacketPlayOutPlayerInfo();
        PacketPlayOutPlayerInfo.PlayerInfoData data = packet.new PlayerInfoData(this.gameprofile, 0,
                EnumGamemode.NOT_SET, CraftChatMessage.fromString(tablist_name)[0]);
        List<PacketPlayOutPlayerInfo.PlayerInfoData> players = (List<PacketPlayOutPlayerInfo.PlayerInfoData>) getField(packet, "b");
        players.add(data);
        this.setField(packet, "a", PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER);
        this.setField(packet, "b", players);
        this.sendPacket(packet);
    }

    /*
     *  update npc to the recipient's tablist
     */
    public void updateToTabList() {
        PacketPlayOutPlayerInfo packet = new PacketPlayOutPlayerInfo();
        PacketPlayOutPlayerInfo.PlayerInfoData data = packet.new PlayerInfoData(this.gameprofile, 0,
                EnumGamemode.NOT_SET, CraftChatMessage.fromString(tablist_name)[0]);
        List<PacketPlayOutPlayerInfo.PlayerInfoData> players = (List<PacketPlayOutPlayerInfo.PlayerInfoData>) getField(packet, "b");
        players.add(data);
        this.setField(packet, "a", PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_DISPLAY_NAME);
        this.setField(packet, "b", players);
        this.sendPacket(packet);
    }


    /*
     *  add npc from the recipient's tablist
     */
    public void addToTabList() {
        PacketPlayOutPlayerInfo packet = new PacketPlayOutPlayerInfo();
        PacketPlayOutPlayerInfo.PlayerInfoData data = packet.new PlayerInfoData(this.gameprofile, 0,
                EnumGamemode.NOT_SET, CraftChatMessage.fromString(tablist_name)[0]);
        List<PacketPlayOutPlayerInfo.PlayerInfoData> players = (List<PacketPlayOutPlayerInfo.PlayerInfoData>) getField(packet, "b");
        players.add(data);
        this.setField(packet, "a", PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER);
        this.setField(packet, "b", players);
        this.sendPacket(packet);
    }


    /*
     * set player action, such as shift, onfire, ect recommending to use 'public void setAction(Action action)'.
     */
    public void setAction(byte action) {
        this.setMetaData(action);
    }


    /*
     * set player action, such as shift, onfire, ect.
     */
    public void setAction(Action action) {
        this.setMetaData(action.build());
    }


    /*
     * make sure the npc is near a bed or on it.
     */
//    public void setSleep(boolean state) {
//        if (state) {
//            Location bed = new Location(this.location.getWorld(), 0, 0, 0);
//            PacketPlayOutBed packet = new PacketPlayOutBed();
//            this.setField(packet, "a", this.entityId);
//            this.setField(packet, "b", new BlockPosition(bed.getX(), bed.getY(), bed.getZ()));
//
//            for (Player p : (this.recipient_type == Recipient.ALL ? Bukkit.getOnlinePlayers() : this.recipients)) {
//                ((CraftPlayer) p).sendBlockChange(bed, Material.BED_BLOCK, (byte) 0);
//                ((CraftPlayer) p).sendBlockChange(bed.add(0, 0, 1), Material.BED_BLOCK, (byte) 2);
//            }
//
//            this.sendPacket(packet);
//            this.teleport(location.clone().add(0, 0.3, 0), false);
//
//        } else {
//            this.setAnimation(NPCAnimation.LEAVE_BED);
//            this.teleport(location.clone().subtract(0, 0.3, 0), true);
//        }
//    }


    /*
     * delete the npc from the server.
     */
    public void destroy() {
        PacketPlayOutEntityDestroy packet = new PacketPlayOutEntityDestroy(this.entityId);
        this.removeFromTabList();
        this.sendPacket(packet);
        this.isDestroyed = true;
    }


    /*
     * set npc status such as die or hurt. I recommond to use method 'public void setStatus(NPCStatus status)'
     */
    public void setStatus(byte status) {
        PacketPlayOutEntityStatus packet = new PacketPlayOutEntityStatus();
        this.setField(packet, "a", this.entityId);
        this.setField(packet, "b", status);
        this.sendPacket(packet);
    }


    /*
     * set npc status such as die or hurt.
     */
    public void setStatus(NPCStatus status) {
        this.setStatus((byte) status.getId());
    }


    /*
     * set npc effect such as Night Vision or something else.
     */
    public void setEffect(MobEffect effect) {
        this.sendPacket(new PacketPlayOutEntityEffect(this.entityId, effect));
    }


    /*
     * set npc animation such as Swing arm ect. I recommend using method 'public void setAnimation(NPCAnimation animation)'
     */
    public void setAnimation(byte animation) {
        PacketPlayOutAnimation packet = new PacketPlayOutAnimation();
        this.setField(packet, "a", this.entityId);
        this.setField(packet, "b", animation);
        this.sendPacket(packet);
    }


    /*
     * set npc animation such as Swing arm ect.
     */
    public void setAnimation(NPCAnimation animation) {
        this.setAnimation((byte) animation.getId());
    }


    /*
     * teleport npc to different location
     */
    public void teleport(Location location, Boolean onGround) {
        PacketPlayOutEntityTeleport packet = new PacketPlayOutEntityTeleport();
        this.setField(packet, "a", this.entityId);
        this.setField(packet, "b", location.getX());
        this.setField(packet, "c", location.getY());
        this.setField(packet, "d", location.getZ());
        this.setField(packet, "e", (byte) location.getYaw());
        this.setField(packet, "f", (byte) location.getPitch());
        this.setField(packet, "g", onGround);
        this.sendPacket(packet);
        this.rotateHead(location.getPitch(), location.getYaw());
        this.location = location;
    }


    /*
     * rotate npc head to pitch and yaw.
     */
    public void rotateHead(float pitch, float yaw) {
        PacketPlayOutEntity.PacketPlayOutEntityLook packet = new PacketPlayOutEntity.PacketPlayOutEntityLook(this.entityId, getFixRotation(yaw), (byte) pitch, true);
        PacketPlayOutEntityHeadRotation packet_1 = new PacketPlayOutEntityHeadRotation();
        this.setField(packet_1, "a", this.entityId);
        this.setField(packet_1, "b", getFixRotation(yaw));
        this.sendPacket(packet);
        this.sendPacket(packet_1);
    }




    /*
     * These methods below are not usefull.
     */

    private <T> void setDataWatcherObject(DataWatcherObject<T> datawatcherobject, Object t0) {
        try {
            Method m = this.dataWatcher.getClass().getDeclaredMethod("registerObject", DataWatcherObject.class,
                    Object.class);
            m.setAccessible(true);
            m.invoke(this.dataWatcher, datawatcherobject, t0);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void setMetaData(byte data) {
        this.setDataWatcherObject(this.object_entity_state, data);
        PacketPlayOutEntityMetadata packet = new PacketPlayOutEntityMetadata(this.entityId, this.dataWatcher, true);
        sendPacket(packet);
    }

    private byte getFixRotation(float yawpitch) {
        return (byte) ((int) (this.location.getYaw() * 256.0F / 360.0F));
    }

    private Object getField(Object obj, String field_name) {
        try {
            Field field = obj.getClass().getDeclaredField(field_name);
            field.setAccessible(true);
            return field.get(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private void setField(Object obj, String field_name, Object value) {
        try {
            Field field = obj.getClass().getDeclaredField(field_name);
            field.setAccessible(true);
            field.set(obj, value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void sendPacket(Packet<?> packet, Player player) {
        ((CraftPlayer) player).getHandle().playerConnection.sendPacket(packet);
    }

    private void sendPacket(Packet<?> packet) {
        for (Player p : (this.recipient_type == Recipient.ALL ? Bukkit.getOnlinePlayers() : this.recipients)) {
            this.sendPacket(packet, p);
        }

    }

    enum Recipient {
        ALL, LISTED_RECIPIENTS
    }

    enum NPCAnimation {

        SWING_MAIN_HAND(0),
        TAKE_DAMAGE(1),
        LEAVE_BED(2),
        SWING_OFFHAND(3),
        CRITICAL_EFFECT(4),
        MAGIC_CRITICAL_EFFECT(5);

        private final int id;

        NPCAnimation(int id) {
            this.id = id;
        }

        public int getId() {
            return id;
        }

    }

    enum NPCStatus {

        HURT(2), DIE(3);

        private final int id;

        NPCStatus(int id) {
            this.id = id;
        }

        public int getId() {
            return id;
        }
    }

    public static class Action {

        private boolean on_fire, crouched, sprinting, invisible, glowing, flying_elytra;
        private byte result = 0;

        public Action(boolean on_fire, boolean crouched, boolean sprinting, boolean invisible, boolean glowing,
                      boolean flying_elytra) {
            this.on_fire = on_fire;
            this.crouched = crouched;
            this.sprinting = sprinting;
            this.invisible = invisible;
            this.glowing = glowing;
            this.flying_elytra = flying_elytra;
        }

        public Action() {
        }

        public boolean isOn_fire() {
            return on_fire;
        }

        public Action setOn_fire(boolean on_fire) {
            this.on_fire = on_fire;
            return this;
        }

        public boolean isCrouched() {
            return crouched;
        }

        public Action setCrouched(boolean crouched) {
            this.crouched = crouched;
            return this;
        }

        public boolean isSprinting() {
            return sprinting;
        }

        public Action setSprinting(boolean sprinting) {
            this.sprinting = sprinting;
            return this;
        }

        public boolean isInvisible() {
            return invisible;
        }

        public Action setInvisible(boolean invisible) {
            this.invisible = invisible;
            return this;
        }

        public boolean isGlowing() {
            return glowing;
        }

        public Action setGlowing(boolean glowing) {
            this.glowing = glowing;
            return this;
        }

        public boolean isFlying_elytra() {
            return flying_elytra;
        }

        public Action setFlying_elytra(boolean flying_elytra) {
            this.flying_elytra = flying_elytra;
            return this;
        }

        public byte build() {
            result = 0;
            result = add(this.on_fire, (byte) 0x01);
            result = add(this.crouched, (byte) 0x02);
            result = add(this.sprinting, (byte) 0x08);
            result = add(this.invisible, (byte) 0x20);
            result = add(this.glowing, (byte) 0x40);
            result = add(this.flying_elytra, (byte) 0x80);
            return result;
        }

        private byte add(boolean condition, byte amount) {
            return result += (condition ? amount : 0x00);
        }
    }
}

我需要这个 class 可序列化(最好使用 Gson)以在服务器关闭时保存实体。 目前我正在这样做:

Gson gson = new Gson();
for (NPC npc : npcs) {
    System.out.println(gson.toJson(npc));
}

我遇到堆栈溢出:

java.lang.WhosebugError: null
        at com.google.gson.internal.$Gson$Types.resolve($Gson$Types.java:383) ~[patched_1.16.1.jar:git-Paper-44]
        at com.google.gson.internal.$Gson$Types.resolve($Gson$Types.java:378) ~[patched_1.16.1.jar:git-Paper-44]

最后两行重复了很多次,直到出现:

at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:158) ~[patched_1.16.1.jar:git-Paper-44]
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:100) ~[patched_1.16.1.jar:git-Paper-44]
at com.google.gson.Gson.getAdapter(Gson.java:423) ~[patched_1.16.1.jar:git-Paper-44]

这些行也重复。 打印语句没有输出,NPC实例完好无损,我可以查询他们的字段。

我想 Gson 不能正确序列化 class 因为字段不是本地的并且递归调用一些东西直到堆栈被破坏。

有人可以帮我解决这个问题吗? :)

这可能是由不可序列化的字段之一引起的。继续将每个对象标记为瞬态,直到您可以成功序列化该对象,然后您就知道哪个是罪魁祸首了。

如果一个对象 A 引用了一个对象 B,它也引用了 A,就会发生这种情况。作为使用 minecraft 实体的示例,它看起来像:

PlayerA 有一个引用 Creeper1currentTarget 字段,Creeper1 有一个 Player 类型的 lastDamageBy 字段引用 PlayerA。在这种情况下,序列化 Creeper1 将尝试序列化 PlayerA,后者又会尝试通过它的 currentTarget 字段再次序列化 Creeper1。最终这将导致 WhosebugException.

在您的字段中的某处寻找这种循环引用。否则,您可以在 GSON 代码中放置一个断点并检查导致错误的确切字段。

一旦找到罪魁祸首,您可以删除该循环依赖性,或者使用 transient 关键字或 Exclusion Strategy 来定义如何或是否 deserialize/serialize 这些字段。