java 序列化
什么是序列化?
对象和二进制的转换。
转换的目的是啥?
对象转换为二进制,然后再把二进制恢复为对象。
具体应用场景是,把对象写到磁盘文件,或者更常见的就是把对象传到远程机器(比如,dubbo rpc 框架)。
什么是 java 序列化?
java 序列化特殊一点点,是对象和字节数组( byte[] )的转换。但是,字节数组的本质也是二进制。
序列化的作用?
所以,无论是其他语言,还是 java 语言的序列化,本质作用都是为了从磁盘文件或者远程机器恢复对象。
官方文档介绍
Serialization is used for lightweight persistence and for communication via sockets or Java Remote Method Invocation (Java RMI).
https://docs.oracle.com/javase/7/docs/technotes/guides/serialization/index.html
如何实现序列化?
实现序列化接口
public class Person implements Serializable {
private static final long serialVersionUID = 2709425275741743919L;
}
复制代码
demo
pojo 类,主要是要实现序列化接口。
package test2;
import java.io.Serializable;
public class Person implements Serializable { //实现序列化接口
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
private String address;
public Person() {
}
public Person(String name, Integer age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public String toString() {
return "test2.Person{" +
"name='" + name + ''' +
", age=" + age +
", address='" + address + ''' +
'}';
}
}
复制代码
测试类,核心步骤
序列化
把对象转换为二进制(即字节数组),然后写到磁盘文件
2、反序列化
从磁盘文件恢复对象,其实就是把二进制再转换为对象
package test2;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
* @author gzh
* @createTime 2021/12/6 8:08 PM
*/
public class Test {
public static void main(String[] args) throws Exception {
testversion1L();
}
public static void testversion1L() throws Exception {
File file = new File("Person.out");
// 序列化
ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
Person Person = new Person("Haozi", 22, "上海");
oout.writeObject(Person); //把对象转换为二进制(即字节数组),然后写到磁盘文件
oout.close();
// 反序列化
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newPerson2 = oin.readObject();//从磁盘文件恢复对象,就把二进制转换为对象
oin.close();
System.out.println(newPerson2);
}
}
复制代码
java 序列化到底干了什么?协议格式是什么?
最主要和最本质其实也是把对象转换为二进制。
格式就是,java 独特的那一套东西,主要包括:
java 基础数据类型
java 非基础数据类型
所以,总之,java 序列化的二进制,只有 java 语言自己能够识别,即只有 java 语言自己才可以把二进制再恢复转换为对象。
源码分析参考:
https://juejin.cn/post/7039166805448851463#heading-2
序列化 id 的作用到底是什么?
jdk api 官方文档
Serializable
If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification.
However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassExceptions during deserialization.
Therefore, to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit serialVersionUID value.
It is also strongly advised that explicit serialVersionUID declarations use the private modifier where possible, since such declarations apply only to the immediately declaring class--serialVersionUID fields are not useful as inherited members.
总结
序列化 id,本质是 pojo 类的版本号。
如果没显式写序列化 id,jvm 默认生成一个随机值。
如果写了,一般情况下,值写 1L 就可以了。
用 idea 生成一个很大的随机值也可以。
官方推荐,最好显示写一个值(一般情况写 1L 就可以了)——因为 jvm 的实现可能不一样,导致消费者和提供者的 jvm 生成的版本号不一样。
如果提供者新增了字段或者修改了字段类型,就要升级序列化 id 版本,这样的话,提供者反序列化的时候就会异常:消费者和提供者的序列化 id 版本不一致。这正是我们要的结果——因为如果提供者新增了字段或者修改了字段类型,但是提供者没有升级序列化 id 版本,而是仍然和提供者的版本一样,那么提供者反序列化的时候就不会异常,但是这个时候,提供者的新增字段的值是 null(提供者新增了字段)或者类型转换异常(提供者修改了字段类型)。
demo
接着上面的 demo 代码例子,这里再演示一下。
先给提供者的 pojo 类加个字段-email。
package test;
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
private Integer age;
private String address;
private String email; //新增字段
public Person() {
}
public Person(String name, Integer age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
public Person(String name, Integer age, String address,String email) {
this.name = name;
this.age = age;
this.address = address;
this.email = email;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
", address='" + address + ''' +
", email='" + email + ''' +
'}';
}
}
复制代码
然后,直接从上面 demo 已经生成的文件里反序列化,得到对象。注意,要注释掉序列化代码。
package test;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import test2.Person;
/**
* @author gzh
* @createTime 2021/12/6 8:08 PM
*/
public class Test {
public static void main(String[] args) throws Exception {
testversion1L();
}
public static void testversion1L() throws Exception {
File file = new File("Person.out");
// 序列化
// ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
// Person Person2 = new Person("Haozi", 22, "上海");
// oout.writeObject(Person2);
// oout.close();
// 反序列化
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newPerson2 = oin.readObject(); //得到对象
oin.close();
System.out.println(newPerson2);
}
}
复制代码
打印结果:
Person{name='Haozi', age=22, address='上海', email='null'}
复制代码
说明:
提供者必须也要有对应类 pojo 类
从打印结果看,直接从磁盘文件反序列化可以得到对象,前提是提供者也要存在同一个包同一个类名字的 pojo 类,提供者才可以创建对象。
如果提供者没有对应的 pojo 类,就会报错:找不到类异常。
Exception in thread "main" java.lang.ClassNotFoundException: Person
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:686)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1868)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at Test.testversion1L(Test.java:27)
at Test.main(Test.java:14)
复制代码
如何模拟这个异常?直接把 pojo 类删除或者换个名字即可。
提供者新增字段的值为什么是 null?
如果提供者有对应的 pojo 类,提供者就可以从磁盘文件的二进制数据,创建对象,正如上面的打印结果。但是,由于提供者新增了一个字段,而消费者写入二进制数据到磁盘文件的时候并没有这个字段,所以新增的字段的值是 null。
但是,实际工作当中,我们应该不允许这种情况出现。如果提供者新增了字段,pojo 类要升级一下版本,即序列化 id 的值改为 2L。
继续往下分析,正如上面的例子,消费者写入磁盘的二进制数据的版本是 1L,这个时候,提供者反序列化会报错:版本号不匹配,具体来说是,流(即消费者)的版本号是 1,本地(即提供者)的版本是 2。
Exception in thread "main" java.io.InvalidClassException: Person; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at Test.testversion1L(Test.java:27)
at Test.main(Test.java:14)
Disconnected from the target VM, address: '127.0.0.1:0', transport: 'socket'
Process finished with exit code 1
复制代码
虽然这个时候异常了,但是这正是我们想要的结果。因为可以一眼看出来,消费者和提供者的 pojo 版本不匹配,背后的本质肯定是提供者新增了字段或者修改了字段的类型。这个时候,消费者,也应该新增字段或者修改字段类型,和提供者保持一致。如果提供者没有升级版本号,虽然提供者没有报错,但是提供者新增字段的值是 null(因为消费者缺少该新增字段),这个时候其实反而是有问题的,因为正常情况下提供者新增了字段,肯定是要是使用该字段的值的,值从哪里来?肯定是消费者来。消费者怎么来?也新增字段,并且和提供者版本保持一致即可解决问题。
参考
https://www.liaoxuefeng.com/wiki/1252599548343744/1298366845681698
Java serialVersionUID 有什么作用?https://www.jianshu.com/p/91fa3d2ac892
评论