java 序列化

来源:互联网 发布:安卓版php开发编辑器 编辑:程序博客网 时间:2024/06/11 04:56
序列化用来将对象的状态转换成字节流,以后可以通过这些值再生成相同状态的对象!
对象序列化是对象持久化的一种实现方法,它是将一个对象的属性和方法转化为一种序列化的格式以用于存储和传输,
反序列化就是根据这些保存的信息重建对象的过程。
声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态, transient代表对象的临时数据。
将对象读出或者写入流的主要类有两个:ObjectOutputStream与ObjectInputStream 。
ObjectOutputStream提供用来将对象写入输出流的writeObject方法,ObjectInputStream提供从输入流中读出对象的readObject方法。

如果一个类要完全负责自己的序列化,则实现Externalizable接口而不是Serializable接口。
Externalizable接口定义包括两个方法writeExternal()与readExternal()。
利用这些方法可以控制对象数据成员如何写入字节流。
类实现Externalizable时,头写入对象流中,然后类完全负责序列化和恢复数据成员,除了头以外,根本没有自动序列化。这里要注意了。
声明类实现Externalizable接口会有重大的安全风险。
writeExternal()与readExternal()方法声明为public,恶意类可以用这些方法读取和写入对象数据。
如果对象包含敏感信息,则要格外小心。这包括使用安全套接或加密整个字节流。

原则:
1.谨慎地实现Serializable接口
(1)实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了改变这个类的实现的灵活性。
(2)实现Serializable的第二个代价是,它增加了出现Bug和安全漏洞的可能性。
序列化机制是一种语言之外的对象创建机制。无论你是接受了默认的行为还是覆盖了默认的行为,反序列化机制都是一个"隐藏的构造器",
具备与其他构造器相同的特点。
(3)实现Serializable的第三个代价是,随着类发行新的版本,相关的测试负担也增加了。
为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口。
在为了继承而设计的类中,真正实现了Serializable接口的有Throwable类、Component和HttpServlet抽象类。
因为Throwable类实现了Serializable接口,所以RMI(远程接口调用)的异常可以从服务器端传到客户端
Component实现了Serializable接口,因此GUI可以被发送、保存和恢复。
HttpServlet实现了Serializable接口,因此回话状态session state可以被缓存。

千万不要认为实现Serializable接口是很容易的问题,除非一个类在用了一段时间后就会被抛弃,否则,
实现Serializable接口就是一个很严肃的承诺

2.考虑使用自定义的序列化形式
如果没有先认真考虑默认的序列化形式是否合适,就不要贸然的接受。

对于一个对象来说,理想的序列化形式应该只包含该对象的逻辑数据,而逻辑数据与物理表示法应该是各自独立的。

如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。
如:对于下面这些仅仅表示人名的类,默认的序列化形式就是合理的
public class PersonName implments Serializable{
private final String lastName;
private final String firstName;
private final String middleName;
...
}
从逻辑的角度而言,一个名字包含姓、名和中间名,PersonName中的实例域精确地反映了它的逻辑内容。

即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束的关系和安全性,对PersonName这个类而言,
readObject方法必须确保lastName和firstName非null的。

下面的类与PersonName不同,它是一个极端,该类表示了一个字符串列表(暂时忽略最好使用标准类库中List实现)。
public final class StringList implements Serializable{
private int size=0;
private Entry head=null;

private static class Entry implements Serializable{
String date;
String next;
String previous;
}
...
}

当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点:
(1)它使这个类导出API永远地束缚在该类的内部表示法上;
上面例子中,私有的StringList.Entry类变成了公有API的一部分。如果将来的版本中,内部表示法发生了变化,
StringList类仍将需要接受链表形式的输入,并产生链表形式的输出。这个类永远也摆脱不掉维护链表项所需要的所有代码,
即使它不再使用链表作为内部数据结构了,也仍然需要这些代码。
(2)它会消耗过多的空间;
在上面的例子中,序列化形式既表示了链表中的每个项,也表示了所有的链表关系,这是必不要的。这些链表项以及链表只不过是实现细节,
不值得记录在序列化中。因为这样的序列化形式过于庞大,所以把它写到磁盘中,或者在网络上发送都非常慢。
(3)它消耗过多的时间
(4)它会引起栈溢出

对于StringList类,合理的序列化形式可以非常简单,只需要包含链表中字符串的数目,然后紧跟着这些字符串即可。
这样就构成了StringList所表示的逻辑数据,与它的物理表示细节脱离。
下面是StringList的一个修订版,它包含writeObject和readObject方法,用来实现这样的序列化形式。
顺便提醒下,transient(瞬时的)修饰符表明这个实例域将从一个默认序列化中省略掉。
public final class StringList implements Serializable{
private transient int size=0;
private transient Entry head=null;

private static class Entry{
String data;
String next;
String previous;
}

public final void add(String s){
...
}

private void writeObject(ObjectOutputStream oos) throws IOException{
oos.defaultWriteObject();
oos.writeInt(size);

for(Entry e=head;e!=null;e=e.next){
oos.writeObject(e.data);
}
}

private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{{
ois.defaultReadObject();
int numElements=ois.readInt();

for(int i=0;i<numElements;i++){
add((String)ois.readObject())
}
}
}

不管你选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列化版本UISD(serial version UID)。
这样可以避免序列化版本UID成为潜在的不兼容根源。而且,这样做也会带来一个小小的性能好处。
如果没有提供显式的序列化版本UID,就需要在运行时通过一个高开销的计算过程来产生一个序列号版本UID。
private static final long serialVersionUID=randomLongValue;

3.保护性地编写readObject方法
如下一个不可变的日期范围类,它包含可变的私有Date域。该类通过在其构造器和访问方法中保护性地拷贝Date对象,
极力地维护其约束条件和不可变性。

public class Period {
private Date start;
private Date end;

public Period(Date start,Date end){
this.start=new Date(start.getTime());
this.end=new Date(end.getTime());

if(this.start.compareTo(this.end)>0){
throw new IllegalArgumentException(start+"after"+end);
}
}

public Date start(){
return new Date(start.getTime());
}

public Date end(){
return new Date(end.getTime());
}

public String toString(){
return start+" - "+end;
}
}
假设你决定要把这个类做成可序列化的,因为Period对象的物理表示法正好反应了它的逻辑数据内容,
所以,使用默认的序列化形式并没有什么不合理的。因此,为了使这个类成为可序列化的,似乎你所需要做的也就是在类的声明中增加
"implements Serializable"字样。然而,如果你真的这样做了,那么这个类将不再保证它的关键约束了。

仅在Period类的声明中加"implements Serializable"字样。
那么,这个不完整的程序将产生一个Period实例,它的结束时间比开始时间还要早。
public class BogusPeriod {
private static final byte[] serializedForm=new byte[]{
-84,-19,0,5,115,114,0,23,99,111,109,46,121,99,46,101,102,102,101,99,116,105,118,101,46,80,101,114,105,111,100,0,0,0,0,0,0,0,1,2,0,2,76,0,3,101,110,100,116,0,16,76,106,97,118,97,47,117,116,105,108,47,68,97,116,101,59,76,0,5,115,116,97,114,116,113,0,126,0,1,120,112,115,114,0,14,106,97,118,97,46,117,116,105,108,46,68,97,116,101,104,106,-127,1,75,89,116,25,3,0,0,120,112,119,8,0,0,1,80,-20,-7,-72,0,120,115,113,0,126,0,3,119,8,0,0,1,81,-121,120,-128,0,120,
};

public static void main(String[] args) {
//createByte()
Period p=(Period) deserialize(serializedForm);
System.out.println(p);
}

private static Object deserialize(byte[] sf){
try {
InputStream is=new ByteArrayInputStream(sf);
ObjectInputStream ois=new ObjectInputStream(is);
return ois.readObject();
}catch (Exception e) {
throw new IllegalArgumentException(e);
}
}

@SuppressWarnings("unused")
private static void createByte(){
try {
Period p=new Period(new Date(115,11,10),new Date(115,10,10));
ByteArrayOutputStream baos = new ByteArrayOutputStream(); //构造一个字节输出流
ObjectOutputStream oos = new ObjectOutputStream(baos); //构造一个类输出流
oos.writeObject(p); //写这个对象

byte[] buf = baos.toByteArray(); //从这个地层字节流中把传输的数组给一个新的数组
oos.flush();
oos.close();
for(int i=0;i<buf.length;i++){
System.out.print(buf[i]+",");
}
//构建一个类输入流,地层用字节输入流实现
// ByteArrayInputStream bais = new ByteArrayInputStream(buf);
// ObjectInputStream ois = new ObjectInputStream(bais);
// System.out.println(ois.readObject());
// } catch (ClassNotFoundException e) {
// e.printStackTrace();
// } 
}catch (IOException e) {
e.printStackTrace();
}
}
}

修改:
public class Period implements Serializable{
private static final long serialVersionUID = 1L;
private Date start;
private Date end;

public Period(Date start,Date end){
this.start=new Date(start.getTime());
this.end=new Date(end.getTime());

if(this.start.compareTo(this.end)>0){
throw new IllegalArgumentException(start+"after"+end);
}
}

private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException{
s.defaultReadObject();
if(start.compareTo(end)>0){
throw new IllegalArgumentException(start+"after"+end);
}
}

public Date start(){
return new Date(start.getTime());
}

public Date end(){
return new Date(end.getTime());
}

public String toString(){
return start+" - "+end;
}
}
尽管这样的修正避免了攻击者创建无效的Peroid实例,但是,这里仍然隐藏着一个更为微妙的问题。
通过伪造字节流,要想创建可变的Period实例仍是有可能的,做法是:字节流以一个有效的Period实例开头,然后附加上两个额外的引用,
指向Period实例中的两个私有的Date域。攻击者从ObjectInputStream中读取Peroid实例,然后读取附加在其后的"恶意编写的对象引用"。
这些对象引用使得攻击者能够访问到Peroid对象的内部的私有Date域所引用的对象,通过改变这些Date实例,攻击者可以改变Peroid实例。
public class MutablePeriod {
public final Period period;

public final Date start;
public final Date end;

public MutablePeriod(){
try {
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream out=new ObjectOutputStream(bos);
out.writeObject(new Period(new Date(),new Date()));

byte[] ref={0x71,0,0x7e,0,5};
bos.write(ref);
ref[4]=4;
bos.write(ref);

ObjectInputStream ins=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
period=(Period) ins.readObject();
start=(Date) ins.readObject();
end=(Date) ins.readObject();

} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}

@SuppressWarnings("deprecation")
public static void main(String[] args) {
MutablePeriod mp=new MutablePeriod();
Period pd=mp.period;
Date end=mp.end;
//Date start=mp.start;
end.setYear(114);
System.out.println(pd);
}
}

问题的根源在于,Period的readObject方法并没有完成足够的保护性拷贝。当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,
如果哪个域包含了这样的对象引用,就必须要做保护性的拷贝,这是非常重要的。
因此,对于每个可序列化的不可变类,如果它包含了私有可变组件。那么在它的readObject方法中,必须要对这些组件进行保护性。
下面的readObject方法可以确保Period的约束条件不会遭到破坏,以保持它的不可变性。

private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException{
s.defaultReadObject();
start=new Date(start.getTime());
end=new Date(start.getTime());

if(start.compareTo(end)>0){
throw new IllegalArgumentException(start+"after"+end);
}
}

4.对于实例控制,枚举类型优于readResolve
如果一个Singleton模式的类,如果这个的声明加上了"implements Serializable"的字样,它就不再是一个Singleton。
无论该类使用的是默认的序列化还是自定义的,也跟它是否提供了显式的readObject()方法无关。
任何一个readObject方法,不管是显式还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。

注意:如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient的。

以下是一个有问题的Singleton类:
public class Elvis implements Serializable{
private static final long serialVersionUID = 1L;

public static final Elvis INSTANCE=new Elvis();

private String[] favoriteSongs={"Hund Dog","Heartbreak Hotel"};

public void printFavorites(){
System.out.println(Arrays.toString(favoriteSongs));
}

private Object readResolve(){
System.out.println("调用了readResolve方法...");
return INSTANCE;
}


下面是一个"盗用者"类,是根据上述的描述构造的:
public class ElvisStealer implements Serializable{
private static final long serialVersionUID = 1L;

static Elvis impersonater; //掩饰

private Elvis payload; //(导弹、火箭等的)有效载荷,有效负荷;收费载重;(工厂、企业等)工资负担

private Object readResolve(){
impersonater=payload;
return new String[]{"A Fool Such as I"};
}
}

测试:
public class ElvisImpersonator {
private static final byte[] serializedForm=new byte[]{
(byte)0xac,(byte)0xed,0x00,0x05,0x73,0x72,0x00,0x05,
0x45,0x6c,0x76,0x69,0x73,(byte)0x84,(byte)0xe6,
(byte)0x93,0x33,(byte)0xc3,(byte)0xf4,(byte)0x8b,
0x32,0x02,0x00,0x01,0x4c,0x00,0x0d,0x66,0x61,0x76,
0x6f,0x72,0x69,0x74,0x65,0x53,0x6f,0x6e,0x67,0x73,
0x74,0x00,0x12,0x4c,0x6a,0x61,0x76,0x61,0x2f,0x6c,
0x61,0x6e,0x67,0x2f,0x4f,0x62,0x6a,0x65,0x63,0x74,
0x3b,0x78,0x70,0x73,0x72,0x00,0x0c,0x45,0x6c,0x76,
0x69,0x73,0x53,0x74,0x65,0x61,0x6c,0x65,0x72,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x01,
0x4c,0x00,0x07,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64,
0x74,0x00,0x07,0x4c,0x45,0x6c,0x76,0x69,0x73,0x3b,
0x78,0x70,0x71,0x00,0x7e,0x00,0x02
};

public static void main(String[] args) {
Elvis elvis=(Elvis) deserialize(serializedForm);
Elvis implersonater=ElvisStealer.impersonater;
elvis.printFavorites();
implersonater.printFavorites();
}                                                                                                                                  

private static Object deserialize(byte[] sf){
try {
InputStream is=new ByteArrayInputStream(sf);
ObjectInputStream ois=new ObjectInputStream(is);
return ois.readObject();
}catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
}

通过将favoriteSongs域声明为transient,可以修正这个问题。

如果反过来,将一个可序列化的实例受控的类编写成枚举,就可以绝对保证除了所声明的常量之外,不会有别的实例。JVM对此提供了保障,这一点你可以确信无疑。
public enum EnumElvis{
INSTANCE;
private String[] favoriteSongs={"Hund Dog","Heartbreak Hotel"};

public void printFavorites(){
System.out.println(Arrays.toString(favoriteSongs));
}
}
0 0
原创粉丝点击