学习《Effective Java》和相关知识点,加入自己的理解整理。序列化中的自定义方法整理链接-https://blog.csdn.net/qq_37903936/article/details/90236423
序列化
对象被序列化后,成为字节流,可以在网络中传递或者存储在磁盘中,等待进行反序列化的读取,将字节流组装成对象。
必须注意地是,对象序列化保存的是对象的"状态",即它的成员变量。由此可知,对象序列化不会关注类中的静态变量,因为static修饰的是类变量,它存在于对象实例化之前,另外由transient关键字修饰的变量也不会被序列化。
定义
序列化
将对象编码成字节流
反序列化
从字节流编码中重新构建对象
反序列化炸弹
通过精心编制的字节流使反序列化的时候时间和使用空间爆炸式增长,如多层嵌套的HashSet实例,反序列化的时候需要计算hashcode值,计算次数成指数增长。下面方法,通过root生成的字节流,反序列化时需要的时间和空间根据循环次数指数增长。
static void bomb() {
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for (int i = 0; i < 5; i++) {
Set<Object> t1 = new HashSet<>();
Set<Object> t2 = new HashSet<>();
t1.add("foo");
s1.add(t1);
s1.add(t2);
s2.add(t1);
s2.add(t2);
s1 = t1;
s2 = t2;
}
System.out.println(root);
}
serialVersionUID
序列版本UID
- 每个可序列化的类都有一个唯一标识号与它有关。
- 如果在一个可序列化类中,没有声明一个私有静态的final的long类型的字段,系统就会对这个类的结构运用一个加密的散列函数,从而在运行时自动产生标识号。这个自动产生的值会根据类名称、接口名称、所有公有的和受保护的成员名称所影响,如果修改了任意内容,那么这个标识号也会发生改变,可能会导致兼容性发生变化,出现InvalidClassException,因此尽量指定serialVersionUID。
ObjectInputStream和ObjectOutputStream
ObjectInputStream的readObject方法进行反序列化。这个方法其实是个神奇的构造器,它可以将类路径上几乎任何类型的对象都实例化,只要该类型实现了Serializable 接口。
File file = new File("D:\\new\\person.txt");
ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
Person person = new Person("张三", 18);
oout.writeObject(person);
oout.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newPerson = oin.readObject();
oin.close();
System.out.println(newPerson);
默认的序列化形式
当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下 4 个缺点
- 它使这个类的导出 API 永远地束缚在该类的内部表示法上。
- 它会消耗过多的空间 。
- 它会消耗过多的时间 。
- 它会引起栈溢出,比如上面的bomb()方法
自定义的序列化形式
- 一般来讲,只有当自行设计的自定义序列化形式与默认的序列化形式基本相同时,才能接受默认的序列化形式。
- 对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而物理数据与逻辑数据应该是相分离的
- 如果一个对象的物理表示法等于它的逻辑内容,可能就适合于使用默认的序列化形式。
尽量不要实现Serializable
- 实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。可以看做序列化后的字节流变成了导出的Api的一部分,那么在后期修改的灵活性上会受到限制。
- 违背了类中成员变量的访问控制
- 如果接受了默认的序列化形式,并且以后又要修改这个类的内部表示法,那么可能会导致反序列化异常
- 它增加了出现Bug和安全漏洞的可能性。序列化机制是一种语言之外的对象创建机制。通常情况下我们都是利用构造器创建对象,但是反序列化是一个“隐藏的构造器”,具备与其他构造器相同的特点,反序列化构造器没有建立起对象内部各个成员之间的约束关系,因此会破坏对象的正常状态。
- 随着类发行新的版本,相关的测试负担也会增加。每次新版本的发布要检测序列化类在新老版本之间序列化和反序列化功能的正常性。
反序列化缺陷
- 反序列化炸弹
- 反序列化可能破坏构造器中的约束规则
保证反序列化的安全性
- 避免序列化攻击的最佳方式是永远不要反序列化任何东西
- 永远不要反序列化不被信任的数据
- 使用反序列化过滤:如果无法避免序列化,又不能绝对确保被反序列化的数据的安全性,就应利用Java9中新增的对象反序列化过滤,它可以在数据被反序列化之前,为它们定义一个过滤器,通过操作白名单和黑名单来允许或者拒绝某些类反序列化。
- 使用跨平台的数据结构表示法,如JSON
- 使用自定义序列化,精心设计自定义序列化的内容
- 使用序列化代理模式,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类被称作序列化代理,它应该有一个单独的构造器,其参数类型就是那个外围类。
package serializable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Objects;
public class Person implements Serializable{
private static final long serialVersionUID = 2883313162693024310L;
public Person(String name, Integer age) {
if (Objects.isNull(age) || Objects.isNull(name)) {
throw new RuntimeException("年龄或姓名不能为空");
} else if (age < 18) {
throw new RuntimeException("年龄不能小于18岁");
}
this.name = name;
this.age = age;
}
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
private Object writeReplace() {
return new PersonProxy(this);
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
private static class PersonProxy implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
PersonProxy(Person p) {
System.out.println("执行代理序列化");
this.name = p.name;
this.age = p.age;
}
private Object readResolve() {
System.out.println("执行代理反序列化");
return new Person(name, age);
}
}
}
package serializable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashSet;
import java.util.Set;
public class SimpleSerial {
public static void main(String[] args) throws Exception {
File file = new File("D:\\new\\person.txt");
ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
Person person = new Person("张三", 18);
oout.writeObject(person);
oout.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newPerson = oin.readObject();
oin.close();
System.out.println(newPerson);
}
}
使用序列化的场景
- 如果一个类作为另一个类的成员,而后者通过序列化实现功能,那么该类也应该实现序列化接口,否则会NotSerializableException异常
- 内部类不应该实现Serializable接口,但是静态成员类可以实现Serializable接口,比如序列化代理模式。