这篇文章次要给大家解说序列化和反序列化。
序列化是网络通信中十分重要的一个机制,好的序列化形式可能间接影响数据传输的性能。
序列化
所谓的序列化,就是把一个对象,转化为某种特定的模式,而后以数据流的形式传输。
比方把一个对象间接转化为二进制数据流进行传输。当然这个对象能够转化为其余模式之后再转化为数据流。
比方 XML、JSON 等格局。它们通过另外一种数据格式表白了一个对象的状态,而后再把这些数据转化为二进制数据流进行网络传输。
反序列化
反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列复原为对象的过程成为对象的反序列化
序列化的高阶意识
后面的代码中演示了,如何通过 JDK 提供了 Java 对象的序列化形式实现对象序列化传输,次要通过输入流 java.io.ObjectOutputStream 和对象输出流 java.io.ObjectInputStream 来实现。
java.io.ObjectOutputStream:示意对象输入流 , 它的 writeObject(Object obj)办法能够对参数指定的 obj 对象进行序列化,把失去的字节序列写到一个指标输入流中。
java.io.ObjectInputStream:示意对象输出流 , 它的 readObject()办法源输出流中读取字节序列,再把它们反序列化成为一个对象,并将其返回
须要留神的是,被序列化的对象须要实现 java.io.Serializable 接口
serialVersionUID 的作用
在 IDEA 中通过如下设置能够生成 serializeID,如图 5 - 1 所示
字面意思上是序列化的版本号,但凡实现 Serializable 接口的类都有一个示意序列化版本标识符的动态变量。
<center> 图 5 -1</center>
上面演示一下 serialVersionUID 的作用。首先须要创立一个一般的 spring boot 我的项目,而后依照上面的步骤来进行演示
创立 User 对象
public class User implements Serializable {
private static final long serialVersionUID = -8826770719841981391L;
private String name;
private int age;
}
编写 Java 序列化的代码
public class JavaSerializer {public static void main(String[] args) {User user=new User();
user.setAge(18);
user.setName("Mic");
serialToFile(user);
System.out.println("序列化胜利,开始反序列化");
User nuser=deserialFromFile();
System.out.println(nuser);
}
private static void serialToFile(User user){
try {
ObjectOutputStream objectOutputStream=
new ObjectOutputStream(new FileOutputStream(new File("user")));
objectOutputStream.writeObject(user);
} catch (IOException e) {e.printStackTrace();
}
}
private static <T> T deserialFromFile(){
try {ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(new File("user")));
return (T)objectInputStream.readObject();} catch (IOException | ClassNotFoundException e) {e.printStackTrace();
}
return null;
}
}
UID 验证演示步骤
- 先将 user 对象序列化到文件中
- 而后批改 user 对象,减少 serialVersionUID 字段
- 而后通过反序列化来把对象提取进去
- 演示预期后果:提醒无奈反序列化
论断
Java 的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比拟,如果雷同就认为是统一的,能够进行反序列化,否则就会呈现序列化版本不统一的异样,即是 InvalidCastException。
从后果能够看出,文件流中的 class 和 classpath 中的 class,也就是批改过后的 class,不兼容了,出于平安机制思考,程序抛出了谬误,并且回绝载入。从谬误后果来看,如果没有为指定的 class 配置 serialVersionUID,那么 java 编译器会主动给这个 class 进行一个摘要算法,相似于指纹算法,只有这个文件有任何改变,失去的 UID 就会截然不同的,能够保障在这么多类中,这个编号是惟一的。所以,因为没有显指定 serialVersionUID,编译器又为咱们生成了一个 UID,当然和后面保留在文件中的那个不会一样了,于是就呈现了 2 个序列化版本号不统一的谬误。因而,只有咱们本人指定了 serialVersionUID,就能够在序列化后,去增加一个字段,或者办法,而不会影响到前期的还原,还原后的对象照样能够应用,而且还多了办法或者属性能够用。
tips: serialVersionUID有两种显示的生成形式:
一是默认的 1L,比方:private static final long serialVersionUID = 1L;
二是依据类名、接口名、成员办法及属性等来生成一个 64 位的哈希字段
当实现 java.io.Serializable 接口的类没有显式地定义一个 serialVersionUID 变量时候,Java 序列化机制会依据编译的 Class 主动生成一个 serialVersionUID 作序列化版本比拟用,这种状况下,如果 Class 文件 (类名,办法明等) 没有发生变化(减少空格,换行,减少正文等等),就算再编译屡次,serialVersionUID 也不会变动的。
Transient 关键字
Transient 关键字的作用是控制变量的序列化,在变量申明前加上该关键字,能够阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
如果咱们心愿 User 类中的 name 字段不序列化,则依照以下计划进行批改。
批改 User 类
public class User implements Serializable {
private static final long serialVersionUID = -8826770719841981391L;
private transient String name;
private int age;
}
测试成果
public class JavaSerializer {public static void main(String[] args) {User user=new User();
user.setAge(18);
user.setName("Mic");
serialToFile(user);
System.out.println("序列化胜利,开始反序列化");
User nuser=deserialFromFile();
System.out.println(nuser.getName()); // 打印反序列化的后果,发现后果是 NULL.
}
}
绕开 transient 机制
在 User 类中重写 writeObject 和 readObject 办法。
public class User implements Serializable {
private static final long serialVersionUID = -8826770719841981391L;
private transient String name;
private int age;
private void writeObject(ObjectOutputStream out) throws IOException {out.defaultWriteObject();
out.writeObject(name);// 减少写入 name 字段
}
private void readObject(ObjectInputStream in) throws Exception{in.defaultReadObject();
name=(String)in.readObject();}
}
这两个办法是在 ObjectInputStream 和 ObjectOutputStream 中,别离反序列化和序列化对象时反射调用指标对象中的这两个办法。
序列化的总结
- Java 序列化只是针对对象的状态进行保留,至于对象中的办法,序列化不关怀
- 当一个父类实现了序列化,那么子类会主动实现序列化,不须要显示实现序列化接口
- 当一个对象的实例变量援用了其余对象,序列化这个对象的时候会主动把援用的对象也进行序列化(实现深度克隆)
- 当某个字段被申明为 transient 后,默认的序列化机制会疏忽这个字段
- 被申明为 transient 的字段,如果须要序列化,能够增加两个公有办法:writeObject 和 readObject
常见的序列化技术及优劣剖析
随着分布式架构、微服务架构的遍及。服务与服务之间的通信成了最根本的需要。这个时候,咱们不仅须要思考通信的性能,也须要思考到语言多元化问题
所以,对于序列化来说,如何去晋升序列化性能以及解决跨语言问题,就成了一个重点思考的问题。
因为 Java 自身提供的序列化机制存在两个问题
- 序列化的数据比拟大,传输效率低
- 其余语言无奈辨认和对接
以至于在起初的很长一段时间,基于 XML 格局编码的对象序列化机制成为了支流,一方面解决了多语言兼容问题,另一方面比二进制的序列化形式更容易了解。
以至于基于 XML 的 SOAP 协定及对应的 WebService 框架在很长一段时间内成为各个支流开发语言的必备的技术。
再到起初,基于 JSON 的简略文本格式编码的 HTTP REST 接口又基本上取代了简单的 Web Service 接口,成为分布式架构中近程通信的首要抉择。
然而 JSON 序列化存储占用的空间大、性能低等问题,同时挪动客户端利用须要更高效的传输数据来晋升用户体验。在这种状况下与语言无关并且高效的二进制编码协定就成为了大家谋求的热点技术之一。
首先诞生的一个开源的二进制序列化框架 -MessagePack。它比 google 的 Protocol Buffers 呈现得还要早。
XML 序列化框架介绍
XML 序列化的益处在于可读性好,不便浏览和调试。然而序列化当前的字节码文件比拟大,而且效率不高,实用于对性能不高,而且 QPS 较低的企业级外部零碎之间的数据交换的场景,同时 XML 又具备语言无关性,所以还能够用于异构零碎之间的数据交换和协定。比方咱们熟知的 Webservice,就是采纳 XML 格局对数据进行序列化的。XML 序列化 / 反序列化的实现形式有很多,熟知的形式有 XStream 和 Java 自带的 XML 序列化和反序列化两种。
引入 jar 包
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.12</version>
</dependency>
编写测试程序
public class XMLSerializer {public static void main(String[] args) {User user=new User();
user.setName("Mic");
user.setAge(18);
String xml=serialize(user);
System.out.println("序列化实现:"+xml);
User nuser=deserialize(xml);
System.out.println(nuser);
}
private static String serialize(User user){return new XStream(new DomDriver()).toXML(user);
}
private static User deserialize(String xml){return (User)new XStream(new DomDriver()).fromXML(xml);
}
}
JSON 序列化框架
JSON(JavaScript Object Notation)是一种轻量级的数据交换格局,绝对于 XML 来说,JSON 的字节流更小,而且可读性也十分好。当初 JSON 数据格式在企业使用是最广泛的
JSON 序列化罕用的开源工具有很多
- Jackson(https://github.com/FasterXML/…)
- 阿里开源的 FastJson(https://github.com/alibaba/fa…)
- Google 的 GSON (https://github.com/google/gson)
这几种 json 序列化工具中,Jackson 与 fastjson 要比 GSON 的性能要好,然而 Jackson、GSON 的稳定性要比 Fastjson 好。而 fastjson 的劣势在于提供的 api 非常容易应用
引入 jar 包
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
编写测试程序
public class JsonSerializer{public static void main(String[] args) {User user=new User();
user.setName("Mic");
user.setAge(18);
String xml=serializer(user);
System.out.println("序列化实现:"+xml);
User nuser=deserializer(xml);
System.out.println(nuser);
}
private static String serializer(User user){return JSON.toJSONString(user);
}
private static User deserializer(String json){return (User)JSON.parseObject(json,User.class);
}
}
Hessian 序列化
Hessian 是一个反对跨语言传输的二进制序列化协定,绝对于 Java 默认的序列化机制来说,Hessian 具备更好的性能和易用性,而且反对多种不同的语言
实际上 Dubbo 采纳的就是 Hessian 序列化来实现,只不过 Dubbo 对 Hessian 进行了重构,性能更高
引入 jar 包
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>
编写测试程序
public class HessianSerializer {public static void main(String[] args) throws IOException {User user=new User();
user.setName("Mic");
user.setAge(18);
byte[] bytes=serializer(user);
System.out.println("序列化实现");
User nuser=deserializer(bytes);
System.out.println(nuser);
}
private static byte[] serializer(User user) throws IOException {ByteArrayOutputStream bos=new ByteArrayOutputStream(); // 示意输入到内存的实现
HessianOutput ho=new HessianOutput(bos);
ho.writeObject(user);
return bos.toByteArray();}
private static User deserializer(byte[] data) throws IOException {ByteArrayInputStream bis=new ByteArrayInputStream(data);
HessianInput hi=new HessianInput(bis);
return (User)hi.readObject();}
}
Avro 序列化
Avro 是一个数据序列化零碎,设计用于反对大批量数据交换的利用。它的次要特点有:反对二进制序列化形式,能够便捷,疾速地解决大量数据;动静语言敌对,Avro 提供的机制使动静语言能够不便地解决 Avro 数据。
Avro 是 apache 下 hadoop 的子项目,领有序列化、反序列化、RPC 性能。序列化的效率比 jdk 更高,与 Google 的 protobuffer 相当,比 facebook 开源 Thrift(后由 apache 治理了)更优良。
因为 avro 采纳 schema,如果是序列化大量类型雷同的对象,那么只须要保留一份类的构造信息 + 数据,大大减少网络通信或者数据存储量。
引入 jar 包
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro-ipc</artifactId>
<version>1.8.2</version>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.avro</groupId>
<artifactId>avro-maven-plugin</artifactId>
<version>1.8.2</version>
<executions>
<execution>
<id>schemas</id>
<phase>generate-sources</phase>
<goals>
<goal>schema</goal>
</goals>
<configuration>
<sourceDirectory>${project.basedir}/src/main/avro</sourceDirectory>
<outputDirectory>${project.basedir}/src/main/java</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
编写 avsc 文件
创立 /src/main/avro 目录,专门用来存储 Avrode scheme 定义文件。
{
"namespace":"com.gupao.example",
"type":"record",
"name":"Person",
"fields":[{"name":"name","type":"string"},
{"name":"age","type":"int"},
{"name":"sex","type":"string"}
]
}
avsc 文件中的语法定义如下:
- namespace: 命名空间,在应用插件生成代码的时候,User 类的包名就是它
- type: 有 records, enums, arrays, maps, unions , fixed 取值,records 是相当于一般的 class
- name: 类名,类的全名有 namespace+name 形成
- doc: 正文
- aliases: 取的别名,其余中央应用能够应用别名来援用
-
fields: 属性
- name: 属性名
- type: 属性类型,能够是用 [“int”,”null”] 或者 [“int”,1] 执行默认值
- default: 也能够应用该字段指定默认值
- doc: 正文
生成代码
执行maven install
,
会在 main/java 目录下生成 Person 类。
编写测试程序
public class AvroSerializer {public static void main(String[] args) throws IOException {Person person=Person.newBuilder().setName("Mic").setAge(18).setSex("男").build();
ByteBuffer byteBuffer=person.toByteBuffer(); // 序列化
System.out.println("序列化大小:"+byteBuffer.array().length);
Person nperson=Person.fromByteBuffer(byteBuffer);
System.out.println("反序列化:"+nperson);
}
}
上面这种形式是基于文件的模式来实现序列化和反序列化
public class AvroSerializer {public static void main(String[] args) throws IOException {Person person=Person.newBuilder().setName("Mic").setAge(18).setSex("男").build();
/* ByteBuffer byteBuffer=person.toByteBuffer(); // 序列化
System.out.println("序列化大小:"+byteBuffer.array().length);
Person nperson=Person.fromByteBuffer(byteBuffer);
System.out.println("反序列化:"+nperson);*/
DatumWriter<Person> personDatumWriter=new SpecificDatumWriter<>(Person.class);
DataFileWriter<Person> dataFileWriter=new DataFileWriter<>(personDatumWriter);
dataFileWriter.create(person.getSchema(),new File("person.avro"));
dataFileWriter.append(person);
dataFileWriter.close();
System.out.println("序列化胜利.....");
DatumReader<Person> personDatumReader=new SpecificDatumReader<>(Person.class);
DataFileReader<Person> dataFileReader=new DataFileReader<Person>(new File("person.avro"),personDatumReader);
Person nper=dataFileReader.next();
System.out.println(nper);
}
}
kyro 序列化框架
Kryo 是一种十分成熟的序列化实现,曾经在 Hive、Storm)中应用得比拟宽泛,不过它不能跨语言. 目前 dubbo 曾经在 2.6 版本反对 kyro 的序列化机制。它的性能要优于之前的 hessian2
zookeeper 中应用 jute 作为序列化
Protobuf 序列化
Protobuf 是 Google 的一种数据交换格局,它独立于语言、独立于平台。Google 提供了多种语言来实现,比方 Java、C、Go、Python,每一种实现都蕴含了相应语言的编译器和库文件,Protobuf 是一个纯正的表示层协定,能够和各种传输层协定一起应用。
Protobuf 应用比拟宽泛,次要是空间开销小和性能比拟好,非常适合用于公司外部对性能要求高的 RPC 调用。另外因为解析性能比拟高,序列化当前数据量绝对较少,所以也能够利用在对象的长久化场景中
然而要应用 Protobuf 会相对来说麻烦些,因为他有本人的语法,有本人的编译器,如果须要用到的话必须要去投入老本在这个技术的学习中
protobuf 有个毛病就是要传输的每一个类的构造都要生成对应的 proto 文件,如果某个类产生批改,还得从新生成该类对应的 proto 文件
应用 protobuf 开发的个别步骤是
- 配置开发环境,装置 protocol compiler 代码编译器
- 编写.proto 文件,定义序列化对象的数据结构
- 基于编写的.proto 文件,应用 protocol compiler 编译器生成对应的序列化 / 反序列化工具类
- 基于主动生成的代码,编写本人的序列化利用
装置 protobuf 编译工具
- https://github.com/google/pro… 找到 protoc-3.5.1-win32.zip
-
编写 proto 文件
syntax="proto2"; package com.gupao.example; option java_outer_classname="UserProtos"; message User { required string name=1; required int32 age=2; }
数据类型阐明如下:
- string / bytes / bool / int32(4 个字节)/int64/float/double
- enum 枚举类
- message 自定义类
-
修饰符
- required 示意必填字段
- optional 示意可选字段
- repeated 可反复,示意汇合
- 1,2,3,4 须要在以后范畴内是惟一的,示意程序
-
生成实例类,在 cmd 中运行如下命令
protoc.exe --java_out=./ ./User.proto
实现序列化
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.12.2</version>
</dependency>
编写测试代码.
public class ProtobufSerializer {public static void main(String[] args) throws InvalidProtocolBufferException {UserProtos.User user=UserProtos.User.newBuilder().setName("Mic").setAge(18).build();
ByteString bytes=user.toByteString();
System.out.println(bytes.toByteArray().length);
UserProtos.User nUser=UserProtos.User.parseFrom(bytes);
System.out.println(nUser);
}
}
Protobuf 序列化原理解析
咱们能够把序列化当前的数据打印进去看看后果
public static void main(String[] args) {UserProtos.User user=UserProtos.User.newBuilder().
setAge(300).setName("Mic").build();
byte[] bytes=user.toByteArray();
for(byte bt:bytes){System.out.print(bt+" ");
}
}
10 3 77 105 99 16 -84 2
咱们能够看到,序列化进去的数字根本看不懂,然而序列化当前的数据的确很小,那咱们接下来带大家去理解一下底层的原理
失常来说,要达到最小的序列化后果,肯定会用到压缩的技术,而 protobuf 外面用到了两种压缩算法,一种是 varint,另一种是 zigzag
varint
先说第一种,咱们先来看【Mic】是怎么被压缩的
“Mic”这个字符,须要依据 ASCII 对照表转化为数字。
M =77、i=105、c=99
所以后果为 77 105 99
大家必定有个疑难,这里的后果为什么间接就是 ASCII 编码的值呢?怎么没有做压缩呢?有没有同学可能答复进去
起因是,varint 是对字节码做压缩,然而如果这个数字的二进制只须要一个字节示意的时候,其实最终编码进去的后果是不会变动的。如果呈现须要大于一个字节的形式来示意,则须要进行压缩。
比方,咱们设置的 age=300,这里须要 2 个字节来存储。那看一下它是如何被压缩的。
300 如何被压缩
这两个字节字节别离的后果是:-84、2
-84 怎么计算来的呢?咱们晓得在二进制中示意正数的办法,高位设置为 1,并且是对应数字的二进制取反当前再计算补码示意(补码是反码 +1)
所以如果要反过来计算
- 【补码】10101100 -1 失去 10101011
- 【反码】01010100 失去的后果为 84. 因为高位是 1,示意正数所以后果为 -84
存储格局
protobuf 采纳 T -L- V 作为存储形式
tag 的计算形式是 field_number(以后字段的编号) << 3 | wire_type
比方 Mic 的字段编号是 1,类型 wire_type 的值为 2 所以:1 <<3 | 2 =10
age=300 的字段编号是 2,类型 wire_type 的值是 0,所以 : 2<<3|0 =16
所以依照 T -L- V 的格局,第一个字段为 name,所以它的数据为 {10} {3} {77 105 99},第二个字段为 age,{16} {2} {-82 2}
5.5.3 正数的存储形式
在计算机中,正数会被示意为很大的整数,因为计算机定义正数符号位为数字的最高位,所以如果采纳 varint 编码表示一个正数,那么肯定须要 5 个比特位。所以在 protobuf 中通过 sint32/sint64 类型来示意正数,正数的解决模式是先采纳 zigzag 编码(把符号数转化为无符号数),在采纳 varint 编码。
sint32:(n << 1) ^ (n >> 31)
sint64:(n << 1) ^ (n >> 63)
比方存储一个(-300)的值。
-
批改 proto 原始文件
message User { required string name=1; required int32 age=2; required sint32 status=3; // 减少一个 sint 的字段 }
-
设置一个值
UserProtos.User user=UserProtos.User.newBuilder().setAge(300).setName("Mic").setStatus(-300).build();
- 此时的输入后果:
10 3 77 105 99 16 -84 2 24 -41 4
咱们发现,针对于正数类型,压缩进去的数据是不一样的,这里采纳的编码方式是 zigzag 的编码,再采纳 varint 进行编码压缩。
比方存储一个(-300)的值
-300
原码:0001 0010 1100
取反:1110 1101 0011
加 1:1110 1101 0100
n<<1: 整体左移一位,左边补 0 -> 1101 1010 1000
n>>31: 整体右移 31 位,右边补 1 -> 1111 1111 1111
n<<1 ^ n >>31
1101 1010 1000 ^ 1111 1111 1111 = 0010 0101 0111
十进制:0010 0101 0111 = 599
这样做的目标,是打消高位的 1,从而造成一个能够被压缩的数据。针对 599 再采纳 varint 进行编码。
varint算法:从右往做,选取 7 位,高位补 1 /0(取决于字节数)
失去两个字节
1101 0111 0000 0100
-41 、4
5.5.4 总结
Protocol Buffer 的性能好,次要体现在 序列化后的数据体积小 & 序列化速度快,最终使得传输效率高,其起因如下:
序列化速度快的起因:
a. 编码 / 解码 形式简略(只须要简略的数学运算 = 位移等等)
b. 采纳 Protocol Buffer 本身的框架代码 和 编译器 共同完成
序列化后的数据量体积小(即数据压缩成果好)的起因:
a. 采纳了独特的编码方式,如 Varint、Zigzag 编码方式等等
b. 采纳 T – L – V 的数据存储形式:缩小了分隔符的应用 & 数据存储得紧凑
序列化技术选型
技术层面
- 序列化空间开销,也就是序列化产生的后果大小,这个影响到传输的性能
- 序列化过程中耗费的时长,序列化耗费工夫过长影响到业务的响应工夫
- 序列化协定是否反对跨平台,跨语言。因为当初的架构更加灵便,如果存在异构零碎通信需要,那么这个是必须要思考的
- 可扩展性 / 兼容性,在理论业务开发中,零碎往往须要随着需要的疾速迭代来实现疾速更新,这就要求咱们采纳的序列化协定基于良好的可扩展性 / 兼容性,比方在现有的序列化数据结构中新增一个业务字段,不会影响到现有的服务
- 技术的风行水平,越风行的技术意味着应用的公司多,那么很多坑都曾经淌过并且失去了解决,技术解决方案也绝对成熟
- 学习难度和易用性
选型倡议
- 对性能要求不高的场景,能够采纳基于 XML 的 SOAP 协定
- 对性能和间接性有比拟高要求的场景,那么 Hessian、Protobuf、Thrift、Avro 都能够。
- 基于前后端拆散,或者独立的对外的 api 服务,选用 JSON 是比拟好的,对于调试、可读性都很不错
- Avro 设计理念偏于动静类型语言,那么这类的场景应用 Avro 是能够的
版权申明:本博客所有文章除特地申明外,均采纳 CC BY-NC-SA 4.0 许可协定。转载请注明来自
Mic 带你学架构
!
如果本篇文章对您有帮忙,还请帮忙点个关注和赞,您的保持是我一直创作的能源。欢送关注同名微信公众号获取更多技术干货!