JAVA序列化机制
什么是序列化和反序列化?
-
序列化:把Java对象转换为字节序列的过程。
-
反序列化:把字节序列恢复为Java对象的过程。
市面上的几种序列化方式
Java对象是在JVM
中生成的,如果需要远程传输或保存到硬盘上,就需要将Java对象转换成可传输的文件流。
市面上目前有的几种转换方式:
-
利用Java的序列化功能序列成字节(字节流)也就是接下来要讲的。一般是需要加密传输时才用。
-
将对象包装成
Json
字符串(字符流)转
Json
工具有Jackson
、FastJson
或者GJson
,它们各有优缺点:- Jackson:
Map
、List
的转换可能会出现问题。转复杂类型的Bean
时,转换的Json
格式不是标准的Json
格式。适合处理 大文本Json。 - FastJosn:速度最快。将复杂类型的
Bean
转换成Json
可能会有问题:引用类型如果没有引用被出错。适合对性能有要求的场景。 - GJson:功能最全,可以将复杂的
Bean
和Json
字符串进行互转。性能上面比FastJson
有所差距。适合处理小文本Json
,和对于数据正确性有要求的场景。
- Jackson:
-
protoBuf
工具(二进制)性能好,效率高,字节数很小,网络传输节省IO。但二进制格式可读性差。
为什么需要序列化和反序列化?
之所以需要序列化和反序列化,主要是因为Java对象是在JVM
中生成的,是内存中的数据,如果需要把对象的字节序列远程传输或保存到硬盘上时,你就需要将Java对象转换成二进制流。 这个转换过程就是序列化。
假如别人传给你一个二进制流数据,当你想要恢复成内存中的对象时,你就需要反序列化
Java平台允许我们在内存中创建可复用的Java对象,但只有当JVM
(Java虚拟机)处于运行时,这些对象才可能存在,也就是这些对象的生命周期不会比JVM
的生命周期更长。但在现实应用中,就可能要求在JVM
停止运行之后能够保存指定的对象(持久化对象),并在将来重新读取被保存的对象。
网络通信时,无论是何种类型的数据,都会转成字节序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
Java原生序列化实现
实现了如下两个接口之一的类的对象才能被序列化:
-
Serializable
-
Externalizable
序列化:ObjectOutputStream
代表对象输出流,它的writeObject(Object obj)
方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
反序列化:ObjectInputStream
代表对象输入流,它的readObject()
方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
注意:使用writeObject()
和readObject()
方法的对象必须已经被序列化
关于serialVersionUID
如果serialVersionUID
没有显式生成,系统就会自动生成一个。此时,如果在序列化后我们将该类代码作了改动(包括类名、接口名、方法、属性),系统在反序列化时会重新生成一个新的serialVersionUID
然后去和已经序列化的对象进行比较,就会报序列号版本不一致的错误。为了避免这种问题, 一般系统都会要求实现Serialiable
接口的类显式的声明一个serialVersionUID
。
所以显式定义serialVersionUID
有如下两种用途:
-
希望类的不同版本对序列化兼容时,需要确保类的不同版本具有相同的
serialVersionUID
; -
不希望类的不同版本对序列化兼容时,需要确保类的不同版本具有不同的
serialVersionUID
。
序列化机制算法
- 所有保存到磁盘中的对象都有一个序列化编号
- 当程序试图序列化一个对象时,程序先检查该对象是否已经被序列化过。如果从未被序列化过,系统就会将该对象转换成字节序列并输出;如果已经序列化过,将直接输出一个序列化编号。
示例:要被序列化的对象对应的类的代码
public class Person implements Serializable {
private String name = null;
private Integer age = null;
public Person(){
System.out.println("无参构造");
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
//getter setter方法省略...
@Override
public String toString() {
return "[" + name + ", " + age+"]";
}
下面MySerilizable
是一个简单的序列化程序,它先将一个Person
对象保存到文件person.txt
中,然后再从该文件中读出被存储的Person对象,并打印该对象。
public class MySerilizable {
public static void main(String[] args) throws Exception {
File file = new File("person.txt");
//序列化持久化对象
ObjectOutputStream out =
new ObjectOutputStream(new FileOutputStream(file));
Person person = new Person("Peter", 27);
out.writeObject(person);
out.close();
//反序列化,并得到对象
ObjectInputStream in =
new ObjectInputStream(new FileInputStream(file));
// 没有强制转换到Person类型
Object newPerson = in.readObject();
in.close();
System.out.println(newPerson);
}
}
输出结果:[Peter, 27]
结果没有打印“无参构造”,说明反序列化机制无需通过构造器来初始Java对象。
没有调整构造方法,却能反序列化生成Java对象,这是怎么做到的呢?实际上反序列化时,是通过调用native
方法给类的成员变量贬值的。
结论:
-
反序列化读取的仅仅是Java对象的数据,而不是Java类,所以在反序列化时必须提供该Java对象所属类的class文件(这里是Person.class),否则会引发ClassNotFoundException异常。
-
当重新读取被保存的Person对象时,并没有调用Person的任何构造器,说明反序列化机制无须通过构造器来初始化对象。
如何实现选择序列化?
1. transient关键字
当对某个对象进行序列化时,系统会自动将该对象的所有属性依次进行序列化,如果某个属性引用到别一个对象,则被引用的对象也会被序列化。如果被引用的对象的属性也引用了其他对象,则被引用的对象也会被序列化。这就是递归序列化。
有时候,我们并不希望出现递归序列化,或是某个存敏感信息(如银行密码)的属性不被序列化,我们就可通过transient关键字修饰该属性来阻止被序列化。
将上面的Person
类的age
属性用transient
修饰:
transient private Integer age = null;
再去执行MySerilizable
的结果为:
[Peter, null] // 返序列化时没有值,说明age字段未被序列化
2. writeObject()与readObject()
使用`transient`关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入`writeObject()`方法与`readObject()`方法可以控制如何序列化各属性,甚至完全不序列化某些属性(此时就跟`transient`一样)。
如果我们想要上面的`Person`类里的`name`属性在序列化后存在文件里不让别人知道具体是什么(加密),我们就可在`Person`类里加如下代码:
//自定义序列化
private void writeObject(ObjectOutputStream out)
throws IOException {
// 将当前类的非静态和非瞬态字段写入此流。
//如果不写,如果还有其他字段,则不会被序列化
// out.defaultWriteObject();
out.writeObject(new StringBuffer(name).reverse());
//将name简单加密(即反转),这样别人就知道是怎么回事,
// 当然实际应用不可能这样加密。
out.writeInt(age);
}
//反序列化
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
// 从此流读取当前类的非静态和非瞬态字段。
//如果不写,其他字段就不能被反序列化
//in.defaultReadObject();
//解密:即简单的反转
name = ((StringBuffer)in.readObject()).reverse().toString();
age = in.readInt();
}
详细的自定义序列化与反序列化可参见 ObjectOutputStream
和 ObjectInputStream
类的JDK文档。
3. Externalizable接口
Externalizable
接口与Serializable
接口类似,只是 Externalizable
接口需要强制自定义序列化。
要序列化对象的代码:
public class Teacher implements Externalizable{
private String name;
private Integer age;
//setter、getter方法省略
public Teacher(){
System.out.println("无参构造");
}
public Teacher(String name,Integer age){
System.out.println("有参构造");
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out)
throws IOException {
//将name简单加密
out.writeObject(new StringBuffer(name).reverse());
//out.writeInt(age); //注掉这句后,age属性将不能被序化
}
@Override
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
name = ((StringBuffer) in.readObject()).reverse().toString();
//age = in.readInt();
}
@Override
public String toString() {
return "[" + name + ", " + age+ "]";
}
}
主函数代码改为:
public class MySerilizable {
public static void main(String[] args) throws Exception {
File file = new File("person.txt");
//序列化持久化对象
ObjectOutputStream out =
new ObjectOutputStream(new FileOutputStream(file));
Teacher person = new Teacher("Peter", 27);
out.writeObject(person);
out.close();
//反序列化,并得到对象
ObjectInputStream in =
new ObjectInputStream(new FileInputStream(file));
// 没有强制转换到Person类型
Object newPerson = in.readObject();
in.close();
System.out.println(newPerson);
}
}
打印结果:
有参构造
无参构造 //与Serializable 不同的是,还调用了无参构造
[Peter, null] //age未被序列化,所以未取到值
单例模式的序列化
当我们使用Singleton
模式时,应该是期望某个类的实例应该是唯一的,但如果该类是可序列化的,那么情况可能略有不同。对前面使用的Person
类进行修改,使其实现Singleton
模式,如下所示:
public class Person implements Serializable {
private static class InstanceHolder {
private static final Person instatnce =
new Person("John", 31, "男");
}
public static Person getInstance() {
return InstanceHolder.instatnce;
}
private String name = null;
private Integer age = null;
private String gender = null;
private Person() {
System.out.println("必须私有化的无参构造");
}
private Person(String name, Integer age, String gender) {
System.out.println("有参构造");
this.name = name;
this.age = age;
this.gender = gender;
}
...
}
同时要修改 MySerilizable
应用,使得能够保存/获取上述单例对象,并进行对象相等性比较,如下代码所示:
public class MySerilizable {
public static void main(String[] args) throws Exception {
File file = new File("person.txt");
ObjectOutputStream out =
new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(Person.getInstance()); // 保存单例对象
out.close();
ObjectInputStream in =
new ObjectInputStream(new FileInputStream(file));
Object newPerson = in.readObject();
in.close();
System.out.println(newPerson);
// 将获取的对象与Person类中的单例对象进行相等性比较
System.out.println(Person.getInstance() == newPerson);
}
}
打印结果:
有参构造
[John, 31, 男]
false //说明不是同一个对象
从上面结果能发现,序列化后反序列化之后,不再是同一个对象了。其实是可以这样解释的:Java有4种创建对象的方式(new
、newInstance()
、clone()
以及这里的readObject()
方法),readObject
方法就是相当于新建了一个对象,所以上面引用的会是不同的对象。
反序列化漏洞
黑客会可以在你反序列化的数据里加上恶意代码,当你反序列化时就执行这段恶意代码。
像这种情况,就需要我们将拿到手的数据进行校验,如签名等。
序列化对象注意事项
-
对象的类名、属性都会被序列化;而方法、
static
属性(静态属性)、transient
属性(即瞬态属性)都不会被序列化(这也就是第4条注意事项的原因) -
虽然加
static
也能让某个属性不被序列化,但static
不是这么用的 -
要序列化的对象的引用属性也必须是可序列化的,否则该对象不可序列化,除非以
transient
关键字修饰该属性使其不用序列化。 -
反序列化地象时必须有序列化对象生成的class文件(很多没有被序列化的数据需要从class文件获取)。
在实际开发过程中,可能你发送后,对方并没有你的实体类对象,那怎么办,这时就需要你这边将对象转成字符串的形式,示例:
MqInfo info = new MqInfo(); String body = JSON.toJSONString(mqInfo)
-
当通过文件、网络来读取序列化后的对象时,必须按实际的写入顺序读取。
-
要传输的对象,不要忘记了实现
Serializable
接口
参考:https://blog.csdn.net/zcl_love_wx/article/details/52126876