默认序列化方法
基本形式
基本形式非常简单,要让类支持序列化,只需要实现Serializable接口即可,该接口是一个标记接口。序列化的类的所有实例域必须都是可序列化的,也就是说,实例域为引用类型,则该类型也必须实现Serializable。
public class WriteObject {
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) { //第一次序列化person
// 序列化
Person person = new Person("9龙", 23);
// 反序列化
Person p1 = (Person) ios.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Person implements Serializable {
private int age;
private String name;
public Person(String name, int age) {
this.age = age;
this.name = name;
}
}
transient
用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
序列化版本号
想象一个场景,某个应用中的某个类的对象被序列化并存储,随后该类被改变——添加了字段,当应用再次运行,试图读取并反序列化之前的对象,由于应用不知道类被改变,这时候序列化就会出问题。
为了解决这个问题,Java引入了版本控制。可以给类添加一个serialVersionUID,用来标志类是否被改变,如果开发者不添加,JVM会给每个类默认添加一个UID。
自定义序列化方法
readObject
和writeObject
可以在类中增加readObject
和writeObject
方法,这样一来,ObjectStream就会在序列化的实际调用自定义的序列化/反序列化方法。
static class Person implements Serializable {
transient private Integer age = null;
private void writeObject(ObjectOutputStream out) throws IOException {
// 默认序列化方法
out.defaultWriteObject();
// 自定义序列化方法
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 默认反序列化方法
in.defaultReadObject();
// 自定义反序列化方法
age = in.readInt();
}
}
Externalizable
另一种方式是实现接口Externalizable,这种方式同样可以自定义序列化反序列化方法。但这种方式需要提供无参构造器,给ObjectStream反射调用(注意,后文也会说,Serializable是JVM直接构造对象,Externalizable是调用无参构造器,后者的优势在于可以利用无参构造器约束类)。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
writeReplace
和readResolve
第三种方式是在类中提供writeReplace
和readResolve
方法,该方法会在ObjectStream序列化/反序列化才被调用,用于替换原来序列化/反序列化之后的对象。常常用来保证单例模式反序列化一致性。
public class SerializeDemo05 {
// 其他内容略
static class Person implements Serializable {
// 添加此方法
private Object readResolve() {
return instatnce;
}
// 其他内容略
}
// 其他内容略
}
原理
你可能会好奇,这些方法都是怎么起作用但——似乎除了Externalizable之外,都无法通过多态机制来调用我们都实现。其实方法非常简单直接——ObjectStream和ObjectStreamClass中写死的,通过反射技术获得相应的方法,在合适的时候调用。可以看下以下源码。
if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
cons = getSerializableConstructor(cl);
writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class },
Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },
Void.TYPE);
readObjectNoDataMethod = getPrivateMethod(
cl, "readObjectNoData", null, Void.TYPE);
hasWriteObjectData = (writeObjectMethod != null);
}
domains = getProtectionDomains(cons, cl);
writeReplaceMethod = getInheritableMethod(
cl, "writeReplace", null, Object.class);
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
序列化算法原理
JVM构造对象
需要注意,实现Serializable的类,在反序列化的时候,不会调用构造方法,而是直接由JVM生成。这一点非常重要,也给类的约束在构造方法中约定,那么默认的反序列化方法就无法保证这种约束。
另外一点,Java的序列化机制需要注意的就是,它会在序列化/反序列过程中调用一系列类自身的方法,例如上面提到的readObject, writeObject, wirtReplace, readResolve
等等,这给Java序列化机制带来了方便——看起来实现序列化只需要实现Serializable,毫不费力,但也给Java序列化机制带来了很多风险——这些类自身的方法,是调用方完全无法控制的。攻击者通过巧妙的设计就可以利用这个机制进行反序列化攻击。
同一对象多次序列化机制
对于同一对象,Java序列化机制只会序列化一次,所以你会看到不可思议的“意外”。以下引用自参考文章一。
public class WriteObject {
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) { //第一次序列化person
Person person = new Person("9龙", 23);
oos.writeObject(person);
System.out.println(person); //修改name
person.setName("海贼王");
System.out.println(person); //第二次序列化person
oos.writeObject(person); //依次反序列化出p1、p2
Person p1 = (Person) ios.readObject();
Person p2 = (Person) ios.readObject();
System.out.println(p1 == p2); // true
System.out.println(p1.getName().equals(p2.getName())); //true
// output
// Person{age=23, name='9龙'}
// Person{age=23, name='海贼王'}
// true
// true
} catch (Exception e) {
e.printStackTrace();
}
}
}
要理解上述结果,必须理解序列化算法:
- 所有保存到磁盘的对象都有一个序列化编码号
- 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
- 如果此对象已经序列化过,则直接输出编号即可。
图示:
缺陷
- 无法跨语言:Java 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。
- 容易被攻击:对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 hashCode 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。例如下面这个案例就可以很好地说明。
- 序列化后的流太大:Java 序列化中使用了 ObjectOutputStream 来实现对象转二进制编码,编码后的数组很大,非常影响存储和传输效率。
- 序列化性能太差:Java 的序列化耗时比较大。序列化的速度也是体现序列化性能的重要指标,如果序列化的速度慢,就会影响网络通信的效率,从而增加系统的响应时间。
- 序列化编程限制:
- Java 官方的序列化一定需要实现 Serializable 接口。
- Java 官方的序列化需要关注 serialVersionUID
参考
https://juejin.cn/post/6844903848167866375#heading-9
https://dunwu.github.io/javacore/io/java-serialization.html#_5-java-%E5%BA%8F%E5%88%97%E5%8C%96%E9%97%AE%E9%A2%98
https://blog.csdn.net/Leon_cx/article/details/81517603