dma*_*a_k 34 java serialization
考虑以下情况:
有一个序列化文件,由旧版本的应用程序创建.不幸的是,已经序列化了类的包已更改.现在我需要将此文件中的信息加载到同一个类中,但位于不同的包中.此类已serialVersionUID定义且未更改(即兼容).
问题:是否可以使用任何技巧从该文件加载新的类实例(除了将类复制到旧包中然后使用反序列化包装器逻辑)?可以用来readResolve()从移动/重命名类中恢复吗?如果没有,请解释原因.
Igo*_*din 51
有可能的:
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;
}
}
Run Code Online (Sandbox Code Playgroud)
如果更改了字段结构,这也允许忽略serialVersionUID不匹配甚至反序列化类.
如果使用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
Run Code Online (Sandbox Code Playgroud)
是如何写的package.Sample.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
Run Code Online (Sandbox Code Playgroud)
00 12表示18,字符数"newpackage.Sample"有.
当然,您可以让修补程序自动更新.
如果您的类移动到另一个命名空间,请使用此类而不是 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);
}
}
Run Code Online (Sandbox Code Playgroud)
您可以按如下方式使用它:
ObjectInputStream objectStream = new SafeObjectInputStream(inputStream, "org.oldnamespace", "org.newnamespace");
objectStream.readObject();
Run Code Online (Sandbox Code Playgroud)
如果您的某些类发生更改,它不会因 StreamCorruptedException 而失败。相反,它会尝试加载尽可能多的字段。您可以通过readObject在类中实现方法来执行数据验证/升级。
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Validate read data here
}
Run Code Online (Sandbox Code Playgroud)
可能最好的办法是重新创建旧类(名称,包和序列ID),以序列化形式读取,然后将数据复制到新对象的实例并重新序列化.
如果您有很多这些序列化对象,也许您可以编写一个小脚本来执行此操作,以便"架构更改"一次完成.
另一种选择是恢复旧类并实现其readResolve方法以返回新类的实例(可能通过声明复制构造函数).就个人而言,我认为我会选择模式更改脚本,然后删除旧类.
问题:是否可以使用任何技巧从该文件加载新的类实例(除了将类复制到旧包中然后使用反序列化包装器逻辑)?
我不认为你可以使用任何其他"技巧",不涉及至少部分重新实现序列化协议.
可以使用readResolve()从移动/重命名类中恢复吗?如果没有,请解释原因.
不,因为反序列化机制会更早地失败,在它试图找到被反序列化的类的阶段 - 它无法知道不同包中的类是否具有readResolve()它应该使用的方法.