深入分析java序列化

时间:2022-01-21 17:34:54

概念

先来点简单的概念:
what?why?
什么是序列化?为什么要序列化?
答曰:将java对象转成字节序列,用以传输和保存
where?
使用场景是什么?
答曰:对象的传输;状态的备份,例如jvm的dump文件;
好了,不装*了,下面说的详细点。其实对象的序列化主要有两种用途:

  • 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中
  • 在网络上传送对象的字节序列

在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

实现方式

how?

  1. (需要序列化的类)实现Serializable接口。查看源码可知Serializable是个空接口(里面没有任何方法),即标记接口。作用就是明确地告诉java,这个类需要序列化,不实现这个接口,那这个对象是没法序列化和传输的(如果不加implements Serializable,会报错,建议试试,直观感受下),类似的标记接口还有cloneable。
  2. 创建输入输出流。首先,序列化一个对象,需要要创建某些OutputStream(如FileOutputStream、ByteArrayOutputStream等),然后将这些OutputStream封装在一个ObjectOutputStream中。这时候,只需要调用writeObject()方法就可以将对象序列化,并将其发送给OutputStream(对象的序列化是基于字节的,不能使用Reader和Writer等基于字符的层次结构)。其次,反序列的过程(即将一个序列还原成为一个对象),则需要将一个InputStream(如FileInputstream、ByteArrayInputStream等)封装在ObjectInputStream内,然后调用readObject()即可。简单来说即:
    序列化:ObjectOutputStream.writeObject(Object)
    反序列化:ObjectInputStream.readObject()

见下面的例子:
先定义一个待序列化的对象:

package com.alibaba.serialize.common;

import java.io.Serializable;

/*
* 不加implements Serializable,会报错
*/

public class User implements Serializable{
private static final long serialVersionUID = 1L;
private String username;
private int age;

public User(String username, int age) {
this.username = username;
this.age = age;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

再写一个序列化的例子

package com.alibaba.serialize.common;

import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeExample {

public static final String out_file = "src/com/alibaba/serialize/temp.out";

public static void main(String[] args) {
User user = new User("tony", 18);
try {
//如果这里改成网络输出流而不是文件输出流(反序列化那里也同样改成网络输入流),则可以在网络上传输
ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream(out_file)));

out.writeObject("用户信息..");
out.writeObject(user);
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("序列化完成。请调用反序列化类DeserializeExample完成反序列化!");
}
}

最后再写一个反序列化的例子

package com.alibaba.serialize.common;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class DeserializeExample {
public static void main(String[] args) throws Exception{
ObjectInputStream in = new ObjectInputStream(
new BufferedInputStream(new FileInputStream(SerializeExample.out_file)));
String title = (String) in.readObject();
System.out.println(title);
User user = (User) in.readObject();
System.out.println("用户姓名:"+user.getUsername());
System.out.println("用户年龄:"+user.getAge());
}
}

分别运行SerializeExampleDeserializeExample 可以看到对应的效果。

序列化的文件关系

说几点平常不注意容易忽略的地方。

  1. 如果一个类没有实现Serializable接口,但是它的基类实现了,那么这个类也是可以序列化的;
  2. 相反,如果一个类实现了Serializable接口,但是它的父类没有实现,那么这个类还是可以序列化(Object是所有类的父类),但是序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,会发现该变量数值与序列化时的数值不同(一般为null或者其他默认值),而且这个父类里面必须有无参的构造方法,不然子类反序列化的时候会报错。

见示例:
先写个父类

package com.alibaba.serialize.parent;

import java.io.Serializable;

public class CarSerialize{
private String name;
private Long price;

/*
* 没有这个无参构造函数会报错,可以删除,测试下
*/

public CarSerialize() {
super();
}

public CarSerialize(String name, Long price) {
this.name = name;
this.price = price;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Long getPrice() {
return price;
}

public void setPrice(Long price) {
this.price = price;
}

}

再写个子类

package com.alibaba.serialize.parent;

import java.io.Serializable;

public class BMWSerialize extends CarSerialize implements Serializable{
private String name;
private Long price;

public BMWSerialize(String name, Long price) {
super(name, price);
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Long getPrice() {
return price;
}

public void setPrice(Long price) {
this.price = price;
}
}

最后写个测试类

package com.alibaba.serialize.parent;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializeTest {

public static void main(String[] args) {
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
out.writeObject(new BMWSerialize("BMW",100L));
out.close();

ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
BMWSerialize bmw = (BMWSerialize) oin.readObject();
System.out.println("解密后的字符串:" + bmw.getName());
oin.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

上述代码中,大家可以尝试按照我上面说的两点,分情况进行测试。

这里还有个需要注意的地方
同一个流的对象引用关系被很好地保留了下来,不同流的对象引用关系则无法保证匹配
这一点我感觉比较容易理解,就不写了,有时间可以测试下。

序列化ID

思考一个问题:如果序列化之后,两端或者版本不同,class不一致怎么办?

???
???

这里就有序列化ID的概念了,serialVersionUID适用于JAVA的序列化机制。反序列化时,如果类发生了变动,则构造器和赋值语句不会生效。

简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException(如同上面一节,你把父类的无参构造方法删除之后,再反序列化也会报这个错)。

serialVersionUID有两种显示的生成方式:

  • 一是默认的1L,比如:private static final long serialVersionUID = 1L
  • 二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:private static final long serialVersionUID = xxxxL;

再思考一个问题,如果序列化ID是一样的,假设A端是序列化,B端是反序列化,反序列化之前B端序列化对象发生变化,会有几种情况?其实逻辑还是比较容易理解的,略微总结了下,大致如下:

  • B加字段:序列化,反序列化正常,B端新增加的int字段被赋予了字段类型的默认值(如0或者false)
  • B删字段:序列化,反序列化正常(不会报错),B端字段少于A端,A端多的字段值丢失

自定义序列化

既然题目是深入分析,那当然要接着分析,哈哈~所以,再问一个问题,当对象中有敏感字段怎么办?如password。是不是自己可以决定序列化方式呢?这里解释一下

在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作,见示例(代码摘自参考资料)。
先写一个自定义序列化过程的待序列化对象。

package com.alibaba.serialize.customer;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectInputStream.GetField;
import java.io.ObjectOutputStream;
import java.io.ObjectOutputStream.PutField;
import java.io.Serializable;

public class SecurityInfo implements Serializable{
private String password = "pass";

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

private void writeObject(ObjectOutputStream out) {
try {
PutField putFields = out.putFields();
System.out.println("原密码:" + password);
password = "encryption";//模拟加密
putFields.put("password", password);
System.out.println("加密后的密码" + password);
out.writeFields();
} catch (IOException e) {
e.printStackTrace();
}
}

private void readObject(ObjectInputStream in) {
try {
GetField readFields = in.readFields();
Object object = readFields.get("password", "");
System.out.println("要解密的字符串:" + object.toString());
password = "pass";//模拟解密,需要获得本地的密钥
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

}
}

再写一个测试类

package com.alibaba.serialize.customer;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class CustomerSerialize {

public static void main(String[] args) {
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
out.writeObject(new SecurityInfo());
out.close();

ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
SecurityInfo t = (SecurityInfo) oin.readObject();
System.out.println("解密后的字符串:" + t.getPassword());
oin.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

ok!运行下看看吧。
其实说到自定义序列化,还可以通过实现java序列化另一个接口Externalizable,实现Externalizable接口就需要实现它的两个方法,如下:
深入分析java序列化

大致总结了下,Externalizable和Serializable的区别:

实现Serializable接口 实现Externalizable
系统自动存储必要信息 程序员决定存储哪些信息
Java内建支持,易于实现,只需实现该接口如何即可,无须任何代码支持 仅仅提供两个空方法,实现该接口必须为两个空方法提供实现
性能略差 性能略高

其实虽然Externalizable性能高,但是我们一般很少在代码里看到,这个原因需要请假下别人,个人认为可能是自己写不够通用,太麻烦,而且一般敏感信息也不会这么传。

几种序列化协议比较

这里主要介绍和对比几种当下比较流行的序列化协议,包括XML、JSON、Protobuf、Thrift和Avro。这里因为我用的也不多,只用过json和xml,所以,对比的话这里列了个参考资料: http://tech.meituan.com/serialization_vs_deserialization.html

本文还参考:
http://blog.csdn.net/zhaozheng7758/article/details/7820018

感谢上述作者~