网站首页 > java教程 正文
序列化&反序列化
序列化:即将对象转换为字节序列的过程;Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。
反序列化:即将字节序列转换回对象的过程。转换后的对象包含了转换前的对象的状态。
如何实现对象序列化
我们知道,在java中要实现一个对象的序列化,只需要实现Serializable接口即可。一般还有一个IDE自动生成的serialVersionUID字段。
serialVersionUID
这个字段实际上也是很重要的,如果去掉,在序列化或反序列化的过程中会根据一系列很复杂的算法生成,导致速度变慢。
transient
对于我们不想序列化的字段,可以使用transient修饰它。比如:private transient Date start;则说明start字段不参与序列化。
静态字段
静态字段也不会参与序列化。
实现序列化
通常将对象通过ObjectOutputStream写入一个文件或者在网络上传输后进行反序列化:
Period period = new Period(new Date(),new Date());
System.out.println("before serilization: " + period);
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("d:/Period.b"))) {
out.writeObject(period);
}
实现反序列化
通过ObjectInputStream从文件读取:
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("d:/Period.b"))) {
period = (Period) in.readObject();
System.out.println("after deseriliaztion: " + period);
}
自定义序列化-readObject-writeObject
readObject和writeObject用于自定义序列化。比如对静态字段进行序列化就需要编写readObject和writeObject来实现序列化与反序列化。
示例:
public class Foo extends AbstractFoo implements Serializable {
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// 手动反序列化并实例化父类的state
int x = in.readInt();
int y = in.readInt();
initialize(x,y);
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// 手动序列化父类的state
out.writeInt(getX());
out.writeInt(getY());
}
private static final long serialVersionUID = -3881436735239828405L;
}
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合使用默认的序列化形式。
比如下面的类:
public class Name implements Serializable {
private static final long serialVersionUID = 7188905641007443816L;
private String lastName;
private String firstName;
private String middleName;
public Name(String lastName, String firstName, String middleName) {
this.lastName = lastName;
this.firstName = firstName;
this.middleName = middleName;
}
}
从逻辑的角度而言,一个名字包含姓、名和中间名。Name中的实例域精确的反应了它的逻辑内容。
即使你确定了使用默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。对于Name这个类而言,readObject方法必须确保firstName和lastName是非null的。
修正的版本:
public class Name implements Serializable {
private static final long serialVersionUID = 7188905641007443816L;
private String lastName;
private String firstName;
private String middleName;
public Name(String lastName, String firstName, String middleName) {
this.lastName = lastName;
this.firstName = firstName;
this.middleName = middleName;
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();// 从该流中读取当前类的非静态和非瞬态字段。
String lastNameTemp = s.readObject().toString();
String firstNameTemp = s.readObject().toString();
String middleNameTemp = s.readObject().toString();
if (lastNameTemp == null || firstNameTemp == null) {
throw new IllegalArgumentException("firstName or lastName can't be null!");
}
this.lastName = lastNameTemp;
this.middleName = middleNameTemp;
this.firstName = firstNameTemp;
}
}
ps:readObject是ObjectInputStream在反序列化的时候通过反射调用的。
保护性的编写readObject方法
readObject实际上相当于另一个公有的构造器,如同其他构造器一样,也必须注意同样的所有注意事项。构造器必须检查其参数的有效性,并且在必要的时候对参数进行有效性拷贝 。
同样的,readObject也应该这么做。
不严格的说,readObject是一个“用字节流来作为唯一参数“的构造器。
所以,我们需要编写readObject方法,并对从流读取的对象的域进行合法性校验。
public class Period implements Serializable {
private static final long serialVersionUID = -5896804371611599645L;
private Date start;
private Date end;
public Period(Date start, Date end) {
Date s = new Date(start.getTime());
Date e = new Date(end.getTime());
if (s.before(e)) {
throw new IllegalArgumentException("start can't after end.");
}
this.start = s;
this.end = e;
}
public Date getStart() {
return new Date(start.getTime());
}
public Date getEnd() {
return new Date(end.getTime());
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
// 保护性拷贝易变的组件,否则攻击者可能通过篡改序列化后的字节流,拿到Period的2个Date域的引用,从而进行修改,最终改变Period。
start = new Date(start.getTime());
end = new Date(end.getTime());
if (start.after(end)) {
throw new InvalidObjectException(start + " after " + end);
}
}
@Override
public String toString() {
return start + " - " + end;
}
}
上述代码,如果不对start和end进行保护性拷贝,实际上是有可能篡改Period对象的。
篡改方式:
通过伪造字节流,创建可变的Period仍是可能的,做法是:字节流以一个有效的Period开头,然后附加2个额外的引用,指向Period实例域的2个私有的Date域。
攻击者从ObjectInputStream读取Period实例,然后读取附加在其后面的“恶意编制的对象引用”。这些对象引用使得攻击者能够访问到Period对象内部的私有的Date域所引用的对象。
通过改变这些Date实例,可以改变Period实例。
篡改实例:
/**
* @author Donny
* @createTime 2020-05-06 16:48
* @description 可变的Period
*/
public class MutablePeriod {
private final Period period;
private final Date start;
private final Date end;
public MutablePeriod() {
/**
* 如果不对Period的readObject做保护性拷贝
* 通过伪造字节流,创建可变的Period仍是可能的,做法是:字节流以一个有效的Period开头,然后附加2个额外的引用,指向Period实例域的2个私有的Date域。
* 攻击者从ObjectInputStream读取Period实例,然后读取附加在其后面的“恶意编制的对象引用”。这些对象引用使得攻击者能够访问到Period对象内部的私有的Date域所引用的对象。
* 通过改变这些Date实例,可以改变Period实例。
*/
try {
// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(baos);
// 序列化一个有效的Period对象
out.writeObject(new Period(new Date(),new Date()));
/**
* Append rogue "previous object refs" for internal Date fields in Period.
* For details,see "Java Object Serialization Specification",Section 6.4.
*/
byte[] ref = {0x71,0,0x7e,0,5}; // Ref #5
baos.write(ref);// start field
ref[4] = 4; // Ref #4
baos.write(ref); // end field
// 反序列化
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
period = (Period) in.readObject();
start = (Date) in.readObject();
end = (Date) in.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new AssertionError(e);
}
}
public static void main(String[] args) {
MutablePeriod mp = new MutablePeriod();
Period p = mp.period;
Date pEnd = mp.end;
pEnd.setYear(78);
System.out.println(p);
pEnd.setYear(69);
System.out.println(p);
}
}
让序列化的对象和反序列化后的对象为同一对象
如果使用枚举实现的单例模式,可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象(JVM对此提供保障)。
对于其他方式实现的单例模式,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve方法。
readResolve特性允许你用readObject创建的实例代替另一个实例(反序列化的)。对于一个正在被反序列化的对象,如果它的类定义了一个readResolve方法啊,并且具备正确的声明,那么在反序列化之后,新建对象上的readResolve方法就会被调用。然后,该方法返回的对象引用将会被返回,取代新建的对象。
public class Elvis3 implements Serializable {
private static final Elvis3 INSTANCE = new Elvis3();
public static Elvis3 getInstance() {
return INSTANCE;
}
// readResolve method to preserve singleton property
private Object readResolve() {
// return the one true Elvis and let the garbage collector take care of the Elvis impersonator.
return INSTANCE;
}
}
上面的readResolve忽略了被反序列化的对象,只返回该内初始化时创建的那个特殊的INSTANCE实例 。
如果依赖readResolve对实例进行控制,则带有对象引用类型的所有的域都应该声明为transient。
如果必须编写可序列化的实例受控的类,它的实例在编译时还不知道,那么就不能通过枚举的方式来实现,只能通过通过readResolve来保证。
考虑使用序列化代理序列化实例
当你发现自己必须在一个不能被客户端扩展的类上编写readObject或writeObject方法时,就应该考虑使用序列化代理模式。要想稳健的将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。
实现方式(本例中Period为待序列化的类,SerializationProxy是序列化代理类):
1.序列化代理类与要序列化的类都实现Serializable接口;
2.序列化代理带有一个接收一个待序列化的类参数的构造器;
3.待序列化类编写writeReplace方法,并返回序列化代理对象;
4.在序列化代理类中编写readResolve方法,返回待序列化实例。
实现过程:
1.序列化
在我们序列化Period时,序列化系统发现Period中定义了writeReplace,转而使用writeReplace返回对象的序列化方式;
Period.writeReplace() ==> SerializationProxy.writeObject().也就是说序列化后存储的实际是序列化代理对象(SerializationProxy)。
2.反序列化
SerializationProxy.readObject() ==> SerializationProxy.readResolve().
反序列化的时候因为已经知道是SerializationProxy,所以直接使用它的反序列化方式。
要序列化的类:
public class Period implements Serializable {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
Date s = new Date(start.getTime());
Date e = new Date(end.getTime());
if (s.after(e)) {
throw new IllegalArgumentException(start + " after " + e);
}
this.start = s;
this.end = e;
}
public Date getStart() {
return new Date(start.getTime());
}
public Date getEnd() {
return new Date(end.getTime());
}
@Override
public String toString() {
return start + " - " + end;
}
private Object writeReplace() {
System.out.println("call Period.writeReplace().");
return new SerializationProxy(this);
}
private void readObject(ObjectInputStream in) throws InvalidObjectException {
System.out.println("call Period.readObject().");
throw new InvalidObjectException("proxy required.");
}
private void writeObject(ObjectOutputStream out) throws InvalidObjectException {
System.out.println("call Period.writeObject().");
throw new InvalidObjectException("proxy required.");
}
}
序列化代理类:
public class SerializationProxy implements Serializable {
private static final long serialVersionUID = -2475499946017458008L;
private final Date start;
private final Date end;
public SerializationProxy(Period period) {
this.start = period.getStart();
this.end = period.getEnd();
}
private Object readResolve() {
System.out.println("call SerializationProxy.readResolve().");
return new Period(start,end);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("call SerializationProxy.readObject().");
in.defaultReadObject();
}
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("call SerializationProxy.writeObject().");
out.defaultWriteObject();
}
}
测试代码:
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 对Period进行序列化和反序列化
String name = "d:/Period.d";
Period period = new Period(new Date(),new Date());
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(name))) {
out.writeObject(period);
}
System.out.println("start to deserilize.");
try(ObjectInputStream in = new ObjectInputStream(new FileInputStream(name))) {
period = (Period) in.readObject();
System.out.println(period.toString());
}
}
}
测试结果:
call Period.writeReplace().
call SerializationProxy.writeObject().
start to deserilize.
call SerializationProxy.readObject().
call SerializationProxy.readResolve().
Thu May 07 11:00:32 CST 2020 - Thu May 07 11:00:32 CST 2020
总结
通过本文的内容,你会发现实现序列化从来都表示实现Serializable接口就完事了,要考虑的事情有很多。如何编写合适的序列化代码是一个值得考虑的问题。
猜你喜欢
- 2024-10-08 Java核心知识 基础六 JAVA序列化(创建可复用的Java对象)
- 2024-10-08 java中的序列化(JAVA中的序列化)
- 2024-10-08 Java 面试题之 序列化和反序列化的深入理解
- 2024-10-08 Oracle:Java 的序列化就是个错误,我们要删掉它!
- 2024-10-08 java序列化和反序列化实例详解(java序列化和反序列化实例详解区别)
- 2024-10-08 java对象序列化(java对象序列化到文件)
- 2024-10-08 实战案例:轻松搞定Java两种序列化机制
- 2024-10-08 java序列化机制之protobuf(快速高效跨语言)
- 2024-10-08 Java路径-40-Java序列化(未检测到java sdk环境请在配置中添加sdk安装路径)
- 2024-10-08 java序列化知多少(java序列化怎么实现)
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)