JDK序列化机制及源码解读四:拓展知识

这是Java序列化机制及源码解读系列的第三篇,记录Java序列化的一些拓展知识。有新知识点也会持续更新。

1、transient

transient是一个关键字,用于修饰可序列化的变量,当用transient修饰时,代表该变量在序列化时被忽略。未序列化的变量在反序列化时对象的该属性值为默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.*;

public class SomeTeast {
public static void main(String[] args) {
try {
// 序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:\\1.txt"));
SerialTest serialTest = new SerialTest();
objectOutputStream.writeObject(serialTest);
objectOutputStream.flush();
objectOutputStream.close();

// 反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:\\1.txt"));
SerialTest serialTest1 = (SerialTest) objectInputStream.readObject();
System.out.println("----------------transient int i 未被序列化,值为默认值0--------------------");
System.out.println(serialTest1.i);
System.out.println("----------------String s 被序列化,值为123--------------------");
System.out.println(serialTest.s);
} catch (Exception e) {
e.printStackTrace();
}
}
}

class SerialTest implements Serializable{
public String s = "123";
public transient int i = 1;
}

输出结果:

image-20210702091255324

上述变量i因为有transient修饰,在序列化会被忽略,在反序列化时对象的该属性值为默认值0。

2、写入时替换对象-writeReplace和保护性恢复对象-readResolve

在Serializable的官方API中,有关于writeReplace和readResolve的描述:

writeObject方法负责为其特定的类编写对象的状态,以便相应的readObject方法可以恢复它。 可以通过调用out.defaultWriteObject来调用保存对象字段的默认机制。 该方法不需要关注属于其超类或子类的状态。 通过使用writeObject方法或通过使用DataOutput支持的原始数据类型的方法将各个字段写入ObjectOutputStream来保存状态。

readObject方法负责从流中读取并恢复类字段。 它可以调用in.defaultReadObject来调用恢复对象的非静态和非瞬态字段的默认机制。 defaultReadObject方法使用流中的信息将保存在流中的对象的字段分配给当前对象中相应命名的字段。 当处理类进化到添加新字段时,这将处理这种情况。 该方法不需要关注属于其超类或子类的状态。 通过使用writeObject方法或通过使用DataOutput支持的原始数据类型的方法将各个字段写入ObjectOutputStream来保存状态。

在前两部分学习ObjectOutputStream和ObjectInputStream中,其实也有提到:

ObjectOutputStream#writeObject0中使用hasWriteReplaceMethod()判断目标类是否有自定义writeReplace,有的话其实会调用writeReplace。

所以writeReplace和readResolve会代替writeObject和readObject进行序列化和反序列化。

writeReplace:

实现了writeReplace的类,会在写入对象流时被替换为writeReplace的返回对象。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.*;

public class RelpaceTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
MyReplace myReplace = new MyReplace();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:\\1.txt"));
objectOutputStream.writeObject(myReplace);
objectOutputStream.close();

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:\\1.txt"));
Object object = objectInputStream.readObject();
// 输出反序列化对象的Class对象,如果没有调用writeReplace,,应该是MyPlace,调用了的话应该是java.lang.String
System.out.println(object.getClass());
System.out.println(object.toString());
objectInputStream.close();
}
}

class MyReplace implements Serializable{

private static final long serialVersionUID = 5633032948089326039L;

public int i = 111;

private Object writeReplace(){
return new String("1234");
}
}

执行结果证明了的确调用了writeReplace,序列化对象被替换为writeReplace定义的对象 即 String实例。

image-20210702095112018

查看文件,对象被替换就已经在序列化阶段发生了,而非反序列化阶段。

image-20210702100857189

使用writePlace需要注意以下几点:

  • 一旦实现了writeReplace,则不再需要实现writeObject,实现了writeReplace在序列化时会自动别调用;
  • writeReplace返回的对象必须是可序列化的,因为实际序列化的对象就是writeReplace返回的对象;
readResolve:

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ResolveTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
MyResolve myResolve = new MyResolve();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:\\1.txt"));
objectOutputStream.writeObject(myResolve);
objectOutputStream.close();

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:\\1.txt"));
// 输出反序列化对象的Class对象,如果没有调用readReplace,应该是MyResolve,调用了的话应该是java.lang.String
Object object = objectInputStream.readObject();
System.out.println(object.getClass());
System.out.println(object.toString());
objectInputStream.close();
}
}

class MyResolve implements Serializable{

private static final long serialVersionUID = -3790334555111045021L;
public int i = 111;

private Object readResolve(){
// 直接替换成一个String实例
return new String("Be replaced when deserializable.");
}
}

输出结果如下图,对象被替换成String对象。

image-20210702100653080

查看序列化后的文件内容,发现在被序列化时并没有替换成String对象,对象被替换确实发生在反序列化阶段。

image-20210702100805324

readResolve注意点:

  • readResolve只会在反序列化阶段替换对象,替换的对象就是readResolve的返回对象;
  • 同writeReplace,实现了readResolve不需要在实现readObject;
  • 在WebLogic的反序列化漏洞的补丁中,多次使用readResolve进行黑名单过滤,过滤危险类。

3、readObjectNoData

官方描述

如果序列化流未将给定类列为反序列化对象的超类,则readObjectNoData方法负责初始化其特定类的对象的状态。 这可能发生在接收方使用与发送方不同的反序列化实例的类的版本的情况下,并且接收者的版本扩展了不被发送者版本扩展的类。 如果序列化流已被篡改,也可能发生这种情况; 因此,尽管存在“敌对”或不完整的源流,readObjectNoData可用于正确初始化反序列化对象。

readObjectNoData被用来解决反序列化异常问题,需要实现:private void readObjectNoData()

当反序列化遇到如下异常会自动调用该方法:

  • 序列化版本不兼容;

  • 输入流被篡改或者损坏。

参考链接:

https://blog.csdn.net/lirx_tech/article/details/51303966