基于 Java 提供的对象输入/输出流 ObjectInputStreamObjectOutputStream, 可以直接把 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 使用是否方便;
  • 使用者需要手工开发的工作量和难度.

sc_ik
127 声望8 粉丝

我用时间换到了什么