跳到主要内容
版本:1.0.0

原生序列化

Java 原生序列化是通过 withXlang(false) 选择的 Java 专用编码格式。当所有写入方和读取方都是 Java/JVM 进程,并且载荷应遵循 JVM 类型系统而不是可移植的 xlang 类型系统时,请使用它。对于仅面向 Java/JVM、用于替代 JDK serialization、Kryo、FST、Hessian 或 Java-only Protocol Buffers 载荷的场景,原生序列化是合适的起点。

本页中的原生序列化指 Fory 的 xlang=false 编码模式。它不同于 GraalVM native image 支持;后者请参阅 GraalVM 支持

当字节必须由非 Java Fory 运行时读取时,请使用默认 Java 模式 Xlang 序列化

何时使用原生序列化

在以下情况下使用原生序列化:

  • 载荷只由 Java/JVM 应用产生和消费。
  • 对象模型使用 Java 特有类型、JDK 集合、包装类型、继承、接口或多态,并且不需要跨语言 Schema。
  • 现有类依赖 JDK 序列化钩子,例如 writeObjectreadObjectwriteReplacereadResolvereadObjectNoDataExternalizable
  • 你需要通过 Fory.copy(...) 进行 Java 对象复制。
  • 大型基本类型数组或二进制载荷应使用原生模式的带外缓冲区。
  • 你正在替换 Java-only 序列化框架,并希望覆盖最广泛的 Java 对象表面。

当载荷必须由 Python、C++、Go、Rust、JavaScript/TypeScript、C#、Swift、Dart 或其他非 Java 运行时读取时,请改用 xlang 序列化。

创建原生运行时

import org.apache.fory.Fory;

Fory fory = Fory.builder()
.withXlang(false)
.requireClassRegistration(true)
.withRefTracking(true)
.build();

byte[] bytes = fory.serialize(object);
Object decoded = fory.deserialize(bytes);

为每种配置创建并复用一个 ForyThreadSafeFory 实例。创建 Fory 的成本不低,因为运行时会缓存类元数据、序列化器和生成的代码。

import org.apache.fory.Fory;
import org.apache.fory.ThreadSafeFory;

ThreadSafeFory fory = Fory.builder()
.withXlang(false)
.requireClassRegistration(true)
.withRefTracking(true)
.buildThreadSafeFory();

fory.register(Order.class, 100);

在并发序列化开始前,应在启动阶段注册类和序列化器。当类加载器、注册、安全、Schema 演进或引用跟踪设置不同时,请使用单独的运行时。

Schema 演进

原生序列化默认使用 schema-consistent 模式。在 schema-consistent 模式下,写入方和读取方的类应具有相同 Schema。这是原生模式下最直接的路径,也是同步部署的合适默认值。

当 Java 类可能在写入方和读取方部署之间独立演进时,请启用兼容模式:

Fory fory = Fory.builder()
.withXlang(false)
.withCompatible(true)
.build();

兼容模式允许读取方在 Schema 元数据仍兼容时容忍字段的新增、删除或重排。它还会默认启用元数据共享。字段 ID、类版本检查、元信息共享以及未知类处理,请参阅 Schema 演进

注册与安全

类注册默认启用。对于服务边界,请保持启用并显式注册应用类:

Fory fory = Fory.builder()
.withXlang(false)
.requireClassRegistration(true)
.build();

fory.register(Order.class, 100);
fory.register(LineItem.class, 101);

显式数值 ID 可以避免注册顺序漂移。如果使用不带 ID 的 fory.register(MyClass.class),每个写入方和读取方都必须按相同顺序注册类。当类型 ID 协调更困难时,也可以使用基于名称的注册:

fory.register(Order.class, "com.example", "Order");

只有在可信环境中才应禁用类注册。如果需要动态类加载,请安装 TypeCheckerAllowListChecker,使反序列化能够拒绝非预期类:

import org.apache.fory.Fory;
import org.apache.fory.resolver.AllowListChecker;

AllowListChecker checker = new AllowListChecker(AllowListChecker.CheckLevel.STRICT);
checker.allowClass("com.example.*");

Fory fory = Fory.builder()
.withXlang(false)
.requireClassRegistration(false)
.withTypeChecker(checker)
.withMaxDepth(100)
.build();

使用 withMaxDepth(...) 限制不可信或外部提供载荷的对象图深度。完整安全配置请参阅 类型注册

Java 对象表面

原生序列化负责 Java 特有的对象表面:

  • POJO、record、枚举、基本类型数组、对象数组以及常见 JDK 集合。
  • 继承、接口、多态字段、共享引用和循环对象图。
  • 不需要映射到可移植 xlang 类型的 Java 包装类型和集合行为。
  • 需要 Java serialization 兼容性的类所使用的 JDK 序列化钩子。
  • 通过 registerSerializer(...)registerSerializerAndType(...) 注册的自定义序列化器。

对于普通应用类,Fory 可以使用生成的序列化器,并避免 JDK ObjectOutputStream 语义。需要 JDK 序列化钩子的类可能会使用 Java serialization 兼容路径;当基于钩子的路径成本过高时,应优先为热点类使用 Fory 自定义序列化器。

JDK 序列化钩子

Java 原生模式支持许多现有 Java 对象模型中的 JDK 序列化钩子:

  • writeObjectreadObject
  • writeReplacereadResolve
  • readObjectNoData
  • Externalizable
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class MyClass implements Serializable {
private void writeObject(ObjectOutputStream out) throws IOException {
// 自定义序列化逻辑。
}

private void readObject(ObjectInputStream in) throws IOException {
// 自定义反序列化逻辑。
}

private Object writeReplace() {
return this;
}

private Object readResolve() {
return this;
}
}

Fory 原生载荷不是 JDK ObjectOutputStream 载荷。这些钩子会为了 Java 对象兼容性而被遵循,但新的载荷应由 Fory 写入和读取。

从 Java 序列化框架迁移

替换 JDK serialization、Kryo、FST、Hessian 或 Java-only Protocol Buffers 管线时:

  1. .withXlang(false) 开始,因为数据仅面向 Java。
  2. 保持 requireClassRegistration(true),并使用显式 ID 注册应用类。
  3. 如果写入方和读取方部署会独立滚动,请使用 .withCompatible(true)
  4. 只有当身份或循环引用很重要时,才启用 .withRefTracking(true)
  5. 为那些否则会使用昂贵 JDK 序列化钩子的热点类添加自定义序列化器。
  6. 尽可能将旧字节流和新字节流分开。

当应用必须读取可能是 JDK ObjectOutputStream 字节或 Fory 原生模式字节的数据时,JavaSerializer.serializedByJDK 可以在回退到 Fory 前识别 JDK 载荷:

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import org.apache.fory.serializer.JavaSerializer;

if (JavaSerializer.serializedByJDK(bytes)) {
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
return objectInputStream.readObject();
}
return fory.deserialize(bytes);

只在确实接受两种格式的边界使用这个桥接方式。除此之外,原生模式 Fory 载荷应由 Fory 直接写入和读取。

对象图与引用跟踪

启用引用跟踪时,原生模式支持共享引用和循环引用:

Fory fory = Fory.builder()
.withXlang(false)
.withRefTracking(true)
.build();

只有对于身份和循环都不是数据模型组成部分的值形对象图,才应禁用引用跟踪:

Fory fory = Fory.builder()
.withXlang(false)
.withRefTracking(false)
.build();

引用跟踪是一项语义选择。关闭它可以提升性能并减小载荷大小,但重复引用会反序列化为不同对象,并且不支持循环。

对象复制

Fory 可以在不物化字节数组的情况下深拷贝 Java 对象。完整复制语义、自定义复制钩子和故障排查,请参阅 对象复制

Fory fory = Fory.builder()
.withXlang(false)
.withRefCopy(true)
.build();

MyClass copy = fory.copy(original);

withRefCopy(true) 控制复制操作中的引用保留。它不同于控制序列化和反序列化的 withRefTracking(...)

零拷贝序列化

原生模式支持将大型二进制值和基本类型数组作为带外 BufferObject 载荷:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.fory.Fory;
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.serializer.BufferObject;

Fory fory = Fory.builder()
.withXlang(false)
.build();

List<Object> value = Arrays.asList("str", new byte[1000], new int[100], new double[100]);
Collection<BufferObject> bufferObjects = new ArrayList<>();
byte[] bytes = fory.serialize(value, bufferObject -> !bufferObjects.add(bufferObject));
List<MemoryBuffer> buffers = bufferObjects.stream()
.map(BufferObject::toBuffer)
.collect(Collectors.toList());

Object decoded = fory.deserialize(bytes, buffers);

对于应以带外方式发送的缓冲区,回调返回 false。主字节数组仍包含根对象图,并按回调顺序引用这些缓冲区。

当传输层能够分别携带主载荷和缓冲区时,请使用这种方式。如果流会作为一个字节数组存储或发送,请省略回调,让 Fory 将缓冲区内容保留在带内。

原生序列化还支持字节数组、MemoryBufferByteBufferOutputStreamForyInputStreamForyReadableChannel API。选择与你已有边界匹配的 API;当缓冲区或流已经可用时,避免再通过 byte[] 复制。

类加载器

ClassLoader loader = Thread.currentThread().getContextClassLoader();

Fory fory = Fory.builder()
.withXlang(false)
.withClassLoader(loader)
.build();

每个 Fory 实例都绑定到一个类加载器,因为类元数据和序列化器会被缓存。对于每个应用、插件或租户类加载器,请构建单独的运行时,而不是在已有运行时上切换加载器。

性能指南

  • 复用 ForyThreadSafeFory 实例,不要为每个请求重新构建。

  • 使用显式数值 ID 注册类,以获得紧凑的类型元数据和稳定的部署。

  • 对同步部署的 Java 服务保持 schema-consistent 模式;只有在 Schema 演进需要时才启用兼容模式。

  • 对于没有身份或循环的值形对象图,禁用引用跟踪。

  • 当启动延迟可以接受先解释执行再序列化时,在普通 JVM 上使用异步编译:

    Fory fory = Fory.builder()
    .withXlang(false)
    .withAsyncCompilation(true)
    .build();
  • 在普通 JVM 上保持运行时代码生成启用。对于 GraalVM native image 和 Android 流程,使用静态生成的序列化器。

  • 当传输层支持拆分载荷时,对大型基本类型数组或二进制字段使用零拷贝带外缓冲区。

  • 当对象约定允许时,为热点类使用 Fory 自定义序列化器来替换昂贵的 JDK 序列化钩子。

原生与 Xlang 对比

需求使用原生序列化使用 xlang 序列化
仅 Java/JVM 的载荷可选
非 Java 读取方或写入方
广泛的 Java 对象表面仅限 xlang 类型
JDK 序列化钩子
Java 对象复制
跨运行时的可移植类型映射
默认兼容 Schema 演进
Schema 一致的同语言性能

故障排查

非 Java 运行时无法读取载荷

写入方正在使用原生序列化。请使用 .withXlang(true) 重新构建写入方,并让类型注册与每个对等运行时保持一致。

反序列化期间类被拒绝

保持类注册启用,并在写入方和读取方都注册该类。如果确实需要动态类加载,请只在配合 allow-listing TypeChecker 时使用 requireClassRegistration(false)

滚动部署在字段变更后失败

原生序列化默认使用 schema-consistent 模式。当写入方和读取方版本可能不同时,请使用 .withCompatible(true),并为长期存在的 Schema 添加稳定字段元数据。

对象身份未被保留

为序列化和反序列化启用 .withRefTracking(true)。对于 Fory.copy(...),启用 .withRefCopy(true)

迁移后的边界同时收到 JDK 和 Fory 字节

只在混合格式边界使用 JavaSerializer.serializedByJDK(...),然后将 JDK 字节路由到 ObjectInputStream,将 Fory 原生字节路由到 fory.deserialize(...)

相关主题