一、背景
我们平常写CRUD居多,写各种PO、DTO、VO...很多人习惯性的在实体类名后写上 implements Serializable,并带上静态常量属性 serialVersionUID,知道它是序列化使用。可是有多少人会深入想过,写与不写有什么影响呢?实现序列化接口的标准是什么呢?什么情况下需要去维护serialVersionUID呢?作为一个码农的我,当有人问到一系列的问题时,我只知道服务调用需要使用,版本号跟类的属性标识和反序列化有关系。因为在工作中,我有一次写feign接口还是httpClient调用(具体忘啦),出现接口调用不通,报什么序列化失败,版本号的问题,怎么解决的,我自己都忘啦,过后也缺少深入思考,也许这就是菜鸟永远是菜鸟的原因吧。今天这里,就具体学习了解下
二、概念
1、序列化(对象--->字节流)
序列化是一种处理对象流的机制,所谓对象流也就是将对象的内容进行流化,将数据分解成字节流,以便存储在文件中或在网络传输
2、反序列化(字节流--->对象)
知道序列化的含义,反序列顾名思义与序列化完全相反的过程,这里不重复说明
三、使用场景
1、网络传输
网络传输的数据都必须是二进制数据,但是在Java中都是对象,是没有办法在网络传输,因此需要对Java对象进行序列化,并且要求这个序列化是可逆的(即可进行反序列化),否则人家不知道你传递的是啥信息
2、对象持久化
将内存中的对象状态保存到文件或者数据库中
3、实现分布式对象
RMI(远程方法调用),要利用对象序列化运行远程主机上的服务,就像本地上运行对象一样
四、如何实现
1、Java原生序列化
前置条件:需要被序列化的类必须实现Serializable接口
具体实现
- 序列化 ObjectOutputStream#writeObject(Object obj)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class Student implements Serializable {
private String username;
@SneakyThrows
public static void main(String[] args) {
Student student = Student.builder().username("琵琶行").build();
try (FileOutputStream fileOut = new FileOutputStream("test.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(student);
log.info("序列化数据保存在文件【test.txt】中");
}
}
}
反序列化 @ObjectInputStream#readObject0方法
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class Student implements Serializable {
private String username;
@SneakyThrows
public static void main(String[] args) {
try (FileInputStream fileInputStream = new FileInputStream("test.txt");
ObjectInputStream in = new ObjectInputStream(fileInputStream)) {
Student student = (Student) in.readObject();
log.info("反序列化后的用户数据{}", student);
}
}
}
缺点:
1、效率较低,序列化后的流数据比较大
2、Java序列化是不跨语言的,必须通过Java的反序列化才能转化成内存中的对象;下面即将说的第三方序列化即JSON序列化是跨语言的,大多数语言是支持,比如我们后端传JSON给前端,然后js做反序列化
2、第三方序列化方式
JSON/Hessian(2) /xml/protobuf/kryo/MsgPack/FST/thrift/protostuff/Avro 其中常用的是 JSON , 但性能最好的是 百度的 jprotobuf ,是百度对Google的 protobuf 进行了封装。 jprotobuf 序列化的速度稍微慢点,但字节很小。 dubbo 使用的是Hessian2 序列化的速度很快 但是字节很大 。
五、JDK序列化serialVersionUID的作用
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致
1、生成方式
- 自动(隐式):Java序列化机制会根据编译的class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID
- 手动(显示):开发人员根据具体情况来指定的,我们应该显式指定一个版本号,这样做的话我们不仅可以增强对序列化版本的控制,而且也提高了代码的可移植性。因为不同的JVM有可能使用不同的策略来计算这个版本号,那样的话同一个类在不同的JVM下也会认为是不同的版本。
如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,未作更改的类,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化(序列化)和反串行化(反序列化)。
2、代码演示
- 没有显示指定版本号,对象属性没有任何修改,可正常序列化和反序列化
- 没有显示指定版本号,增加一个属性反序列化失败,提示版本号不一致
- 显示指定版本号,对象属性没有任何修改,可正常序列化和反序列化!
- 显示指定版本号,对象属性增加一个,可正常反序列化
总结:
升级、兼容老版本
Java 序列化机制会使用该数字对序列化和反序列化过程进行版本控制,以确保同一对象在不同时间或不同位置进行序列化和反序列化时都能够正确地被还原,并且不会因为版本差异引发异常等问题。
六、为什么传输的类要被序列化呢?
其实也并不是所有的类都要被序列化,我们看一下ObjectOutputStream#writeObject 一段方法源码
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
字符串,数组,枚举这些类型是不需要序列化的,但是除此之外如果不实现Serializable接口就会报NotSerializableException异常
七、注意
- transient 修饰的属性,是不会被序列化的;
- static修饰的属性,是不会被序列化的;
串行化只能保存对象的非静态成员交量,不能保存任何的成员方法和静态的成员变量,而且串行化保存的只是变量的值,对于变量的任何修饰符都不能保存。
序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。
八、序列化破坏单例
一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建,如果序列化的对象目标为单例对象,就违背了单例模式的初衷,相当于破坏了单例。
public class Student implements Serializable {
private String username;
private static Student singleton;
private Student() {
}
public static Student getSingleton() {
synchronized (Student.class) {
if (singleton == null) {
singleton = new Student();
singleton.username = "琵琶行";
}
}
return singleton;
}
@SneakyThrows
public static void main(String[] args) {
try (FileOutputStream fileOut = new FileOutputStream("test.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(Student.getSingleton());
log.info("序列化数据保存在文件【test.txt】中");
}
try (FileInputStream fileInputStream = new FileInputStream("test.txt");
ObjectInputStream in = new ObjectInputStream(fileInputStream)) {
Student o = (Student) in.readObject();
//false ,破坏了单例
System.out.println(o == Student.getSingleton());
log.info("反序列化后的用户数据{}", o);
}
}
}
源码分析
private Object readOrdinaryObject(boolean unshared)
throws IOException {
//省略部分代码......
//readObject 返回的对象
Object obj;
try {
//isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。反序列化会通过反射调用无参数的构造方法创建一个新的对象。desc.newInstance() 是通过反射创建新的对象
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
//省略部分代码......
//判断有没有实现ReadResolve方法
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) {
//类中定义啦readResolve,反射执行获取我们定义返回的对象
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
//desc.newInstance() 是通过反射创建新的对象与readResolve方法定义返回的对象不相等
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
//readResolve方法定义返回的对象赋值给obj
handles.setObject(passHandle, obj = rep);
}
}
//返回obj
return obj;
}
由以上分析,可得出解决方案,只需要我们序列化的类定义readResolve 返回我们的单例对象即可
/**
* 序列化破坏单例预防
* @return
*/
public Object readResolve(){
return singleton;
}