- 序列化
- JDK 的序列化
- JDK 序列化的一些细节
- Protobuf 序列化
- Protobuf 环境搭建与操作
- Protobuf 原理分析
- 实际数据传输
- 序列化技术选型
- 远程过程调用 RMI
- RPC
- Java RMI 应用实战 rmi-api、rmi-server、rmi-client
- Java RMI 流程分析
- Java RMI 源码分析
序列化Java 从 0 到架构师目录:【Java从0到架构师】学习记录
在 JVM 创建的对象是在内存当中的,当 JVM 停止运行,释放内存以后,JVM 内存中的对象也会被销毁。但是在有些场景下,我们需要把对象的数据持久化保存起来,就需要使用对应的序列化和反序列化技术。
- 序列化:把内存中的对象信息转化为字节数组的过程
序列化的目的:数据持久化,数据的网络传输 - 反序列化:序列化的逆向操作,把字节数组转换为对象的过程
Java 语言本身提供了对象序列化机制,也是 Java 语言本身最重要的底层机制之一。Java 本身提供的序列化机制存在两个问题:
- 序列化的数据比较大,传输效率低
- 其他语言无法识别和对接
序列化框架选型的常用指标:
- 序列化的字节数据大小
- 序列化的速度和系统资源开销
参考:【Java I/O流】File、字符集、字节流、字符流、缓冲流、数据流、对象流、序列化
对于 JDK 的序列化对象一定要实现 Serializable 接口
实际操作:
- 定义序列化对象
@Data
public class User implements Serializable {
private static final long serialVersionUID = -9212613021140645522L;
private String name;
private Integer age;
}
- 定义接口
// 定义一个序列化和反序列化的操作
public interface ISerializer {
//序列化操作, 把一个对象转换为字节数组(二进制数组)
byte[] serialize(T obj);
//反序列化操作, 把一个字节数组转换为对象
T deSerialize(byte[] data,Class clazz);
}
- 基于 JDK 的方式实现序列化
public class JavaSerialize implements ISerializer {
@Override
public byte[] serialize(T obj) {
//字节输出流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("序列化错误", e);
}
return bos.toByteArray();
}
@Override
public T deSerialize(byte[] data, Class clazz) {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
try {
ObjectInputStream objectInputStream = new ObjectInputStream(bis);
return (T)objectInputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("反序列化错误",e);
}
}
}
- 测试
public class JavaSerializeTest {
ISerializer serializer = new JavaSerialize();
@Test
public void testSerialize() throws Exception {
User user = new User();
user.setAge(18);
user.setName("wolf");
byte[] datas = serializer.serialize(user);
System.out.println("序列化大小:" + datas.length);
// 保存到文件
try (FileOutputStream fos = new FileOutputStream("user.dat")){
fos.write(datas);
fos.flush();
}
}
@Test
public void testDeserialize() throws Exception {
try (FileInputStream fis = new FileInputStream("user.dat")){
byte[] buffer = new byte[1024];
int len = -1;
if((len = fis.read(buffer)) != -1) {
buffer = Arrays.copyOf(buffer, len);
User u = serializer.deSerialize(buffer, User.class);
System.out.println("u = " + u);
}
}
}
}
JDK 序列化的一些细节
serialVersionUID:Java 的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。如果不指定 serialVersionUID,序列化某个类到本地以后,修改了类的一些属性,则反序列化就会失败,因为由于数据变更,自动计算出来的 serialVersionUID 也不一样了。
静态变量不会序列化:序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。
父类的序列化:
- 一个子类实现了 Serializable 接口,若它的父类没有实现 Serializable,那么对于父类中的变量不会实现序列化操作。
- 如果一个父类实现了 Serializabla 接口,子类可以不用实现 Serializable,也会对对象的属性进行序列化操作
transient:声明为 transient 的字段不会进行序列化,对于敏感信息可以声明为 transient
对象的克隆:在 Java 中存在一个 Cloneable 接口,实现这个接口的类都会具备 clone 的能力(clone 是在内存中进行),在性能方面会比直接通过 new 生成对象更好。在 Java 中,克隆分为深度克隆和浅克隆。
- 浅克隆:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。
// 1 实现 Cloneable 接口
// 2 实现 clone 方法, 修改方法权限为 public
@Override
public Boy clone() throws CloneNotSupportedException {
return (Boy) super.clone();
}
- 深度克隆:克隆出来的对象都是不一样的,引用的对象类型也是新的引用
// 1 所有对象都实现序列化的接口
// 2 自定义一个深度克隆方法deepClone, 通过字节数组流和对象流的方式实现对象的深度拷贝
public Boy deepClone() throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Boy) ois.readObject();
}
}
Protobuf 序列化
Protobuf 是 Google 的一种数据交换格式,它独立于语言、独立于平台。Google 提供了多种语言来实现,比如 Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件。
- Protobuf 空间开销小、性能好,非常适合用于对性能要求高的 RPC 调用
- 由于解析性能好,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中
但是使用 Protobuf 会相对来说麻烦些,因为他有自己的语法,有自己的编译器。
Protobuf 的特点:
- 处理速度快:编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等)
- 数据体积小:采用了独特的编码方式,如 Varint、Zigzag 编码方式等等
- 兼容性高:采用 T - L - V 的数据存储方式
- 安装 IDEA 的插件 Protobuf Support
- 导入 Maven 插件
windows-x86_64 com.google.protobuf protobuf-java 3.12.0 org.xolstice.maven.plugins protobuf-maven-plugin 0.6.1 com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier} grpc-java
- 编写 proto 文件:在 main/proto 目录下创建对应的资源
// 使用的协议版本 proto3 proto2
syntax = "proto3";
// 生成的Java类的包
option java_package = "com.hesj.demo.serdemo.domain";
// 创建一个文件名 PersonModel
option java_outer_classname = "PersonModel";
// 具体需要序列化的类
message Person {
// 对于后面的数字是唯一的一个编号
string name = 1;
int32 age= 2;
}
- 使用 Maven 插件生成 Java 模型对象
数据传输方式:在 prot o中,数据的存储形式是 T - L - V 的数据存储方式:
- T:Tag 数据描述的标签头、数据类型、数据编号
- L:数据长度,可选值,对于变长的数据需要指定长度,比如字符串
- V:具体的数据值,有不同的编码方式
数据存储格式:
数据存储编码:
Varint 编码:对于整数 int 类型的数据采用 vint 进行编码,该编码的特点是:
-
对于一个 int 类型的数据,只保留有效的二进制位的数据
比如:100 --> 00000000 00000000 00000010 00001000 --> 00000010 00001000 -
对数据进行序列化:从低位到高位每次取 7 位,放入一个字节中,其中新的字节的最高位 1 表示还有数据,0 表示后面没有数据
00000010 00001000 --> 10001000 00000100二进制数据的高低位参考:二进制什么叫低位高位?
-
反序列化:10001000 00000100 --> 00000010 00001000
Tag 标签:Tag = (field_number << 3) | wire_type
Tag = (field_number << 3) | wire_type field_number: 字段的编号(唯一的) wire_type: proto内置定义好的, 对于不同的数据类型, 有不同的wire_type
message Person {
string name = 1;
int32 age = 2;
}
对于name来说, field_number = 1, wire_type = 2
则 tag_name = (0000 0001 << 3) | 0000 0010
= 0000 1000 | 0000 0010
= 0000 1010 = 9
对于age来说, field_number = 2, wire_type = 0
则 tag_age = (0000 0010 << 3) | 0000 0000
= 0001 0000 | 0000 0000
= 0001 0000 = 16
Zigzag 编码(拓展):在对负数进行编码的时候,如果是使用 vint 编码的话,对于一个负数编码,可能会占用更多的空间,比如 -2 对应的二进制 1111 1111 1111 1111 1111 1111 1111 1110
- proto 中的解决方案是:通过 Zigzag 的编码方式将 -2 转换为对应的正整数存储,再去进行 vint 编码
- 转换规则:略…
数据格式:
message Person {
string name = 1;
int32 age = 2;
}
# 对数据进行编码存储 name字段是字符串, 数据格式是 Tag + Length + Value - Tag: (1 << 3) | 2 ==> 10 ==> 0000 1010, 1个字节 - Length: utf编码, 4个字节 ==> 0000 0100, 1个字节 - Value: hesj ==> 104 101 115 106 ==> 01101000 01100101 01110011 01101010, 4个字节 Tag + Length + Value 共6字节 age字段是 int32, 数据格式是 Tag + Value - Tag: (2<< 3) | 0 ==> 16 ==> 00010000, 1个字节 - Value: 520, 采用vint编码, 10001000 00000100, 2个字节 Tag + Value 共3字节 # 因此传输Person序列化后所占的字节是9字节序列化技术选型
常见的序列化方式:https://github.com/eishay/jvm-serializers/wiki
选取原则:
- 序列化的数据大小
- 序列化的处理速度
- 是否支持跨平台,跨语言
- 可扩展性和兼容性
- 技术的流行度
- 学习的难度
create ser deser total size dfl colfer 49 248 396 643 238 148 protostuff 68 433 634 1067 239 150 minified-json/dsl-platform 46 432 684 1116 353 197 fst-flat-pre 53 501 675 1175 251 165 json/dsl-platform 44 526 806 1332 485 261 json-array/fastjson/databind 53 650 696 1345 281 163 kryo-flat-pre 54 597 758 1355 212 132 smile-col/jackson/databind 54 723 1082 1805 252 165 msgpack/databind 54 796 1052 1848 233 146 cbor-col/jackson/databind 54 732 1147 1879 251 165 protobuf 121 1173 719 1891 239 149 smile/jacksonafterburner/databind 54 913 1175 2088 352 252 thrift-compact 97 1280 808 2088 240 148 cbor/jackson+afterburner/databind 53 888 1239 2126 397 246 flatbuffers 56 1417 758 2175 432 226 thrift 95 1455 731 2186 349 197 json-col/jackson/databind 53 887 1329 2216 293 178 json/fastjson/databind 53 1058 1241 2299 486 262 smile/jackson/databind 53 1011 1300 2311 338 241 scala/sbinary 442 1311 1069 2381 255 147 capnproto 55 1574 979 2553 400 204 json/jackson+afterburner/databind 52 1094 1489 2584 485 261 cbor/jackson/databind 53 1023 1561 2585 397 246 json/protostuff-runtime 54 1353 1632 2986 469 243 json/jackson/databind 54 1164 1866 3030 485 261 json/jackson-jr/databind 53 1426 1962 3389 468 255 xml/jackson/databind 54 2639 4720 7359 683 286 json/gson/databind 56 4667 4403 9070 486 259 bson/jackson/databind 54 4105 5449 9554 506 286 xml/xstream+c 52 4383 9434 13817 487 244 json/javax-tree/glassfish 1249 6818 10284 17102 485 263 xml/exi-manual 54 11375 9891 21266 337 327 java-built-in 53 5046 23279 28325 889 514 scala/java-built-in 514 8280 36105 44385 1293 698 json/protobuf 123 6630 56787 63417 488 253 json/json-lib/databind 61 19853 71969 91822 485 263远程过程调用 RMI RPC
RPC (Remote Procedure Call) 远程过程调用,简单的理解是一个服务请求另一个服务提供的服务
本地过程调用:就是在同一个 JVM 中,直接调用本地的方法
常见的 RPC 框架:
- RMI (JRMP):纯 Java 的 RPC 框架
- SOAP (webservice)
- gRPC
- Dubbo
- SpringCloud
为什么需要 RPC 远程调用:
业务流程:
rmi-api:
- 代码:
public interface IHello extends Remote {
public String hello(String name) throws RemoteException;
}
rmi-server:
- pom.xml:
com.hesj.demo rmi-api 1.0-SNAPSHOT
- 代码:
public class HelloImpl extends UnicastRemoteObject implements IHello {
public HelloImpl() throws RemoteException {
super();
}
public String hello(String name) throws RemoteException {
System.out.println("name = " + name);
return "你好:" + name;
}
}
public class AppServer {
public static void main(String[] args) throws Exception {
// 发布Hello的远程调用
IHello hello = new HelloImpl();
// 创建一个注册监听, 使用远程调用方法
LocateRegistry.createRegistry(8888);
// 注册方法, 类似注册中心
Naming.bind("rmi://127.0.0.1:8888/hello", hello);
System.out.println("服务AppServer启动成功");
}
}
rmi-client:
- pom.xml
com.hesj.demo rmi-api 1.0-SNAPSHOT
- 代码:
public class AppClient {
public static void main(String[] args) throws Exception {
//获取远程调用的代理对象
IHello hello = (IHello) Naming.lookup("rmi://192.168.48.1:8888/hello");
// 调用远程方法
String result = hello.hello("hesj");
System.out.println("result = " + result);
}
}
Java RMI 流程分析
- 启动服务端程序,暴露对应的端口和名称服务
- 启动客户端程序,通过对应的名称服务找到对应的代理对象
- 通过代理对象调用远程方法
- 对于调用参数和返回结果需要进行序列化操作
RMI 调用时序图:
RMI 类图 — 对象发布:
RMI 类图 — 远程调用:
暂略…



