序列化和反序列化的概念
- 序列化:把java对象转换为字节序列的过程称为对象的序列化,这些字节序列可以被保存在磁盘上或通过网络传输,以备以后重新恢复原来的对象
- 反序列化:把字节序列恢复为对象的过程称为对象的反序列化。序列化机制使得对象可以脱离程序的运行而独立存在
序列化的功能/用途
- 持久化对象:Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,
这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中,以此实现该功能 。java中的对象的内部状态只保存在内存中,其生命周期最长与JVM的生命周期一样,即JVM停止之后,所有对象都会被销毁。 - 网络传输:在网络上传送对象的字节序列。
实际应用
- 在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
- 当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
实现
- java.io.Serializable接口,那么它就可以被序列化
- Externalizable:
Serializable接口
· 优点:内建支持
· 优点:易于实现
· 缺点:占用空间过大
· 缺点:由于额外的开销导致速度变比较慢
Externalizable接口
· 优点:开销较少(程序员决定存储什么)
· 优点:可能的速度提升
· 缺点:虚拟机不提供任何帮助,也就是说所有的工作都落到了开发人员的肩上。
在两者之间如何选择要根据应用程序的需求来定。Serializable通常是最简单的解决方案,但是它可能会导致出现不可接受的性能问题或空间问题;在出现这些问题的情况下,Externalizable可能是一条可行之路。
JDK类库中的序列化API
- java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
- java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
序列化与反序列化的编程实现
实现序列化接口的类
public class Person implements Serializable{
private static final long serialVersionUID = -1015228989208411177L;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
序列化过程
public class WriteObject {
public static void main(String[] args) {
ObjectOutputStream oos = null;
try {
//1.创建一个ObjectOutputStream
oos = new ObjectOutputStream(new FileOutputStream("/home/sunyan/object.txt"));
Person per = new Person("孙悟空", 500);
//2.将per对象写入输入流
oos.writeObject(per);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
if(oos != null){
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
反序列化
public class ReadObject {
public static void main(String[] args) {
ObjectInputStream ois = null;
try {
//1.创建一个ObjectInputStream输入流
ois = new ObjectInputStream(new FileInputStream("/home/sunyan/object.txt"));
//2.从输入流中读取一个Java对象,并将其强制类型转换为Person对象
Person p = (Person) ois.readObject();
System.out.println("名字为:" + p1.getName() + "\n年龄为:" + p1.getAge());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally{
try {
if (ois == null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
执行结果:
- 如果我们向文件中使用序列化机制写入了多个Java对象,使用反序列化机制恢复对象必须按照实际写入的顺序读取。
序列化
Person per1 = new Person("孙悟空", 500);
Person per2 = new Person("孙小妹", 50);
oos.writeObject(per1);
oos.writeObject(per2);
反序列化
Person p1 = (Person) ois.readObject();
Person p2 = (Person) ois.readObject();
System.out.println("名字为:" + p1.getName() + "\n年龄为:" + p1.getAge());
System.out.println("名字为:" + p2.getName() + "\n年龄为:" + p2.getAge());
执行结果
- 对象引用的序列化
如果类的属性不是基本类型或者String类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则该类也是不可序列化的,即使该类实现了Serializable,Externalizable接口。
public class Teacher implements Serializable{
private String name;
//类的属性是引用类型,也必须序列化。
//如果Person是不可序列化的,无论Teacher实现Serializable,Externalizable接口,则Teacher
//都是不可序列化的。
private Person student;
public Teacher(String name, Person student) {
super();
this.name = name;
this.student = student;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person getStudent() {
return student;
}
public void setStudent(Person student) {
this.student = student;
}
}
上述代码中,Teacher中有一个引用类型student,若Person未实现接口Serializable。即
public class Person implements Serializable{
}
则在序列化过程中,会报错
- Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
在Person中更改如下代码
private transient int age;
此时1中的代码,经序列化和反序列化后,执行结果如下
-
serialVersionUID
serialVersionUID: 字面意思上是序列化的版本号,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量。序列化 ID 是否一致,决定 虚拟机是否允许反序列化。
实现Serializable接口的类如果类中没有添加serialVersionUID,那么就会出现如下的警告提示
serialVersionUID有两种生成方式:
采用 Add default serial version ID
这种方式生成的serialVersionUID是1L,例如:
private static final long serialVersionUID = 1L;
采用 Add generated serial version ID这种方式生成的serialVersionUID是根据类名,接口名,方法和属性等来生成的,例如:
private static final long serialVersionUID = -1015228989208411177L;
对1中的代码序列化后,修改
private static final long serialVersionUID = -1015228989208411177L;
为
private static final long serialVersionUID = -1015228989208411178L;
此时,进行反序列化,会报错
反序列化漏洞危害
当应用代码从用户接受序列化数据,并试图反序列化改数据进行下一步处理时,会产生反序列化漏洞,其中最有危害性的就是远程代码注入。
这种漏洞产生原因是,java类ObjectInputStream在执行反序列化时,并不会对自身的输入进行检查,这就说明恶意攻击者可能也可以构建特定的输入,在 ObjectInputStream类反序列化之后会产生非正常结果,利用这一方法就可以实现远程执行任意代码。
最后再加一些相关知识点
1、声明为static和transient的成员数据不能被串行化,因为static代表类的状态,transient代表对象的临时数据。
2、要想将父类对象也序列化,就需要让父类也实现Serializable 接口。
3、服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。