1. 简介

本文将探讨Java中的FlatBuffers库,并演示如何使用它进行序列化和反序列化操作。

2. Java中的序列化

序列化是将Java对象转换为字节流的过程,便于网络传输或文件持久化。Java通过java.io.Serializable接口和java.io.ObjectOutputStream/java.io.ObjectInputStream类提供了内置的序列化机制。

然而,由于其存在处理复杂对象图和依赖类时的繁琐性等缺点,Java社区涌现出多种序列化库。常用库包括JacksonGson 较新的序列化标准是Protocol Buffers,这是Google开发的跨语言二进制序列化格式,广泛应用于高性能环境和分布式系统。

3. FlatBuffers

FlatBuffers是Google开发的高效跨平台序列化库,支持C、C++、Java、Kotlin和Go等多种语言。最初为游戏开发设计,因此性能和低内存开销是其核心设计原则。

FlatBuffers与Protocol Buffers同为Google出品,都是基于二进制的数据格式。两者都支持高效的高速序列化/反序列化,主要区别在于:FlatBuffers无需解包到中间数据结构即可直接访问数据。

3.1. FlatBuffers库简介

完整的FlatBuffers实现包含以下组件:

  • FlatBuffer模式文件
  • flatc编译器
  • 序列化/反序列化代码

模式文件作为数据模型结构的模板,语法类似C语言或IDL格式。需先定义模式文件,再用flatc编译器生成代码。

3.2. 表与模式

FlatBuffer是包含嵌套对象(如结构体、表和向量)的二进制缓冲区,通过偏移量组织数据。

这种设计允许像传统指针结构一样原地遍历数据,但严格遵循对齐和字节序规则(始终为小端序),确保跨平台兼容性。表对象还支持向前/向后兼容。

表是FlatBuffers中最基础的数据结构,用于表示带命名字段的复杂对象,类似其他语言中的类或结构体,支持int、short、string、struct、vector甚至其他表等多种字段类型。

3.3. Flatc编译器

flatc编译器是FlatBuffers的核心工具,可根据模式文件生成C++、Java等语言的序列化/反序列化代码。编译器接收模式定义,输出目标语言的代码。

后续章节将使用该编译器生成代码,首先需要构建编译器:

$ git clone https://github.com/google/flatbuffers.git

克隆后使用cmake构建:

$ cd flatbuffers
$ mkdir build
$ cd build
$ cmake ..

验证安装成功:

$ ./flatc --version
flatc version 23.5.26

编译文件位于/flatbuffers/build目录,flatc可执行文件也在其中。建议创建快捷方式方便使用。

4. 使用FlatBuffers

本节通过游戏地形案例演示FlatBuffers的使用。假设我们开发包含海洋、山地、平原等不同地形的游戏,每种地形有独特属性。地形数据需高效序列化后传输给玩家。

4.1. 模式定义

首先定义地形模式:地形在FlatBuffer中表现为表,包含名称(海洋/陆地/山地等)、颜色和3D坐标位置。地形可附加效果(如沙漠的沙暴、陆地的洪水),效果可定义为独立表

模式文件terrain.fbs如下:

namespace MyGame.baeldung;
enum Color:byte { Brown = 0, Red = 1, Green = 2, Blue = 3, White = 4 }
struct Vec3 {
  x:float;
  y:float;
  z:float;
}
table Terrain {
  pos:Vec3; // Struct.
  name:string;
  color:Color = Blue;
  navigation: string;
  effects: [Effect]
}

table Effect {
  name:string;
  damage:short;
}

root_type Terrain;

包含颜色枚举、坐标结构体和两个表(Terrain为根类型)。

4.2. 模式编译

使用flatc编译模式文件:

$ cd <path to schema>
$ flatc --java terrain.fbs

⚠️ 注意:flatc路径因安装位置而异。

4.3. 创建对象与序列化

模式编译完成后,创建沙漠地形及其效果。添加Maven依赖:

<dependency>
    <groupId>com.google.flatbuffers</groupId>
    <artifactId>flatbuffers-java</artifactId>
    <version>23.5.26</version>
</dependency>

导入生成代码:

import MyGame.terrains.*;
import com.google.flatbuffers.FlatBufferBuilder;

编译生成的Effect类提供createEffect()方法。初始化1024字节的构建器:

FlatBufferBuilder builder = new FlatBufferBuilder(INITIAL_BUFFER);

int sandstormOffset = builder.createString("sandstorm");
short damage = 3;
int sandStorm = MyGame.terrains.Effect.createEffect(builder, sandstormOffset, damage);

创建沙漠地形:

byte color = MyGame.terrains.Color.YELLOW;
int terrainName = builder.createString("Desert");
int navigationName = builder.createString("south");

使用Terrain类的静态方法添加元数据和效果:

int effectOffset = MyGame.terrains.Terrain.createEffectsVector(builder, effects);

startTerrain(builder);
addName(builder, terrainName);
addColor(builder, color);
addNavigation(builder, navigationName);
addEffects(builder, effectOffset);
int desert = endTerrain(builder);
builder.finish(desert);

序列化到缓冲区:

ByteBuffer buf = builder.dataBuffer();

4.4. 反序列化操作

将字节数组转为ByteBuffer

ByteBuffer buf = java.nio.ByteBuffer.wrap(buffer);

获取根对象并访问属性:

Terrain myTerrain = Terrain.getRootAsTerrain(buf)
Assert.assertEquals(terrain.name(), "Desert");
Assert.assertEquals(terrain.navigation(), "south");

每个属性都有对应的访问器,访问效果列表:

Effect effect1 = terrain.effectsVector().get(0);
Effect effect2 = terrain.effectsVector().get(2);
Assert.assertEquals(effect1.name(), "Sandstorm");
Assert.assertEquals(effect2.name(), "Drought");

4.5. 修改FlatBuffers

FlatBuffers本质是只读的,但有时需在传输前修改数据(如将沙暴伤害值从3改为10)。此时可使用原地修改功能

需用–gen-mutable参数重新编译模式:

$ ./../flatc --gen-mutable --java terrain.fbs

生成代码提供mutate()方法:

Assert.assertEquals(effect1.damage(), 3);
effect1.mutateDamage((short) 10);
Assert.assertEquals(effect1.damage(), 10);

5. JSON转换

flatc支持二进制与JSON互转。将JSON转为二进制:

flatc --binary <template file> <json file>

$ flatc --binary terrain.fbs sample_terrain.json

二进制转JSON:

flatc --json --raw-binary <template file> -- <binary file>

$ flatc --json --raw-binary terrain.fbs -- sample_terrain.bin

6. FlatBuffers的优势

使用该库的主要优势包括:

  • 零拷贝访问:数据以扁平二进制存储,无需解析/解包即可直接访问
  • 自动兼容性:数据结构变更时自动保持向前/向后兼容
  • 内存高效:仅需缓冲区大小的内存即可操作数据
  • 代码轻量:生成代码极简,仅需单个头文件依赖
  • 强类型安全:编译时即可捕获类型错误

7. 总结

本文深入探讨了FlatBuffers库的序列化/反序列化能力,通过实战代码演示了其核心功能和使用场景。对于需要高性能数据交换的系统(如游戏、分布式服务),FlatBuffers是值得考虑的方案。

完整代码示例可在GitHub获取。


原始标题:Serialization with FlatBuffers in Java