序列化最基本使用
public class A implements Serializable{
private static final long serialVersionUID = 9175036933185692367L;
private final String name;
private String Id;
public A(String name,String Id){
this.name = name;
this.Id = Id;
} public void methodA(){
System.out.println("My name is zazalu" + Id);
}
}
class B{
public static void main(String[] args) throws IOException {
//将A序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("test.txt")));
A a = new A("zazaluA","123456");
oos.writeObject(a);
}
}
class C{
//将A反序列化
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("test.txt")));
A a = (A) ois.readObject();
a.methodA(); //输出My name is zazalu123456
}
}
以上就是最基本的序列化使用方式,不多说。接下来来思考Serializable到底在底层发生了什么
其实,上面的代码其实隐式调用了2个方法
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
接下来我们来演示下,将这两个方法在A类中重写
private void readObject(ObjectInputStream in){
System.out.println("调用了readObject");
}
private void writeObject(java.io.ObjectOutputStream out){
System.out.println("调用了writeObject");
}
然后我们在此运行下,控制台输出就变成了这样:
可见,这两个方法都被调用了。
那么接下来我们就要学习这两个方法到底可以做什么?
首先我们先了解下下面两个方法
in.defaultReadObject();
out.defaultWriteObject(); //in和out是对象输入输出流的两个实例
这两个方法其实是对象流读写对象的默认的底层调用方法。
当我们使用out.writeObject()和in.readObject()的时候,就会默认去调用
in.defaultReadObject();
out.defaultWriteObject();
这两个方法是会把允许序列化的属性和方法进行序列化和反序列化(被transient关键字修饰的属性方法是不会被默认序列化的)
既然
in.defaultReadObject();
out.defaultWriteObject();
这两个是默认的方法,那么看来我们可以自定义序列化过程
如何自定义序列化,其实方法有很多
主要是使用
private void readObject(ObjectInputStream in){
System.out.println("只反序列化Id属性");
out.writeObject(Id);
}
private void writeObject(java.io.ObjectOutputStream out){
System.out.println("只序列化Id属性");
this.Id = in.readObject();
}
在序列化的类中重写这两个方法进行自定义序列化!上述写法就只序列化了一个Id属性,而name属性没有序列化!
但是我们这样自定义序列化是很粗糙的,因为我们没有处理很多细节方法的问题,比如NotActiveException我们就没有做处理。所以我们最好是
in.defaultReadObject();
out.defaultWriteObject();
让这两个方法被调用。所以我们可以如此实现
1.先将所有属性方法哟哦那个transient关键字修饰
2.将你想要序列化的字段用out.writeObject()和in.readObject()来实现
3.在最后调用in.defaultReadObject(); 和in.defaultReadObject(); 来保证细节处理正常
private void readObject(ObjectInputStream in){
System.out.println("只反序列化Id属性");
out.writeObject(Id);
in.defaultReadObject();
}
private void writeObject(java.io.ObjectOutputStream out){
System.out.println("只序列化Id属性");
this.Id = in.readObject();
in.defaultReadObject();
}
序列化安全问题
1.首先,序列化出来的字节流,可以进行分析,从而给攻击者暴露了所有的信息,解决方法:在序列化时对字段进行加密,反序列化的时候再进行解密
2.其次,攻击者可以伪造字节流,然后通过反序列化获取珍贵的信息。
比如(Period是一个类,有start和end这两个Date属性)
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(/*new FileOutputStream(new File("D:/obj.data"))*/bos);
oos.writeObject(new Period(new Date(), new Date()));
/*
伪造一个字节流,这个字节流以一个有效的Period实例所产生的字节流作为开始,然后附加上两个额外的引用,指向Period实例中的两个内部私有Date域,攻击者通过引用攻击内部域
*/
byte[] ref = {0x71, 0 , 0x7e, 0, 5};
bos.write(ref);
ref[4] = 4;
bos.write(ref);
ObjectInputStream ois = new ObjectInputStream(/*new FileInputStream(new File("D:/obj.data"))*/new ByteArrayInputStream(bos.toByteArray()));
period = (Period)ois.readObject();
start = (Date)ois.readObject(); //获取到了Period里的start属性引用
end = (Date)ois.readObject(); //获取到了Period里的end属性引用
......
//获取到的end属性,我们可以修改,因为是对象类型,所以period里的end属性也发生了变化!这实在是太恶毒了
end.setYear(78);
解决办法:使用保护性拷贝!
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException{
in.defaultReadObject();
start = new Date(start.getTime());
end = new Date(end.getTime()); //重新创建start和end对象引用
}
可是这办法因为二次修改了属性的引用,所以无法和final字段配合使用,更多时候我们会希望属性是final的,这样就不会被轻易修改了。
加强解决办法:使用序列化代理
序列化代理
http://blog.csdn.net/drifterj/article/details/7802586
参看这个bolg,写的非常具体了!