如何使用 ByteArray 进行对象序列化和反序列化

How to use ByteArray for object serialisation and deserialisation

上下文

我正在做我的学生项目并构建用于回归测试的测试工具。

主要思想:在 运行 时间内使用 AOP 捕获所有 constructors/methods/functions 调用并将所有数据记录到数据库中。稍后检索数据,运行 constructors/methods/functions 以相同的顺序,并比较 return 个值。

问题

我正在尝试将对象(和对象数组)序列化为字节数组,将其作为 blob 记录到 PostgreSQL 中,稍后(在另一个 运行 时间)检索该 blob 并将其反序列化反对。但是当我在另一个 运行 时间反序列化数据时,它会发生变化,例如,我检索 int 而不是 boolean。如果我在相同的 运行 时间内执行完全相同的操作(序列化 - 插入数据库 - SELECT 从数据库 - 反序列化)一切似乎都正常工作。

我是这样记录数据的:

   private void writeInvocationRecords(InvocationData invocationData, boolean isConstructor) {
        final List<InvocationData> invocationRecords = isConstructor ? constructorInvocationRecords : methodInvocationRecords;
        final String recordsFileName = isConstructor ? "constructor_invocation_records.json" : "method_invocation_records.json";

        byte[] inputArgsBytes = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream out = null;
        try {
            out = new ObjectOutputStream(bos);
            out.writeObject(invocationData.inputArgs);
            out.flush();
            inputArgsBytes = bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                bos.close();
            } catch (IOException ex) {
                // ignore close exception
            }
        }

        byte[] returnValueBytes = null;
        ByteArrayOutputStream rvBos = new ByteArrayOutputStream();
        ObjectOutputStream rvOut = null;
        try {
            rvOut = new ObjectOutputStream(rvBos);
            rvOut.writeObject(invocationData.returnValue);
            rvOut.flush();
            returnValueBytes = rvBos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                rvBos.close();
            } catch (IOException ex) {
                // ignore close exception
            }
        }
        invocationRecords.add(invocationData);
        if (invocationRecords.size() >= (isConstructor ? CONSTRUCTORS_CACHE_SIZE : METHODS_CACHE_SIZE)) {
            List<InvocationData> tempRecords = new ArrayList<InvocationData>(invocationRecords);
            invocationRecords.clear();
            try {
                for (InvocationData record : tempRecords) {
                    SerialBlob blob = new javax.sql.rowset.serial.SerialBlob(inputArgsBytes);
                    SerialBlob rvBlob = new javax.sql.rowset.serial.SerialBlob(returnValueBytes);
                    psInsert.setString(1, record.className);
                    psInsert.setString(2, record.methodName);
                    psInsert.setArray(3, conn.createArrayOf("text", record.inputArgsTypes));
                    psInsert.setBinaryStream(4, blob.getBinaryStream());
                    psInsert.setString(5, record.returnValueType);
                    psInsert.setBinaryStream(6, rvBlob.getBinaryStream());
                    psInsert.setLong(7, record.invocationTimeStamp);
                    psInsert.setLong(8, record.invocationTime);
                    psInsert.setLong(9, record.orderId);
                    psInsert.setLong(10, record.threadId);
                    psInsert.setString(11, record.threadName);
                    psInsert.setInt(12, record.objectHashCode);
                    psInsert.setBoolean(13, isConstructor);
                    psInsert.executeUpdate();
                }
                conn.commit();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

这是我检索数据的方式:

        List<InvocationData> constructorsData = new LinkedList<InvocationData>();
        List<InvocationData> methodsData = new LinkedList<InvocationData>();

        Statement st = conn.createStatement();
        ResultSet rs = st.executeQuery(SQL_SELECT);
        while (rs.next()) {

            Object returnValue = new Object();
            byte[] returnValueByteArray = new byte[rs.getBinaryStream(7).available()];
            returnValueByteArray = rs.getBytes(7);
            final String returnType = rs.getString(6);
            ByteArrayInputStream rvBis = new ByteArrayInputStream(returnValueByteArray);
            ObjectInputStream rvIn = null;
            try {
                rvIn = new ObjectInputStream(rvBis);
                switch (returnType) {
                    case "boolean":
                        returnValue = rvIn.readBoolean();
                        break;
                    case "double":
                        returnValue = rvIn.readDouble();
                        break;
                    case "int":
                        returnValue = rvIn.readInt();
                        break;
                    case "long":
                        returnValue = rvIn.readLong();
                        break;
                    case "char":
                        returnValue = rvIn.readChar();
                        break;
                    case "float":
                        returnValue = rvIn.readFloat();
                        break;
                    case "short":
                        returnValue = rvIn.readShort();
                        break;
                    default:
                        returnValue = rvIn.readObject();
                        break;
                }
                rvIn.close();
                rvBis.close();

            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (rvIn != null) {
                        rvIn.close();
                    }
                } catch (IOException ex) {
                    // ignore close exception
                }
            }

            Object[] inputArguments = new Object[0];
            byte[] inputArgsByteArray = new byte[rs.getBinaryStream(5).available()];
            rs.getBinaryStream(5).read(inputArgsByteArray);

            ByteArrayInputStream bis = new ByteArrayInputStream(inputArgsByteArray);
            ObjectInput in = null;
            try {
                in = new ObjectInputStream(bis);
                inputArguments = (Object[])in.readObject();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (in != null) {
                        in.close();
                    }
                } catch (IOException ex) {
                    // ignore close exception
                }
            }

            InvocationData invocationData = new InvocationData(
                    rs.getString(2),
                    rs.getString(3),
                    (String[])rs.getArray(4).getArray(),
                    inputArguments,
                    rs.getString(6),
                    returnValue,
                    rs.getLong(8),
                    rs.getLong(9),
                    rs.getLong(10),
                    rs.getLong(11),
                    rs.getString(12),
                    rs.getInt(13)
                    );
            if (rs.getBoolean(14)) {
                constructorsData.add(invocationData);
            } else {
                methodsData.add(invocationData);
            }
        }
        st.close();
        rs.close();
        conn.close();

这个问题中固有的错误和误导性想法的爆炸式增长:

你的读写代码有问题。

available() 不起作用。好吧,它按照 javadoc 所说的那样做,如果您阅读 javadoc,并且非常仔细地阅读它,您应该得出正确的结论,那就是那是完全无用的。如果你调用 available(),你就搞砸了。你在这里这样做。更一般地说,您的读写代码不起作用。例如,.read(byteArr) 也不如您所想。见下文。

你试图做的事情背后的整个原则是行不通的

你不能'save the state'任意对象,如果你想推动这个想法,那么如果你可以,那么肯定不会以你的方式'重新做,一般来说,这是高级的 java,涉及破解 JDK 本身以获取它:想一想代表通过网络连接流动的数据的 InputStream。您认为此 InputStream 对象的 'serialization' 应该是什么样子?如果您将序列化视为 'just represent the underlying data in memory',那么您将得到一个代表 OS 'pipe handle' 的数字,可能还有一些 IP、端口和序列号。这是少量数据,所有这些数据 完全没用 - 它没有说明任何关于该连接的有意义的信息,并且这些数据根本不能用于重建它。即使在单个会话的 'scope' 中(即您序列化,然后几乎立即反序列化),因为网络是一个流,一旦您获取一个字节(或发送一个字节),它就消失了。唯一有用的,特别是对于 'lets replay everything that happened as a test' 的概念,序列化策略实际上涉及 'recording' 所有被拾取的字节,碰巧是在运行中。这不是你可以作为 'moment in time' 概念来做的事情,它是连续的。你需要一个记录所有事情的系统(它需要记录每个输入流,每个输出流,每次调用 System.currentTimeMillis() ,每次生成随机数等),然后需要使用结果当你的 API 被要求 'save' 任意状态时记录这一切。

序列化是对象需要选择的事情,并且他们可能必须编写自定义代码才能正确处理它。并非所有对象 都可以 甚至被序列化(如上所述,表示网络管道的 InputStream 是无法序列化的对象的一个​​示例),对于某些对象,序列化它们需要一些花哨的步法,而您唯一的希望就是为该对象提供支持的代码的作者付出了努力。如果他们不这样做,您将无能为力。

java 的序列化框架笨拙地捕捉了这两个概念。这确实意味着您的代码,即使您修复了其中的错误,在 JVM 中可能存在的大多数对象上也会失败。你的测试工具只能用来测试最简单的代码。

如果您不介意,请继续阅读。但如果没有,你需要彻底重新考虑你要用它做什么。

ObjectOutputStream 糟透了

这不仅仅是我的意见,openjdk 团队本身也广泛同意(当然,他们可能不会那样说)。 OOS 发出的数据是一个奇怪的、低效的、未指定的二进制 blob。除了花几年时间对协议进行逆向工程,或者只是反序列化它(这需要拥有所有 classes 和 JVM - 这可能是一个可以接受的负担,你无法以任何可行的方式分析这些数据,取决于您的用例)。

与例如Jackson 将数据序列化为 JSON,您可以用眼球或任何语言解析,甚至没有相关的 class 文件。您可以自己构建 'serialized JSON' 而无需先拥有一个对象(出于测试目的,这听起来是个好主意,不是吗?您也需要测试这个测试框架!)。

如何修复此代码?

如果您理解以上所有注意事项,并且仍然以某种方式得出结论,这个项目,如所写并继续使用 ObjectOutputStream API 仍然是您想要做的(我真的,真的怀疑这是正确的选择):

使用较新的 API。 available() 不是 return 那个 blob 的大小。 read(someByteArray) 保证填满整个字节数组。只需阅读 java 文档,它会详细说明。

无法通过询问输入流来确定输入流的大小。您也许可以询问数据库本身(通常,LENGTH(theBlobColumn) 在 SELECT 查询中效果很好。

如果你不知何故(例如使用 LENGTH(tbc))知道完整的大小,你可以使用 InputStream 的 readFully 方法,这将 实际上 读取所有字节,对比.read,至少读取1,但不保证全部读取。这个想法是:它将读取可用的最小块。想象一个网络管道,其中字节以每秒一个字节的速度滴入网卡的缓冲区。如果到目前为止已经输入了 250 个字节并且您调用 .read(some500SizeByteArr),那么您将得到 250 个字节(填充了 500 个字节中的 250 个,并且 250 是 returned)。如果你调用 .readFully(some500SizeByteArr),那么代码会等待大约 250 秒,然后 returns 500,并填充所有 500 个字节。这就是区别,这也解释了为什么 read 会这样工作。换句话说:如果你不检查 read() 是什么 return,你的代码肯定是坏的。

如果您 不知道 有多少数据,您唯一的选择是 while 循环,或者调用执行该操作的辅助方法。您需要制作一个临时字节数组,然后在循环中继续调用 read 直到它 returns -1。对于每个循环,将该数组中的字节从 0 到(无论 read 调用 returned 是什么),并将这些字节发送到其他地方。例如,ByteArrayOutputStream.

Class匹配

when I deserialize data in another runtime it changes and, for example, instead of boolean, I retrieve int

java 序列化系统并没有神奇地改变你身上的东西。好吧,放一个别针。第一个 运行 中可用的 class 文件(您将 blob 保存在数据库中的位置)很可能与第二个 运行 中的文件不同。瞧,问题。

更一般地说,这是序列化中的问题。如果你序列化,比如说,class Person {Date dob; String name;},然后在软件的更高版本中你意识到使用 j.u.Date 来存储出生日期是一个非常愚蠢的想法,因为 Date 是一个不幸的命名 class(它代表的是一个瞬间,根本不是日期),所以你用 LocalDate 代替它,从而以 class Person{LocalDate dob; String name;} 结束,那么你如何处理这个问题您现在想要反序列化在 Person.class 文件仍有损坏的 Date dob; 字段时生成的 BLOB?

答案是:不能。 Java 内置的序列化机制会在这里抛出异常,它不会尝试这样做。这是 serialVersionUID 系统:Classes 有一个 ID,更改它们的任何内容(例如那个字段)都会更改此 ID; ID 存储在序列化数据中。如果 ID 不匹配,则无法进行反序列化。您可以强制 ID(创建一个名为 serialVersionUID 的字段 - 您可以在网上搜索如何执行此操作),但您仍然会收到错误消息,java 的反序列化程序将尝试反序列化将日期对象放入 LocalDate dob; 字段,当然会失败。

Classes可以自己写代码来解决这个问题。这很重要并且与您无关,因为您正在构建一个框架并且可能无法弹出并为您的测试框架的用户群的自定义 class 文件编写代码。

我告诉过你在 'the serialization mechanism isnt going to magically change types on you' 中插入一个图钉。付出足够的努力来覆盖 serialVersionUID 等,你 can 最终会在那里。但那是因为您编写的代码混淆了类型,例如在您的 readObject 实现中(同样,在网络上搜索 java 的序列化机制,readObject/writeObject - 或者只是开始阅读 java.io.Serializable 的 javadoc,这是一个很好的起点)。

风格问题

你毫无目的地创建对象,你似乎对 variable/reference 和对象之间的区别有些困难。您没有使用 try-with-resources。您的 SELECT 调用方式表明您存在 SQL 注入安全问题。 e.printStackTrace() 因为 catch 块中的 line line 总是 不正确。