上下文
我正在做我的学生项目,并为回归测试构建一个测试工具。
主要思想:在运行时使用AOP捕获所有构造函数/方法/函数调用,并将所有数据记录到数据库中。稍后检索数据,按相同顺序运行构造函数/方法/函数,并比较返回值。
问题
我试图将对象(和对象数组)序列化为字节数组,将其作为blob记录到PostgreSQL中,然后(在另一个运行时)检索该blob并将其反序列化回对象。但是,当我在另一个运行时反序列化数据时,它会发生变化,例如,我检索的不是boolean
,而是int
。如果我在同一个运行时中执行完全相同的操作(序列化-插入数据库-从数据库中选择-反序列化),那么一切似乎都能正常工作。
以下是我如何记录数据:
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)
也不会做你认为它会做的事情。请参见下文。
你试图做的事情背后的整个原则都不起作用
你不能"保存"任意对象的状态,如果你想推动这个想法,那么如果你可以,那么肯定不会以你正在做的方式,而且通常这是高级java,需要破解JDK本身来实现它:想想一个InputStream,它表示通过网络连接流动的数据。您认为这个InputStream对象的"序列化"应该是什么样子?如果您将序列化视为"只表示内存中的底层数据",那么您将得到一个表示操作系统"管道句柄"的数字,可能还有一些IP、端口和序列号。这是一个很小的数据量,所有这些数据都是完全无用的-它没有说明任何关于该连接的有意义的东西,这些数据根本不能用来重建它。即使在单个会话的"范围"内(即,在那里序列化,然后几乎立即反序列化),因为网络是一个流,一旦你获取了一个字节(或发送了一个),它就消失了。唯一有用的是,特别是对于"让我们回放作为测试发生的一切"的概念,序列化策略实际上涉及到在运行中"记录"所有拾取的字节。这不是一件你可以作为"时间瞬间"概念来做的事情,它是连续的。您需要一个记录所有内容的系统(它需要记录每个输入流、每个输出流、每次调用System.currentTimeMillis()
时、每次生成随机数时等),然后当API被要求"保存"任意状态时,需要使用记录结果。
相反,序列化是对象需要选择的事情,并且它们可能必须编写自定义代码才能正确处理它。并不是所有的对象都可以被序列化(如上所述,表示网络管道的InputStream是无法序列化的对象的一个示例),对一些人来说,序列化它们需要一些花哨的技巧,你唯一的希望就是为这个对象提供动力的代码的作者投入到这项工作中。如果他们不这样做,你就无能为力。
java的序列化框架笨拙地捕捉到了这两个概念。这确实意味着,即使你修复了代码中的错误,你的代码也会在JVM中存在的大多数对象上失败。您的测试工具只能用于测试最简单的代码。
如果你对此满意,请继续阅读。但如果不满意,你需要彻底反思你将如何处理这件事。
ObjectOutputStream糟透了
这不仅仅是我的观点,openjdk团队本身也大致同意(当然,他们可能不会这么说)。OOS发出的数据是一个奇怪的、低效的、未被充分利用的二进制blob。除了花几年时间对协议进行逆向工程,或者只是对其进行反序列化(这需要拥有所有的类和JVM——这可能是一个可以接受的负担,取决于您的用例)之外,您无法以任何可行的方式分析这些数据。
与Jackson相反,Jackson将数据序列化为JSON,您可以用眼球或任何语言解析JSON,甚至不需要相关的类文件。您可以自己构建"序列化JSON",而无需首先拥有对象(出于测试目的,这听起来是个好主意,不是吗?您也需要测试这个测试框架!)。
如何修复此代码
如果你理解了上面的所有注意事项,并且仍然以某种方式得出结论,这个项目,正如所写的,并继续使用ObjectOutputStream API,仍然是你想要做的(我真的,真的怀疑这是正确的调用):
使用更新的API。available()
不会返回该blob的大小。read(someByteArray)
不能保证填充整个字节数组。只要阅读javadoc,它就会把它说清楚。
没有办法通过询问输入流来确定输入流的大小。您可以询问数据库本身(通常,LENGTH(theBlobColumn)
在SELECT查询中非常有效
如果您以某种方式(例如使用LENGTH(tbc))知道完整大小,则可以使用InputStream的readFully
方法,该方法将实际读取所有字节,而read
至少读取1,但不能保证读取所有字节。其思想是:它将读取可用的最小块。想象一下,在一个网络管道中,字节以每秒一个字节的速度滴入网卡的缓冲区。如果到目前为止已经有250个字节流入,并且您调用.read(some500SizeByteArr)
,那么您将得到250个字节(500个字节中的250个被填充,并且返回250
)。如果调用.readFully(some500SizeByteArr)
,那么代码将等待大约250秒,然后返回500,并填充所有500个字节。这就是区别,这也解释了read
的工作方式。换句话说:如果您不检查read()
返回的内容,那么您的代码肯定已损坏。
如果您不知道有多少数据,那么您唯一的选择就是使用while
循环,或者调用一个辅助方法。您需要创建一个临时字节数组,然后在循环中继续调用read,直到它返回-1。对于每个循环,取该数组中从0到的字节(无论read
调用返回什么),并将这些字节发送到其他地方。例如,一个ByteArrayOutputStream
。
类匹配
当我在另一个运行时反序列化数据时,它会发生变化,例如,我检索int 而不是布尔值
java序列化系统不会神奇地改变你身上的东西。好吧,把一个针。最有可能的是,第一次运行中可用的类文件(您将blob保存在数据库中的位置)与第二次运行中的文件不同。沃伊拉,问题。
更普遍地说,这是序列化中的一个问题。如果你序列化了class Person {Date dob; String name;}
,然后在软件的后续版本中,你会意识到使用j.u.Date
来存储出生日期是一个非常愚蠢的想法,因为date是一个不幸命名的类(它代表的是一个瞬间,而不是一个日期),所以你用LocalDate
代替它,从而以class Person{LocalDate dob; String name;}
结束,那么如何处理现在想要反序列化在Person.class
文件仍然有损坏的Date dob;
字段时返回的BLOB的问题呢?
答案是:你不能。Java的内置序列化机制会在这里抛出一个异常,它不会尝试这样做。这是serialVersionUID
系统:类有一个ID,更改它们的任何内容(例如该字段)都会更改该ID;ID被存储在串行化的数据中。如果ID不匹配,则无法进行反序列化。你可以强制ID(创建一个名为serialVersionUID
的字段-你可以在网上搜索如何做到这一点),但你仍然会得到一个错误,java的反序列化器将尝试将Date对象反序列化为LocalDate dob;
字段,当然会失败。
类可以编写自己的代码来解决这个问题。这是不平凡的,与您无关,因为您正在构建一个框架,并且可能无法弹出并为测试框架的用户库的自定义类文件编写代码。
我告诉过你要在"序列化机制不会神奇地改变你的类型"中加上一个密码。在重写serialVersionUID等方面付出足够的努力,就可以结束。但这可能是因为您编写的代码混淆了类型,例如在readObject
实现中(同样,在web上搜索java的序列化机制readObject/writeObject,或者只是开始阅读java.io.Serializable
的javadoc,这是一个很好的起点)。
风格问题
如果创建对象是没有目的的,那么在区分变量/引用和对象时似乎会遇到一些问题。您没有使用try with resources。SELECT调用的方式表明存在SQL注入安全问题。e.printStackTrace()
作为catch块中的换行符总是不正确。