Hexo

点滴积累 豁达处之

0%

序列化

序列化,简单说就是为了保存在内存中的各种对象的状态(也就是实例变量,不是方法),并且可以把保存的对象状态再读出来。虽然你可以用你自己的各种各样的方法来保存object states,但是Java给你提供一种应该比你自己好的保存对象状态的机制。 

1 概述

  序列化是指把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对 象的状态以及相关的描述信息。客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

本质 上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。序列化机制的核心作用就是对象状态的 保存与重建。 

2 序列化机制的用途

 通过对象的序列化我们可以得到对象状态信息的字节流数据,这些数据代表了当前对象的状态。当对象转化成二进制数据流之后,我们可以通过多种方式处理它,比如可以通过Socket将 数据发送的远程主机,又或者保存的本地文件中以后期用。同时,当我们通过某种方式获取了对象序列化之后的二进制数据之后,通过反序列化机制实现对象的重建,恢复之前对象的状态。由此可知,序列化后的字节流可以应用到任何想要重建对象的地方,您所需要的就是获取这些字节流数据。 

3 实现序列化的方式

Java API提供了对序列化的支持,要实现对象的序列化和反序列化,基本上包括两个步骤:

1.声明对象具有可序列化的能力

2.通过Java API实现具体的序列化处理

在Java语言中,声明对象具有可序列化的能力主要有两种方式:其一,实现Serializable接口;其二,实现Externalizable接口。两者既有区别又有联系。 

Serializable接口在JDK源码中定义如下:

1
2
3
4
5
6
7
8
9
/** @see java.io.ObjectOutputStream
* @see java.io.ObjectInputStream
* @see java.io.ObjectOutput
* @see java.io.ObjectInput
* @see java.io.Externalizable
* @since JDK1.1
*/
public interface Serializable {
}
Java从JDK1.1开始支持对象的序列化机制,有上图的源码可知,Serializable接口没有声明任何方法,实现该接口的Java类不需要对任何方法提供实现(默认情况下,定制序列化时除外),因此,该接口仅仅是一个”mark interface”,实现该接口意味着告知JVM该对象可以序列化。Java序列化机制要求所有具备序列化的对象必须实现该接口,否则是不能被序列化的,如果对于没有实现该接口的对象进行序列化时,Java API会抛出异常,无法进行序列化。 

Serializable接 口提供了默认的序列化行为,在默认情况下,开发人员只需实现该接口,无需进行其他额外的操作,即可实现的对象的序列化。当然,所谓默认的处理,必然隐藏着 对序列化对象的默认操作,比如对象的哪些属性被序列化。默认情况下,只对对象中非静态的字段以及非瞬时的字段进行序列化,其他的字段是不允许被序列化的。 这种情况的具体表现就是,在序列化的有序字节流中没有保存不能被序列化的字段的状态,因此,在反序列化时,这些字段状态是不能被重建的。但是有一点需要注 意的是,经过反序列化后的对象,除了对可被序列化的字段状态进行重建之外,其他的没有被序列化的字段作为对象属性的一部分,也在对象重建时得以初始化。但 是这些字段的状态是不被保存的,重建后的这些属性仅仅是系统赋予的默认值,而非保存了对象序列化之前的状态。 

实现Serializable接口除了提供默认的序列化方式之外,同样允许开发人员定制序列化,即通过实现以下相同签名的方法来实现:

序列化方法:

1
private void writeObject(java.io.ObjectOutputStream out) throws IOException

反序列化方法:

1
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;

其中,writeObject方法用于定制序列化,readObject方法用于实现定制反序列化。

Externalizable接口集成自Serializable接口,实现该接口意味着对象本身完全掌控自己的序列化方式。该接口JDK源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 /** @author  unascribed
* @see java.io.ObjectOutputStream
* @see java.io.ObjectInputStream
* @see java.io.ObjectOutput
* @see java.io.ObjectInput
* @see java.io.Serializable
* @since JDK1.1
*/
public interface Externalizable extends java.io.Serializable {

void writeExternal(ObjectOutput out) throws IOException;

void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

Externalizable接口定义了两个方法:writeExternal(ObjectOutput out)和readExternal(ObjectInput in)。write方法用于实现定制序列化,read方法用于实现定制反序列化。

基于Externalizable接口的定制和基于Serializable接口的定制有所不同。基于Externalizable接口的定制是通过实现上述两个方法实现的,而且方法中操作的参数也不一样,基于Externalizable接口是通过操作ObejectOutput类和ObjectInput类实现的。而基于Serializable接口是通过操作ObjectOutputStream类和ObjectInputStream类实现的。至于两种方式的序列化定制方式会在稍后的示例中进行展示。 

4 哪些数据被序列化了

我们采用默认的序列化方式(仅仅直接实现Serializable接口)序列化对象时,默认的,只将非静态的和no-transient字段进行序列化,除此之外的其他域在默认情况下是不进行序列化的,也就是说,在序列化的字节流数据中没有对这两种类型数据的记录。这是为什么呢?如果是从代码级别上分析,从JDK源码可知, JDK源码进行了默认的处理,然后将静态属性和瞬时属性排除在序列化之外,但为什么会选择这样的实现呢?我们都知道,对象序列化的本质是将对象的状态通过有序字节流进行保存或传输,由此,问题的焦点应该是对象的状态。静态变量时类变量,属于整个类,并非专属于每个对象实例,因此,不序列化静态变量时合理的。瞬时变量,指的是被transient关键字修饰的变量,该关键字表示为瞬时的,即不做持久化处理的,以此来控制属性是否被包含进入序列化的字节流中。因此,在序列化时,排除transient关键字修饰的属性也是合理的。 

需要注意的是,没有被序列化的属性不会出现在序列化后的有序字节流中,但是,我们在反 序列化时,是可以访问这些变量的。这是因为,序列化的过程保存了你所期望保存的对象的状态(属性当前值),反序列化就是重建对象的过程,在这个过程中,字 节流中所保存的对象状态被重新赋予了新建的对象。此时,对于没有被序列化的属性也是存在的,因为其实类定义的一部分,在新建的对象中是必然存在的。唯一不 同的是,他们的值是类定义的默认值,而非是来自字节流中保存的状态。这也恰恰反映了序列化的本质:保存对象的状态。 

那么,到底哪些数据被序列化到了有序字节流中呢?字节流数据包含了non-static和non-transient类型的成员数据、类名、类签名、可序列化的基类以及类中引用的其他对象。 
  1. 如果基类实现了Serializable接口,则其子类默认的是可序列化的,不必显示声明;
  2. 如果基类没有实现Serializable接口,在反序列化时,会调用基类的无参构造方法,重建基类对象只不过是不会保留基类对象状态。

5 Serializable序列化

基于Serializable接口的默认序列化步骤为:首先,进行序列化声明,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DefaultSerialObject implements Serializable{
private static final long serialVersionUID = -8743340629265050501L;
private transient int noSerialVar ; //该成员不被序列化,但在反序列化构造对象时会重建该属性
private int value;

public int getNoSerialVar() {
return noSerialVar;
}

public void setNoSerialVar(int noSerialVar) {
this.noSerialVar = noSerialVar;
}

public int getValue() {
return value;
}

public void setValue(int value) {
this.value = value;
}
}
实现了Serializable接口的Java类与我们平时定义Java类没有太大区别,唯一需要注意的是serialVersionUID属性。每个实现Serializable接口的对象都有一个serialVersionUID,长整型,64位,唯一标示了该序列化对象。在类定义中,可以显示的定义该静态变量,也可以不定义。在不定义的情况下,Java编译器会隐式的生成该变量。强烈建议显示定义。那么,该变量有什么用途呢?反序列化兼容控制。serialVersionUID相同才能进行反序列化。例如:远程主机需要反序列化对象C,如果在本地和远程主机内的C对象持有的serialVersionUID不同,即使两个类其它部分完全一样,也是不能成功反序列化话的,会抛出异常。因此,如果对类做了修改,为了保证版本对序列化兼容,该变量的值保持不变。从另一个角度来讲,不期望不同版本的类对序列化兼容,则改变该变量值。 

然后,通过Java API进行实际的序列化处理。我们选择的场景是:将对象进行序列化,然后保存到本地文件中。然后,从本地文件读取序列化后的有序字节流,进行反序列化,重建对象。代码示例如下: 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class DefaultSerialTest {
public static void main(String[] args) {
try{
//首先创建一个可以序列化的对象
DefaultSerialObject serialObj = new DefaultSerialObject();
serialObj.setValue(10);
serialObj.setNoSerialVar(11);

System.out.println("the value of serialObj " + serialObj.getValue());
//构造序列化用的输出流
FileOutputStream fileOutputStream = new FileOutputStream("default_serialization");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

//写入序列化对象
objectOutputStream.writeObject(serialObj);
//关闭输出流
objectOutputStream.close();

//反序列化
//构造序列化用的输出流
FileInputStream fileInputStream = new FileInputStream("default_serialization");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
DefaultSerialObject inverseSerialObj = (DefaultSerialObject)objectInputStream.readObject();
System.out.println("the value of inverse serialObj " + inverseSerialObj.getValue());
System.out.println("the noSerialVar of inverse serialObj " + inverseSerialObj.getNoSerialVar());

}catch (Exception e){
e.getStackTrace();
System.out.println("system error");
}
}
}

说明:在实际的对象实例化过程中,涉及到的Java类是ObjectOutputStream和ObjectInputStream。这两个类负责对象序列化的主要工作。

6 定制序列化

上面的代码示例中,展示了最为基本的默认的对象序列化和反序列化方式。之所以称之为是基本的,是因为,我们在对自定义的类进行序列化时完全没有进行任何“干涉”, 系统默认的选择了类定义中符合规则的属性进行序列化,因此这是一种默认的方式。与之相对应的是,我们可以定制序列化及反序列化,以满足实际的需要。例如: 序列化的对象一般在网络上进行传输,所以安全性是必须要考虑的问题。大部分情况下,我们期望对类似于密码等这样的敏感信息进行加密处理,以密文的形式在网 络间传输,增强数据的安全性。但是,通过我们上述的方式进行序列化,默认的处理方式是不能保证密码的密文传输的。因此,针对此类问题,我们必须能够定制对 象的序列化和反序列化过程,只有这样才能将我们的业务逻辑加入其中,以满足实际应用的需要。

如何实现定制呢?

定制序列化和反序列化与上述序列化方式的不同在于:自定义类的实现。

​ 首先,同样,自定义的类要实现Serializable接口,这是序列化处理的前提。不同的是,在定制序列化时,需要根据我们的实际需要,重写writeObject和readObject方法,完成序列化和反序列化的定制。

示例代码如下:

SerializableWriteObject

SerializableReadObject

说明:定制序列化过程中,序列化和反序列化读取信息的顺序要保持一致,否则会出现意想不到的后果。

7 Externalizable(序列化)

实现Extenalizable接口的类将完全由自己控制自身的序列化和反序列化。示例代码如下:

Serializable-Ex1

对象序列化及反序列化测试代码:

Serializable-Ex2

8 序列化带来的问题

8.1 网络传输的安全性

对象进行序列化之后转化成有序的字节流在网络上进行传输,如果通过默认的序列化方式, 则代码都是以明文的方式进行传输。这种情况下,部分字段的安全性是不能保障的,特别是像密码这样的安全敏感的信息。因此,如果您需要对部分字段信息进行特 殊的处理,那么应当选择定制对象的序列化方式,例如对密码等敏感信息进行加密处理。 

8.2 类自身封装的安全性

对对象进行序列化时,类中所定义的被private、final等 访问控制符所修饰的字段是直接忽略这些访问控制符而直接进行序列化的,因此,原本在本地定义的想要一次控制字段的访问权限的工作都是不起作用的。对于序列 化后的有序字节流来说一切都是可见的,而且是可重建的。这在一定程度上削弱了字段的安全性。因此,如果您需要特别处理这些信息,您可以选择相应的方式对这 些属性进行加密或者其他可行的处理,以尽量保持数据的安全性。 

9 小结

1.通过序列化和反序列化实现了对象状态的保存、传输以及对象的重建。在进行对象序列化时,开发人员可以根据自身情况,灵活选择默认方式或者自定义方式实现对象的序列化和反序列化。

2.序列化机制是Java中对轻量级持久化的支持。

3.序列化的字节流数据在网上传输的安全问题需要引起大家足够的注意。

4.序列化破坏了原有类的数据的”安全性“,例如private属性石不起作用的。

参考链接

参考链接