Java 如果对象被移动到另一个包或重命名,如何反序列化该对象?
考虑以下情况: 有一个序列化文件,由较旧版本的应用程序创建。不幸的是,已序列化的类的包已更改。现在我需要将这个文件中的信息加载到同一个类中,但位于不同的包中。此类已定义了Java 如果对象被移动到另一个包或重命名,如何反序列化该对象?,java,serialization,Java,Serialization,考虑以下情况: 有一个序列化文件,由较旧版本的应用程序创建。不幸的是,已序列化的类的包已更改。现在我需要将这个文件中的信息加载到同一个类中,但位于不同的包中。此类已定义了serialVersionUID且未更改(即兼容) 问题:是否可以使用任何技巧从该文件加载新类实例(除了将类复制到旧包中然后使用反序列化包装逻辑之外)?是否可以使用从移动/重命名类中恢复?如果没有,请解释原因。您最好的办法可能是重新创建旧类(名称、包和序列ID),以序列化形式读取,然后将数据复制到新对象的实例并重新序列化 如果您
serialVersionUID
且未更改(即兼容)
问题:是否可以使用任何技巧从该文件加载新类实例(除了将类复制到旧包中然后使用反序列化包装逻辑之外)?是否可以使用从移动/重命名类中恢复?如果没有,请解释原因。您最好的办法可能是重新创建旧类(名称、包和序列ID),以序列化形式读取,然后将数据复制到新对象的实例并重新序列化 如果您有很多这样的序列化对象,也许您可以编写一个小脚本来执行此操作,这样“模式更改”就可以一次性完成 另一种选择是恢复旧类并实现其readResolve方法以返回新类的实例(可能通过声明副本构造函数)。就个人而言,我认为我应该使用模式更改脚本,然后永久删除旧类 问题:是否可以加载 此文件中的新类实例 使用任何技巧(除了琐碎的 将类复制到旧包中并 然后使用反序列化包装器 逻辑 我不认为有任何其他“技巧”可以使用,而不涉及至少部分重新实现序列化协议 编辑:事实上,有一个钩子允许这样做,如果您控制反序列化过程,请参阅另一个答案 可以使用readResolve()来 从移动/重命名服务器恢复 上课?如果没有,请解释原因
不,因为反序列化机制会更早地失败,在它试图定位被反序列化的类的阶段-它无法知道另一个包中的类有一个它应该使用的
readResolve()
方法。我认为不可能做你想做的事情
序列化文件的格式保留类名。具体来说,它有下一个结构:
交流
协议版本号
对象数据
对象的类描述
类说明具有下一种格式:
全名
序列版本唯一ID(SHA1从
字段和方法(签名)
序列化选项
字段描述符
当您尝试反序列化对象时,序列化机制首先比较类名(并且您没有通过此步骤),然后比较SerialVersionId,并且只有在通过这两个步骤后,才会反序列化对象。可能:
class HackedObjectInputStream extends ObjectInputStream {
public HackedObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
ObjectStreamClass resultClassDescriptor = super.readClassDescriptor();
if (resultClassDescriptor.getName().equals("oldpackage.Clazz"))
resultClassDescriptor = ObjectStreamClass.lookup(newpackage.Clazz.class);
return resultClassDescriptor;
}
}
这还允许忽略serialVersionUIDs不匹配,甚至在类的字段结构发生更改时反序列化类。如果使用Cygnus Hex Editor,则可以手动更改包/类的名称 如果新名称(始终包括包)具有相同的大小,则可以用新名称替换旧名称,但如果大小已更改,则需要使用新长度更新名称前的前2个字符 右键单击标准数据类型并更改为Big-Endian 长度是一个有符号的单词 例如:
00 0E 70 61 63 6B 61 67 65 2E 53 61 6D 70 6C 65
. . p a c k a g e . S a m p l e
是如何编写包。示例的。00 0E表示14个字符,“package.Sample”的字符数为
如果要更改为newpackage.Sample,则将该字符串替换为:
00 12 6E 65 77 70 61 63 6B 61 67 65 2E 53 61 6D 70 6C 65
. . n e w p a c k a g e . S a m p l e
00 12表示18,字符数为“newpackage.Sample”
当然,您可以制作一个补丁程序来自动更新它。添加到十六进制编辑方式中 这对我很有用,用新的包名替换旧的包名比实现重写ObjectInputStream的类替换更容易。特别是因为还有匿名类 下面是一个脚本,它用二进制格式的新类路径替换旧类路径 以下是我的hexreplace.sh脚本的内容:
#!/bin/bash
set -xue
OLD_STR=$(echo -n $1 | hexdump -ve '1/1 "%.2X"')
NEW_STR=$(echo -n $2 | hexdump -ve '1/1 "%.2X"')
SRC_FILE=$3
DST_FILE=$4
TMP_FILE=$(mktemp /tmp/bin.patched.XXXXXXXXXX)
[ -f $SRC_FILE ]
hexdump -ve '1/1 "%.2X"' "$SRC_FILE" | sed "s/$OLD_STR/$NEW_STR/g" | xxd -r -p > "$TMP_FILE"
mv "$TMP_FILE" "$DST_FILE"
跑
当源文件和目标文件相同时,脚本正常工作。如果类移动到另一个命名空间,请使用该类而不是ObjectInputStream
class SafeObjectInputStream extends ObjectInputStream {
private final String oldNameSpace;
private final String newNameSpace;
public SafeObjectInputStream(InputStream in, String oldNameSpace, String newNameSpace) throws IOException {
super(in);
this.oldNameSpace = oldNameSpace;
this.newNameSpace = newNameSpace;
}
@Override
protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
ObjectStreamClass result = super.readClassDescriptor();
try {
if (result.getName().contains(oldNameSpace)) {
String newClassName = result.getName().replace(oldNameSpace, newNameSpace);
// Test the class exists
Class localClass = Class.forName(newClassName);
Field nameField = ObjectStreamClass.class.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(result, newClassName);
ObjectStreamClass localClassDescriptor = ObjectStreamClass.lookup(localClass)
Field suidField = ObjectStreamClass.class.getDeclaredField("suid");
suidField.setAccessible(true);
suidField.set(result, localClassDescriptor.getSerialVersionUID());
}
} catch(Exception e) {
throw new IOException("Exception when trying to replace namespace", e);
}
return result;
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (desc.getName().contains(oldNameSpace)) {
String newClassName = desc.getName().replace(oldNameSpace, newNameSpace);
return Class.forName(newClassName);
}
return super.resolveClass(desc);
}
}
如果您的某些类发生更改,它不会因StreamCorruptedException而失败。相反,它将尝试加载尽可能多的字段。您可以通过在类中实现readObject
方法来执行数据验证/升级
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Validate read data here
}
我同意这个答案,我还想补充一点,如果返回的类的全名与最初请求的全名不同,那么重写
ObjectInputStream#resolveClass(ObjectStreamClass)
将没有帮助(事实就是这样)@dma_k:事实上,在我看来,通过重写该方法,你可以实现你想要的。实际上,该方法显示了解决方案,但结果表明,我不可能欺骗反序列化过程,而只能欺骗模型。他不能以某种方式使用反射吗?@dabicho:如果只涉及一个类,那么这种方式就没有意义。您不能使用反射来临时更改类或类似的包。但是,如果您想要一个可重用的通用解决方案,可以使用反射和自定义注释让类说“我是com.oldpackage.OldClass的反序列化目标”,并让反序列化过程自动获取该目标。但是我怀疑这个问题在任何项目中出现的频率是否足以证明这一努力的合理性。+1是将readResolve()
注入“old”类的提示。但我假设我的问题是,已经考虑了恢复旧包中的类,我要求提供替代方案。我想,您的意思是对象数据
在对象的类描述之后执行
ObjectInputStream objectStream = new SafeObjectInputStream(inputStream, "org.oldnamespace", "org.newnamespace");
objectStream.readObject();
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Validate read data here
}