跳到主要内容
版本:1.0.0

对象复制

本文介绍如何使用 Fory#copy(Object) 在内存中复制 Java 对象图。

Fory.copy 是面向 Java 对象图的深拷贝操作。它不会先序列化为字节,而是使用同一套运行时类型系统和序列化器,在内存中创建复制后的对象图。

何时使用对象复制

当你希望为已有 Java 对象图创建一个分离的内存克隆时,可以使用对象复制。

典型使用场景包括:

  • 在修改前克隆请求或响应模型
  • 为乐观更新复制缓存状态
  • 复制包含集合、映射、数组或嵌套 bean 的对象图
  • 在克隆过程中保留共享引用和循环引用

当你需要用于传输、存储或跨进程交换的字节时,应改用序列化。

操作Fory.copyserialize / deserialize
结果Java 对象图二进制载荷及重建后的对象
主要用途内存深拷贝传输、持久化、互操作
复制引用选项withRefCopy(...)withRefTracking(...)
跨语言载荷是,在 xlang 模式下
中间字节缓冲区

快速开始

对于通用对象图,启用 withRefCopy(true),以便正确处理共享引用和循环:

import org.apache.fory.Fory;

public class Example {
public static void main(String[] args) {
Fory fory = Fory.builder()
.withXlang(false)
.withRefCopy(true)
.build();

Order original = new Order();
Order copied = fory.copy(original);
}
}

copy(null) 返回 null

引用语义

最重要的复制选项是 ForyBuilder#withRefCopy(boolean)

withRefCopy(true)

这是通用对象图的安全默认值。共享引用在复制后的对象图中仍保持共享,循环引用也可以被正确复制。

import org.apache.fory.Fory;

public class Example {
static final class Address {
String city;
}

static final class Pair {
Address left;
Address right;
}

public static void main(String[] args) {
Fory fory = Fory.builder()
.withXlang(false)
.withRefCopy(true)
.build();

Address address = new Address();
address.city = "Shanghai";

Pair pair = new Pair();
pair.left = address;
pair.right = address;

Pair copied = fory.copy(pair);
System.out.println(copied.left == copied.right); // true
}
}

withRefCopy(false)

只有在你确定对象图是树形结构,并且不依赖共享引用或循环引用时,才应禁用复制引用跟踪。这样可能更快,但重复引用会被复制为不同对象。

import org.apache.fory.Fory;

public class Example {
static final class Address {
String city;
}

static final class Pair {
Address left;
Address right;
}

public static void main(String[] args) {
Fory fory = Fory.builder()
.withXlang(false)
.withRefCopy(false)
.build();

Address address = new Address();
Pair pair = new Pair();
pair.left = address;
pair.right = address;

Pair copied = fory.copy(pair);
System.out.println(copied.left == copied.right); // false
}
}

如果禁用 withRefCopy,而对象图中包含循环,复制可能会因栈溢出而失败。

withRefCopywithRefTracking

这两个选项控制不同的操作:

  • withRefCopy(true) 影响 Fory.copy(...)
  • withRefTracking(true) 影响序列化和反序列化

启用其中一个不会自动启用另一个。如果应用既会序列化又会复制带有共享引用或循环引用的对象图,请显式配置这两个选项。

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

不可变值与可变值

对于不可变值,Fory 可能复用原始实例。对于可变值,它会创建新的对象图。

实践中,这意味着:

  • String、装箱基本类型、枚举以及许多不可变的 JDK 值类型可能会原样返回
  • 基本类型数组、字符串数组、集合、映射、bean、日期以及其他可变结构会被复制为不同对象

不要只根据对象身份判断复制是否成功。应依据待复制值的可变性约定来判断。

类注册

如果要求类注册,请在调用 copy 前注册要复制的类。

import org.apache.fory.Fory;

public class Example {
public static void main(String[] args) {
Fory fory = Fory.builder().withXlang(false)
.requireClassRegistration(true)
.withRefCopy(true)
.build();

fory.register(Order.class);
Order copied = fory.copy(new Order());
}
}

这遵循与运行时其他部分相同的注册规则:如果运行时要求类注册,复制过程中出现的运行时类型必须先完成注册。

线程安全复制

ThreadSafeFory 也支持 copy(...)

对于通用多线程用法:

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

public class Example {
public static void main(String[] args) {
ThreadSafeFory fory = Fory.builder()
.withXlang(false)
.withRefCopy(true)
.buildThreadSafeFory();

Order copied = fory.copy(new Order());
}
}

同一 API 也适用于 buildThreadLocalFory()buildThreadSafeForyPool(poolSize)

内置覆盖范围

Fory 已经为许多常见 Java 运行时类型提供复制支持,包括:

  • 基本类型值和装箱基本类型
  • 字符串和基本类型数组
  • 常见 JDK 集合和映射
  • Java time 以及日期/时间值
  • bean、record 和嵌套对象图

如果运行时已经知道如何序列化某个可变类型,该序列化器仍可能需要显式的复制实现。对于可变序列化器,默认的 Serializer.copy(...) 会抛出 UnsupportedOperationException,除非该序列化器重写了它。

使用 ForyCopyable 自定义复制

如果某个类型需要自定义复制逻辑,请实现 ForyCopyable<T>

当类本身应该控制嵌套字段的复制方式时,这是最简单的方式:

import java.util.ArrayList;
import java.util.List;
import org.apache.fory.ForyCopyable;
import org.apache.fory.context.CopyContext;

public final class Node implements ForyCopyable<Node> {
private String name;
private final List<Node> neighbors = new ArrayList<>();

@Override
public Node copy(CopyContext copyContext) {
Node copied = new Node();
copyContext.reference(this, copied);
copied.name = name;
for (Node neighbor : neighbors) {
copied.neighbors.add(copyContext.copyObject(neighbor));
}
return copied;
}
}

指导原则:

  • 如果类型可能参与循环或共享引用对象图,在创建复合可变对象后应立即调用 copyContext.reference(origin, copy)
  • 使用 copyContext.copyObject(...) 复制嵌套值,不要手动重复嵌套复制逻辑
  • 让复制逻辑与该类型的正常运行时语义保持一致

在序列化器中自定义复制

当某个类型已经使用自定义序列化器时,请为可变值重写 Serializer.copy(...)

import org.apache.fory.config.Config;
import org.apache.fory.context.CopyContext;
import org.apache.fory.context.ReadContext;
import org.apache.fory.context.WriteContext;
import org.apache.fory.serializer.Serializer;

public final class EnvelopeSerializer extends Serializer<Envelope> {
public EnvelopeSerializer(Config config) {
super(config, Envelope.class);
}

@Override
public Envelope copy(CopyContext copyContext, Envelope value) {
Envelope copied = new Envelope();
copyContext.reference(value, copied);
copied.header = copyContext.copyObject(value.header);
copied.payload = copyContext.copyObject(value.payload);
return copied;
}

@Override
public void write(WriteContext writeContext, Envelope value) {
throw new UnsupportedOperationException("omitted");
}

@Override
public Envelope read(ReadContext readContext) {
throw new UnsupportedOperationException("omitted");
}
}

当复制行为应归属于序列化器而不是领域类时,使用这种方式。

最佳实践

  • 复用 ForyThreadSafeFory 实例,不要为每次复制重新构建
  • 除非你确定对象图无环且不依赖共享引用,否则启用 withRefCopy(true)
  • withRefCopy(false) 视为面向树形数据的性能优化,而不是默认配置
  • 使用共享引用和循环对象图同时测试自定义复制实现
  • 让可变自定义序列化器的复制路径保持显式,不要依赖回退行为

故障排查

循环对象图上的栈溢出或复制失败

如果复制循环对象图失败,请启用 withRefCopy(true)

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

禁用复制引用跟踪只对无环对象图安全。

共享引用未被保留

如果同一个源对象被复制成多个不同的目标对象,说明 withRefCopy 被禁用了。请启用它:

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

单独设置 withRefTracking(true) 不会改变 Fory.copy(...) 的行为。

Copy for ... is not supported

这表示该类型的可变序列化器没有实现 copy(...)

可以通过以下方式修复:

  • 在类上实现 ForyCopyable<T>,或
  • 在已注册的序列化器中重写 Serializer.copy(CopyContext, T)

注册错误

如果运行时使用 requireClassRegistration(true),请确保复制过程中出现的运行时类型已在调用 copy(...) 前注册。

相关主题