Skip to content

Latest commit

 

History

History
391 lines (304 loc) · 12.5 KB

02.二进制序列化.md

File metadata and controls

391 lines (304 loc) · 12.5 KB
title date order categories tags permalink
Java 二进制序列化
2022-02-17 14:34:30 -0800
2
Java
工具
IO
Java
IO
序列化
二进制
/pages/08d872/

Java 二进制序列化

简介

为什么需要二进制序列化库

原因很简单,就是 Java 默认的序列化机制(ObjectInputStreamObjectOutputStream)具有很多缺点。

不了解 Java 默认的序列化机制,可以参考:Java 序列化

Java 自身的序列化方式具有以下缺点:

  • 无法跨语言使用。这点最为致命,对于很多需要跨语言通信的异构系统来说,不能跨语言序列化,即意味着完全无法通信(彼此数据不能识别,当然无法交互了)。
  • 序列化的性能不高。序列化后的数据体积较大,这大大影响存储和传输的效率。
  • 序列化一定需要实现 Serializable 接口。
  • 需要关注 serialVersionUID

引入二进制序列化库就是为了解决这些问题,这在 RPC 应用中尤为常见。

主流序列化库简介

Protobuf

Protobuf 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储 格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。Protobuf 使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL 编译器,生成序列化工具类。

优点:

  • 序列化后体积相比 JSON、Hessian 小很多
  • 序列化反序列化速度很快,不需要通过反射获取类型
  • 语言和平台无关(基于 IDL),IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器
  • 消息格式升级和兼容性不错,可以做到后向兼容
  • 支持 Java, C++, Python 三种语言

缺点:

  • Protobuf 对于具有反射和动态能力的语言来说,用起来很费劲。

Thrift

Thrift 是 apache 开源项目,是一个点对点的 RPC 实现。

它具有以下特性:

  • 支持多种语言(目前支持 28 种语言,如:C++、go、Java、Php、Python、Ruby 等等)。
  • 使用了组建大型数据交换及存储工具,对于大型系统中的内部数据传输,相对于 Json 和 xml 在性能上和传输大小上都有明显的优势。
  • 支持三种比较典型的编码方式(通用二进制编码,压缩二进制编码,优化的可选字段压缩编解码)。

Hessian

Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协 议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节 数也更小。

RPC 框架 Dubbo 就支持 Thrift 和 Hession。

它具有以下特性:

  • 支持多种语言。如:Java、Python、C++、C#、PHP、Ruby 等。
  • 相对其他二进制序列化库较慢。

Hessian 本身也有问题,官方版本对 Java 里面一些常见对象的类型不支持:

  • Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展 CollectionDeserializer 类修复;
  • Locale 类,可以通过扩展 ContextSerializerFactory 类修复;
  • Byte/Short 反序列化的时候变成 Integer。

Kryo

Kryo 是用于 Java 的快速高效的二进制对象图序列化框架。Kryo 还可以执行自动的深拷贝和浅拷贝。 这是从对象到对象的直接复制,而不是从对象到字节的复制。

它具有以下特性:

  • 速度快,序列化体积小
  • 官方不支持 Java 以外的其他语言

FST

FST 是一个 Java 实现二进制序列化库。

它具有以下特性:

  • 近乎于 100% 兼容 JDK 序列化,且比 JDK 原序列化方式快 10 倍
  • 2.17 开始与 Android 兼容
  • (可选)2.29 开始支持将任何可序列化的对象图编码/解码为 JSON(包括共享引用)

小结

了解了以上这些常见的二进制序列化库的特性。在技术选型时,我们就可以做到有的放矢。

(1)选型参考依据

对于二进制序列化库,我们的选型考量一般有以下几点:

  • 是否支持跨语言
    • 根据业务实际需求来决定。一般来说,支持跨语言,为了兼容,使用复杂度上一般会更高一些。
  • 序列化、反序列化的性能
  • 类库是否轻量化,API 是否简单易懂

(2)选型建议

  • 如果需要跨语言通信,那么可以考虑:Protobuf、Thrift、Hession。

    • thriftprotobuf - 适用于对性能敏感,对开发体验要求不高的内部系统。
    • hessian - 适用于对开发体验敏感,性能有要求的内外部系统。
  • 如果不需要跨语言通信,可以考虑:KryoFST,性能不错,且 API 十分简单。

FST 应用

引入依赖

<dependency>
 <groupId>de.ruedigermoeller</groupId>
 <artifactId>fst</artifactId>
 <version>2.56</version>
</dependency>

FST API

示例:

import org.nustaq.serialization.FSTConfiguration;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class FstDemo {

 private static FSTConfiguration DEFAULT_CONFIG = FSTConfiguration.createDefaultConfiguration();

 /**
  * 将对象序列化为 byte 数组
  *
  * @param obj 任意对象
  * @param <T> 对象的类型
  * @return 序列化后的 byte 数组
  */
 public static <T> byte[] writeToBytes(T obj) {
  return DEFAULT_CONFIG.asByteArray(obj);
 }

 /**
  * 将对象序列化为 byte 数组后,再使用 Base64 编码
  *
  * @param obj 任意对象
  * @param <T> 对象的类型
  * @return 序列化后的字符串
  */
 public static <T> String writeToString(T obj) {
  byte[] bytes = writeToBytes(obj);
  return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8);
 }

 /**
  * 将 byte 数组反序列化为原对象
  *
  * @param bytes {@link #writeToBytes} 方法序列化后的 byte 数组
  * @param clazz 原对象的类型
  * @param <T>   原对象的类型
  * @return 原对象
  */
 public static <T> T readFromBytes(byte[] bytes, Class<T> clazz) throws IOException {
  Object obj = DEFAULT_CONFIG.asObject(bytes);
  if (clazz.isInstance(obj)) {
   return (T) obj;
  } else {
   throw new IOException("derialize failed");
  }
 }

 /**
  * 将字符串反序列化为原对象,先使用 Base64 解码
  *
  * @param str   {@link #writeToString} 方法序列化后的字符串
  * @param clazz 原对象的类型
  * @param <T>   原对象的类型
  * @return 原对象
  */
 public static <T> T readFromString(String str, Class<T> clazz) throws IOException {
  byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
  return readFromBytes(Base64.getDecoder().decode(bytes), clazz);
 }

}

测试:

long begin = System.currentTimeMillis();
for (int i = 0; i < BATCH_SIZE; i++) {
    TestBean oldBean = BeanUtils.initJdk8Bean();
    byte[] bytes = FstDemo.writeToBytes(oldBean);
    TestBean newBean = FstDemo.readFromBytes(bytes, TestBean.class);
}
long end = System.currentTimeMillis();
System.out.printf("FST 序列化/反序列化耗时:%s", (end - begin));

Kryo 应用

引入依赖

<dependency>
  <groupId>com.esotericsoftware</groupId>
  <artifactId>kryo</artifactId>
  <version>5.0.0-RC4</version>
</dependency>

Kryo API

示例:

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy;
import org.objenesis.strategy.StdInstantiatorStrategy;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class KryoDemo {

 // 每个线程的 Kryo 实例
 private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> {
  Kryo kryo = new Kryo();

  /**
   * 不要轻易改变这里的配置!更改之后,序列化的格式就会发生变化,
   * 上线的同时就必须清除 Redis 里的所有缓存,
   * 否则那些缓存再回来反序列化的时候,就会报错
   */
  //支持对象循环引用(否则会栈溢出)
  kryo.setReferences(true); //默认值就是 true,添加此行的目的是为了提醒维护者,不要改变这个配置

  //不强制要求注册类(注册行为无法保证多个 JVM 内同一个类的注册编号相同;而且业务系统中大量的 Class 也难以一一注册)
  kryo.setRegistrationRequired(false); //默认值就是 false,添加此行的目的是为了提醒维护者,不要改变这个配置

  //Fix the NPE bug when deserializing Collections.
  ((DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy())
   .setFallbackInstantiatorStrategy(new StdInstantiatorStrategy());

  return kryo;
 });

 /**
  * 获得当前线程的 Kryo 实例
  *
  * @return 当前线程的 Kryo 实例
  */
 public static Kryo getInstance() {
  return kryoLocal.get();
 }

 /**
  * 将对象序列化为 byte 数组
  *
  * @param obj 任意对象
  * @param <T> 对象的类型
  * @return 序列化后的 byte 数组
  */
 public static <T> byte[] writeToBytes(T obj) {
  ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
  Output output = new Output(byteArrayOutputStream);

  Kryo kryo = getInstance();
  kryo.writeObject(output, obj);
  output.flush();

  return byteArrayOutputStream.toByteArray();
 }

 /**
  * 将对象序列化为 byte 数组后,再使用 Base64 编码
  *
  * @param obj 任意对象
  * @param <T> 对象的类型
  * @return 序列化后的字符串
  */
 public static <T> String writeToString(T obj) {
  byte[] bytes = writeToBytes(obj);
  return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8);
 }

 /**
  * 将 byte 数组反序列化为原对象
  *
  * @param bytes {@link #writeToBytes} 方法序列化后的 byte 数组
  * @param clazz 原对象的类型
  * @param <T>   原对象的类型
  * @return 原对象
  */
 @SuppressWarnings("unchecked")
 public static <T> T readFromBytes(byte[] bytes, Class<T> clazz) {
  ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
  Input input = new Input(byteArrayInputStream);

  Kryo kryo = getInstance();
  return (T) kryo.readObject(input, clazz);
 }

 /**
  * 将字符串反序列化为原对象,先使用 Base64 解码
  *
  * @param str   {@link #writeToString} 方法序列化后的字符串
  * @param clazz 原对象的类型
  * @param <T>   原对象的类型
  * @return 原对象
  */
 public static <T> T readFromString(String str, Class<T> clazz) {
  byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
  return readFromBytes(Base64.getDecoder().decode(bytes), clazz);
 }

}

测试:

long begin = System.currentTimeMillis();
for (int i = 0; i < BATCH_SIZE; i++) {
    TestBean oldBean = BeanUtils.initJdk8Bean();
    byte[] bytes = KryoDemo.writeToBytes(oldBean);
    TestBean newBean = KryoDemo.readFromBytes(bytes, TestBean.class);
}
long end = System.currentTimeMillis();
System.out.printf("Kryo 序列化/反序列化耗时:%s", (end - begin));

Hessian 应用

Student student = new Student();
student.setNo(101);
student.setName("HESSIAN");
//把student对象转化为byte数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(bos);
output.writeObject(student);
output.flushBuffer();
byte[] data = bos.toByteArray();
bos.close();
//把刚才序列化出来的byte数组转化为student对象
ByteArrayInputStream bis = new ByteArrayInputStream(data);
Hessian2Input input = new Hessian2Input(bis);
Student deStudent = (Student) input.readObject();
input.close();
System.out.println(deStudent);

参考资料