Java的序列化和反序列化
1、探索Serializable
和对象的序列化的关系
所谓序列化就是将内存中的Java对象转换成字节,从而持久化到硬盘或者用于网络的传输。
接下来就来看一下Java的是怎么将一个对象进行序列化的。(此处不借助第三方的包)
正常的一个实体类:
import java.io.Serializable;
/**
* Description:
*
* @author:qjx
* @date:2021/12/15
*/
public class Food implements Serializable {
private String name;
public Food(String name) {
this.name = name;
}
//setter getter
}
测试代码如下:
ObjectOutputStream
:对象的序列化流作用:把对象转成字节数据的输出到文件中保存,对象的输出过程称为序列化,可实现对象的持久存储。
private static void serialize() throws IOException {
Food food = new Food("唐僧肉~~");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\food.txt"));
oos.writeObject(food);
oos.close();
}
这样我们就得到一个对象转出的二进制文件food.txt
,相反的可以将该文家反序列化到内存,测试代码如下:
ObjectInputStream
:反序列化流将之前使用 ObjectOutputStream 序列化的原始数据恢复为对象,以流的方式读取对象。
private static void deserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("food.txt"));
Food o = (Food)ois.readObject();
System.out.println(o.getName()); //唐僧肉~~
}
如果我们恰巧在实体类在序列化是并没有实现Serializable
接口的话,就会得到这样的异常:java.io.NotSerializableException
如下图:
我们点开Serializable
接口的可以看到就是一个空的接口:
官方给出的接口的部分文档如下:
类的可序列化由实现 java.io.Serializable 接口的类启用。 未实现此接口的类将不会对其任何状态进行序列化或反序列化。 可序列化类的所有子类型本身都是可序列化的。 序列化接口没有方法或字段,仅用于标识可序列化的语义。
可以看出Serializable
就是一个普通java
类是否可以被序列化的flag
具体报错我们跟进ObjectOutputStream
类。
我们跟进到writeObject0 line 1184
,如下图,从而得出String
、Array
、Enum
、和Serializable
只有这些类型的类型的对象才能进行write,否则就会抛出此异常,很明显我们的Food
类并不是其中之一。
总结:在对象序列化为二进制文件时只有以下类型的类的对象可以被序列化
String
Array
Enum
Serializable
2、探索serialVersionUID
序列号与对象序列化的关系
众所周知,在实现了Serializable
接口的类中都有一个serialVersionUID
字段,如下图:
如果我们没有明显声明,运行时也会自动生成该字段,那么serialVersionUID
号有何用?
验证过程:在上面我我们已经获取了food.txt
序列化的类,在反序列化时我们增加一个字段,如下:
public class Food implements Serializable {
private String name;
private String id; //新增id字段验证serialVersionUID作用
//...
}
这是我们试着将该类反序列化到内存就看到了下面的报错:
java.io.InvalidClassException:xxx.Food; local class incompatible: stream classdesc serialVersionUID = 6153504616478967942, local class serialVersionUID = -6029597911198096662
这说的就是在这两个Food类不相容,原因是serialVersionUID
不相等。
跟进到ObjectStreamClass
类的对应报错位置:
suid
就是反序列化出来的类的serialVersionUID
, osc
就是本地类的序列号,想到咱们的类并未声明序列号,所以跟进就看到下面赋值代码,也就是自动生成serialVersionUID
的代码:
总结:
-
1、serialVersionUID是序列化前后的唯一标识符
-
2、默认如果没有人为显式定义过
serialVersionUID
,那编译器会为它自动声明一个! -
3、凡是实现
Serializable
接口的类中,最好都要手动添加此字段。
注意:
- 1、凡是被
static
修饰的字段是不会被序列化的 - 2、凡是被
transient
修饰符修饰的字段也是不会被序列化的
3、序列化的受控和加强
3.1、约束性加持
从上面的过程可以看出,序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,那反序列化出来的对象就会有一定风险了。
毕竟反序列化也相当于一种 “隐式的”对象构造 ,因此我们希望在反序列化时,进行受控的对象反序列化动作。
那怎么个受控法呢?
答案就是: 自行在需要反序列化的类中编写readObject()
函数,用于对象的反序列化构造,从而提供约束性。
既然自行编写readObject()
函数,那就可以做很多可控的事情:比如各种判断工作。
还以上面的Food
类为例,规定我们的name中不能出现 肉
字,我们可以自行编写readObject()
函数用于反序列化的控制:
private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {
// 调用默认的反序列化函数
objectInputStream.defaultReadObject();
// 手工检查反序列化后name中不能出现 `肉` 字
if( name!= null && name.contains("肉")) {
throw new IllegalArgumentException("name不可以出现肉哦!");
}
}
为什么自定义的private
的readObject()
方法可以被自动调用,我们跟进ObjectStreamClass
源码:
发现在ObjectStreamClass
构造时就进行私有方法的反射获取了!!!
3.2、单例模式增强
一个容易被忽略的问题是:可序列化的单例类有可能并不单例!
举个代码小例子就清楚了。
比如这里我们先用java
写一个常见的「静态内部类」方式的单例模式实现:
public class Singleton implements Serializable {
private static final long serialVersionUID = -1576643344804979563L;
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}
public static synchronized Singleton getSingleton() {
return SingletonHolder.singleton;
}
}
然后写一个验证主函数:
public class Test2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream objectOutputStream =
new ObjectOutputStream(
new FileOutputStream( new File("singleton.txt") )
);
// 将单例对象先序列化到文本文件singleton.txt中
objectOutputStream.writeObject( Singleton.getSingleton() );
objectOutputStream.close();
ObjectInputStream objectInputStream =
new ObjectInputStream(
new FileInputStream( new File("singleton.txt") )
);
// 将文本文件singleton.txt中的对象反序列化为singleton1
Singleton singleton1 = (Singleton) objectInputStream.readObject();
objectInputStream.close();
Singleton singleton2 = Singleton.getSingleton();
// 运行结果竟打印 false !
System.out.println( singleton1 == singleton2 );
}
}
运行后我们发现:反序列化后的单例对象和原单例对象并不相等了,这无疑没有达到我们的目标。
解决办法是:在单例类中手写readResolve()
函数,直接返回单例对象,来规避之:
private Object readResolve() {
return SingletonHolder.singleton;
}
这样一来,当反序列化从流中读取对象时,readResolve()
会被调用,用其中返回的对象替代反序列化新建的对象。