一、介绍
基于Java提供的对象输入/输出流ObjectInputStream
和ObjectOutputStream
,可以直接把Java对象作为可存储的字节数组,写入文件,也可以传输到网络上。对程序员来说,基于JDK默认的序列化机制,可以避免操作底层的字节数组,从而提高开发效率。
Java序列化的目的主要有两个:
- 网络传输
- 对象持久化
Netty的NIO网络开发,主要关注的是网络传输方面。
当进行跨进程服务调用时,需要把被传输的Java对象编码为字节数组或者ByteBuffer对象。当远程服务读取到ByteBuffer或者字节数组时,需要将其解码为发送时的Java对象,这被称为Java对象编解码技术。
Java序列化仅仅是Java编解码技术的一种,由于它的种种缺陷,衍生出了很多种编解码技术和框架,后续章节我们会结合Netty介绍几种业界主流的编解码技术和框架。
二、Java序列化的缺点
Java序列化从JDK1.1版本就已经提供,它不需要添加额外的类库,只需要实现 java.io.Serializable 并生成序列ID即可,因此,它从诞生之初就得到了广泛的应用。
但是在RPC(远程服务调用)时,很少直接使用Java序列化进行消息的编解码和传输。
2.1 无法跨语言
无法跨语言是Java序列化最致命的问题,对于跨进程的服务调用,服务提供者可能会使用C++或者其他语言开发,当我们需要和异构语言进行交互时,Java序列化就难以胜任。
由于Java序列化技术是Java语言内部的私有协议,其他语言并不支持,对于用户来说他完全是黑盒。对于Java序列化后的字节数组,别的语言无法进行反序列化,这就严重阻碍了它的应用。事实上,目前几乎所有流行的Java RPC 通信框架,都没有使用Java序列化作为编解码框架,原因就在于它无法跨语言,而这些RPC框架往往需要支持跨语言调用。
2.2 序列化后的码流太大
我们通过一个实例来看下Java序列化后的字节数组大小:
先创建一个Java对象:
package com.lsh.serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.nio.ByteBuffer;
/**
* @author :LiuShihao
* @date :Created in 2021/4/9 12:28 下午
* @desc :
* UserInfo是一个普通的Java对象,实现了Serializable接口,并生成了序列化ID,可以通过JDK的Java序列化机制进行序列化和反序列化
*
*/
//getset方法
@Data
//全参构造
@AllArgsConstructor
//无参构造
@NoArgsConstructor
public class UserInfo implements Serializable {
private static final long serialVersionUID = -8018991226271912056L;
private String userName;
private int userID;
/**
*
* 使用基于ByteBuffer的通用二进制编解码技术对UserInfo对象进行编码,编码结果仍然是byte数组,
* 可以与传统的JDK序列化后的码流大小进行对比
*
* Buffer的属性:
* 容量(capacity):缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变
* 上界(limit):缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数
* 位置(position):下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新
* 标记(mark):下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position =mark。标记在设定前是未定义的(undefined)。这四个属性之间总是遵循以下关系:0 <= mark <= position <= limit <= capacity
*
* @return
*/
public byte[] codeC(){
ByteBuffer buffer = ByteBuffer.allocate(1024);
byte[] value = this.getUserName().getBytes();
buffer.putInt(value.length);
//put() :相对写,向position的位置写入一个byte,并将postion+1,为下次读写作准备
buffer.put(value);
buffer.putInt(this.userID);
//Buffer有两种模式,写模式和读模式。在写模式下调用flip()之后,Buffer从写模式变成读模式。
buffer.flip();
//remaining() 返回limit和position之间相对位置差
byte[] result = new byte[buffer.remaining()];
buffer.get(result);
return result;
}
}
然后在对比两种序列化编码后的码流的大小:
package com.lsh.serializable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
/**
* @author :LiuShihao
* @date :Created in 2021/4/9 12:40 下午
* @desc :
*/
public class TestUserInfo {
/**
* 先调用两种编码接口对UserInfo对象编码,然后分别打印两者编码后的码流的大小
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
UserInfo userInfo = new UserInfo("Welcomde to Netty",100);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(bos);
os.writeObject(userInfo);
os.flush();
os.close();
byte[] b = bos.toByteArray();
System.out.println("The jdk serializable length is :"+b.length);
bos.close();
System.out.println("------------------------------------------");
System.out.println("The byte array serializable length is :"+userInfo.codeC().length);
}
}
运行结果:
测试结果发现,采用JDK序列化机制编码后的二进制数组大小竟然是二进制编码的5倍。
我们评判一个编解码框架的优劣时,往往会考虑一下几个因素:
- 是否支持跨语言,支持的语言种类是否丰富;
- 编码后码流的大小;
- 编解码的性能;
- 类库是否小巧,API使用是否方便;
- 使用者需要手工开发的工作量和难度;
在同等情况下,编码后的字节数组越大,存储的时候越占用空间,存储的硬件成本越高,并且在网络传输时更占宽带,导致系统的吞吐量降低,Java序列化后的码流偏大,也一直被行业所诟病,导致它的应用范围受到了很大限制。
2.3 序列化性能太低
对UserInfo进行改造,新增一个方法,在创建一个性能测试版本的UserInfo测试程序:
在UserInfo类中新增下面这个方法:
public byte[] codeC(ByteBuffer buffer){
buffer.clear();
byte[] value = this.userName.getBytes();
buffer.putInt(value.length);
buffer.put(value);
buffer.putInt(this.userID);
buffer.flip();
value = null;
byte[] result = new byte[buffer.remaining()];
buffer.get(result);
return result;
}
编码性能测试类:
package com.lsh.serializable;
import java.io.ByteArrayOutputStream;