1.引子
我们在开发一些网络应用,客户端服务器模式应用场景,需要考虑数据如何在网络中进行传递;应用微服务化以后,需要考虑请求报文、响应报文如何在服务之间传递。
像这种跨进程的相互协作,那么客户端与服务器之间,服务与服务之间需要有一套彼此都能够理解的协议。比如说我们从小要学习普通话,只要大家都讲普通话,那么不管你来自东南西北哪个省,相互交流起来畅通无阻,这里的普通话就是协议。
有了协议,自然还需要考虑发起请求一方如何把方言,转换成普通话,这就是编码;接收请求一方如何把普通话,转换成方言,这就是解码。你看编解码我们也就很好理解了。
对于计算机来说,它老人家只认识0和1组成的二进制数据。于是我们在开发应用的时候,需要将编程语言表达的对象,转换成二进制数据,这就是序列化;将二进制数据,再转换回对象,这就是反序列化。你看序列化反序列化理解起来也不难。
业界常见的编解码框架有protobuf、thrift等,今天这篇文章我们不讨论protobuf、thrift框架,我想给你分享的是
- java编程语言,有提供序列化机制吗?
- 为什么jdk明明提供了序列化机制(实现Serializable接口),我们却不推荐使用它?
- 实际应用中,我们都是基于什么考虑,来选择编解码框架的?
2.案例
2.1.如何衡量选择编解码框架
在引子部分我们知道了,编解码,与序列化反序列化本质上讲的是一个事情。并且留下了3个问题
- java编程语言,有提供序列化机制吗?
- 为什么jdk明明提供了序列化机制(实现Serializable接口),我们却不推荐使用它?
- 实际应用中,我们都是基于什么考虑,来选择编解码框架的?
其实3个问题,归纳起来本质上是一个问题。我们一起来尝试找到答案。首先毫无疑问,jdk有提供序列化机制,java.io.Serializable接口我们都难以忘怀。在需要将对象持久化到文件,或者网络上进行传输,都会情不自禁的让目标对象,实现Serializable接口
来看第二个问题,既然jdk提供了序列化机制,为什么不推荐使用它呢?要回答这个问题,我们可以通过回答第三个问题,从而一并得到第二个问题的答案。
第三个问题是:实际应用中,如何衡量选择合适的编解码框架?我们需要考虑这么几个点
- 跨语言,既然是跨进程的应用,服务与服务之间完全有可能采用不同的编程语言实现
- 编码码流精简,要在网络上进行传输,编码以后的码流要精简(越小越好),节省带宽
- 高性能,要求编码解码速度要够快
对于跨语言、编码码流精简、编解码性能,jdk序列化机制都不能很好的支持,这就是为什么我们说不推荐使用jdk序列化机制的原因和理由。下面我们通过一个实际的案例,来进一步验证
2.2.jdk序列化机制有什么问题
我将通过通用的二进制机制,将对象编码为字节数组;与通过jdk序列化机制,将对象编码为字节数组。进行二者在
- 码流大小
- 编码性能
方面的对比,从而验证我们说jdk序列化机制存在的问题。案例代码非常简单,我直接贴出相关的代码,你一看应该就能明白了。其中你需要重点关注codeC() 、jdkCodeC()方法的实现
2.2.1.编码案例实现代码
/**
* java序列化缺点分析
* 1.不能跨语言
* 2.序列化后码流太大
* 3.序列化性能低
* @author ThinkPad
* @version 1.0
* @date 2021/4/4 12:07
*/
public class User implements java.io.Serializable{
private int userId;
private String userName;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
//==================================================
/**
* 通用二进制机制,将User对象转换成字节数组
* @return
*/
public byte[] codeC(){
// 创建字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// id序列化
buffer.putInt(this.userId);
// 名称序列化
byte[] nameBytes = this.userName.getBytes();
buffer.putInt(nameBytes.length);
buffer.put(nameBytes);
buffer.flip();
// 将buffer转换成字节数组
byte[] result = new byte[buffer.remaining()];
buffer.get(result);
return result;
}
/**
* jdk序列化机制,将User对象转换成字节数组
* @return
*/
public byte[] jdkCodeC(){
byte[] result = null;
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
try {
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
// jdk序列化
oos.writeObject(this);
oos.flush();
// 获取序列化后的字节数组
result = bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
oos.close();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
}
2.2.2.测试代码
2.2.2.1.码流大小测试
public static void main(String[] args) {
// 1.创建用户对象
User user = new User();
user.setUserId(1);
user.setUserName("Hello World!");
// 2.通用二级制机制,将User对象转换字节数组
byte[] codeC = user.codeC();
log.info("通用二级制机制,将User对象转换字节数组,数组长度:{}",codeC.length);
log.info("---------------------华丽丽分割线----------------------");
// 3.jdk对象序列化机制,将User对象转换成字节数组
byte[] jdkCodeC = user.jdkCodeC();
log.info("jdk对象序列化机制,将User对象转换成字节数组,数组长度:{}", jdkCodeC.length);
}
通用二级制机制,将User对象转换字节数组,数组长度:20
----------------------华丽丽分割线----------------------
jdk对象序列化机制,将User对象转换成字节数组,数组长度:111
2.2.2.2.编码性能测试
public static void main(String[] args) {
// 1.创建用户对象
User user = new User();
user.setUserId(1);
user.setUserName("Hello World!");
// 性能测试对比
int loop = 1000000;
long start = System.currentTimeMillis();
for(int i=0; i<loop; i++){
user.codeC();
}
long end = System.currentTimeMillis();
log.info("通用二进制机制,执行{}次数,共耗时{}毫秒",loop,(end - start));
log.info("---------------------华丽丽分割线----------------------");
long start1 = System.currentTimeMillis();
for(int i=0; i<loop; i++){
user.jdkCodeC();
}
long end1 = System.currentTimeMillis();
log.info("jdk对象序列化机制,执行{}次数,共耗时{}毫秒",loop,(end1 - start1));
}
通用二进制机制,执行1000000次数,共耗时287毫秒
---------------------华丽丽分割线----------------------
jdk对象序列化机制,执行1000000次数,共耗时1372毫秒