基于 Java 提供的对象输入/输出流 ObjectInputStream
和 ObjectOutputStream
, 可以直接把 Java 对象作为可存储的字节数组写入文件, 也可以传输到网络上.
Java 序列化的目的主要有两个:
- 网络传输
- 对象持久化
当进行远程跨进程服务调用时, 需要把被传输的 Java 对象编码为字节数组或者 ByteBuffer
对象. 而当远程服务读取到 ByteBuffer
对象或字节数组时, 需要将其解码为发送时的 Java 对象. 这被称为 Java 对象编解码技术.
Java 序列化缺点
Java 序列化仅仅是 Java 编解码技术的一种, 由于它的种种缺陷, 衍生除了多种解码器技术和框架.
无法跨语言
对于跨进程的服务调用, 服务提供者可能会使用 C++ 或其他语言开发, 当我们需要和其他语言交互时, 由于 Java 序列化技术是 Java 语言内部的私有协议, 其他语言并不支持, 所以无法对其进行反序列化.
序列化后的码流太大
下面我们通过一个实例看下 Java 序列化后的字节数组大小.
public class UserInfo implements Serializable {
private static final long serialVersionUID = 1L;
private String userName;
private int userID;
public byte[] codeC() {
ByteBuffer buffer = ByteBuffer.allocate(1024);
byte[] value = this.userName.getBytes();
buffer.put(value);
buffer.putInt(this.userID);
buffer.flip();
value = null;
byte[] result = new byte[buffer.remaining()];
buffer.get(result);
return result;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getUserID() {
return userID;
}
public void setUserID(int userID) {
this.userID = userID;
}
}
public class App
{
public static void main( String[] args ) throws Exception {
UserInfo userInfo = new UserInfo();
userInfo.setUserID(100);
userInfo.setUserName("Welcome to Netty");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(userInfo);
objectOutputStream.flush();
objectOutputStream.close();
byte[] bytes = byteArrayOutputStream.toByteArray();
System.out.println("The jdk serializable length is: " + bytes.length);
System.out.println("The byte array serializable length is: " + userInfo.codeC().length);
}
}
测试结果
The jdk serializable length is: 102
The byte array serializable length is: 20
测试结果令人震惊, 采用 JDK 序列化机制编码后的二进制数组大小尽然是二进制编码的 5.1 倍. 在同等情况下, 编码后的字节数组越大, 存储的时候就越占空间, 存储的硬件成本就越高, 并且在网络传输时更占带宽, 导致系统的吞吐量降低.
序列化性能太低
可以让创建代码循环 100 万次, 然后在前后加入获取系统时间.
业界主流的编解码框架
Google 的 Protobuf 介绍
Protobuf 全称 Google Protocole Buffers, 它由谷歌开源而来, 在谷歌内部久经考验. 它将数据结构以 .proto
文件进行描述, 通过代码生成工具可以生成对应数据结构的 POJO 对象和 Protobuf 相关的属性和方法.
它的特点如下.
- 结构化数据存储格式(XML JSON等);
- 高效的编解码性能;
- 语言无关、平台无关、扩展性好;
- 官方支持 Java、C++ 和 Python 三种语言.
为什么不使用 xml. 尽管 xml 的可读性和可扩展性非常好, 也非常适合描述数据结构, 但是 xml 解析的时间开销和 xml 为了可读性而牺牲的空间开销都非常大, 因此不适合做高性能的通信协议. Protobuf 使用二进制编码, 在空间和性能上具有更大的优势.
Protobuf 另一个比较吸引人的地方就是它的 数据描述文件和代码生成机制, 利用数据描述文件对数据结构进行说明的优点如下.
- 文本化的数据结构描述语言, 可以实现语言和平台无关, 特别适合异构系统间的集成.
- 通过标识字段的顺序, 可以实现协议的前向兼容;
- 自动代码生成, 不需要手工编写同样数据结构的 C++ 和 Java 版本;
- 方便后续的管理和维护. 相比于代码, 结构化的文档更容易管理和维护.
总结
我们判断一个编码器框架的优劣时, 往往会考虑以下几个因素.
- 是否支持跨语言, 支持的语言种类是否丰富;
- 编码后的码流大小;
- 编解码的性能;
- 类库是否小巧, API 使用是否方便;
- 使用者需要手工开发的工作量和难度.