自定义编解码器
JsonValueCodec<T> 是 Fory JSON 用于处理单个完整 JSON 值的流式编解码器 SPI。它直接写入
Fory 的 String 或 UTF-8 写入器,并直接从 Fory 的 Latin-1、UTF-16 或 UTF-8 读取器读取。
它不是 JSON 抽象语法树(AST)或 JsonNode 编解码器。它负责处理包含 JSON null 在内的完整值,
但绝不处理 Map 键;JSON 对象成员名称仍由 MapKeyCodec 负责。
使用 writeString、writeChar、writeFieldName 或 writer 的转义追加方法,
以遵循实例的 escapeNonAscii 设置。直接写入原始内容会保留传入内容;
输出原始 token 的自定义编解码器应自行负责转义。见非 ASCII 转义。
如果应用编解码器在每种表示形式下都采用相同语义,请继承 AbstractJsonValueCodec<T>,
只需实现一次 JSON 结构:
import java.math.BigDecimal;
import org.apache.fory.json.codec.AbstractJsonValueCodec;
import org.apache.fory.json.reader.JsonReader;
import org.apache.fory.json.writer.JsonWriter;
public final class MoneyCodec extends AbstractJsonValueCodec<Money> {
@Override
public void write(JsonWriter writer, Money value) {
if (value == null) {
writer.writeNull();
} else {
writer.writeBigDecimal(value.amount);
}
}
@Override
public Money read(JsonReader reader) {
return reader.tryReadNullToken() ? null : new Money(reader.readBigDecimal());
}
}
final class Money {
final BigDecimal amount;
Money(BigDecimal amount) {
this.amount = amount;
}
}
AbstractJsonValueCodec 会为每次操作增加一次虚方法调用。对于性能敏感的编解码器,
或者行为依赖具体读取器或写入器的情况,请直接实现 JsonValueCodec<T>,并提供全部五个
针对特定表示形式的方法。
import org.apache.fory.json.ForyJson;
ForyJson json =
ForyJson.builder()
.registerCodec(Money.class, new MoneyCodec())
.build();
对拥有专用读取/写入操作的类型,不允许精确 registerCodec 注册或精确类工厂注册:
boolean、byte、short、int、long、float、double和char,以及各自的装箱类型String、CharSequence、Number、BigInteger、BigDecimal和UUIDLocalDate、LocalTime、LocalDateTime、Instant、Duration、ZoneOffset、ZonedDateTime、Year、YearMonth、MonthDay、Period、OffsetTime和OffsetDateTimebyte[]、String[]和long[]
该限制仅匹配精确类型,不包括应用子类,也不禁用具体使用位置上的 JsonCodec、JsonFormat 或其他语义映射。如果这些类型的字段或参数需要不同表示,请使用上述机制。
ObjectCodec 实例属于创建它的 resolver,不能直接注册。语言模块需要提供对象模型时,应使用精确的 JsonCodecFactory。
当一个工厂负责一组声明类型或参数化类型时,使用 JsonCodecFactory:
import org.apache.fory.json.JsonCodecFactory;
JsonCodecFactory factory =
(type, resolver, runtimeType) ->
type.getRawType() == Money.class ? new MoneyCodec() : null;
ForyJson json =
ForyJson.builder()
.withModule(context -> context.registerCodecFactory(factory))
.build();
声明的数组、集合和 Map 中的 Object 元素或值也会查询工厂。工厂接管 Object 时,会替换这些位置
默认的自然 JSON 值映射。
可配置工厂必须重写 factoryKey(),返回一个确定性值,覆盖所有可能改变生成编解码器类、对象模型或生成操作的选项。默认的工厂类名仅适用于无配置工厂。
仅当工厂在动态写入期间为值的实际类选择编解码器时,runtimeType 才为 true;声明的根类型和复合子类型收到的是 false。复合编解码器如果在构造后仍需此区分,必须保留该标志供后续 resolveTypes 调用使用,不能从 resolver 状态推断。
外层属性仍控制其名称、忽略方向和包含策略。如果属性被 NON_NULL 或 NON_EMPTY 省略,
就不会调用编解码器的写入操作。NON_EMPTY 直接检查 Java 空值类型,
其他类型使用编解码器的 isEmpty 方法;见自定义空值。
如果属性会被输出,或者该值是数组元素、集合元素、Map 值、Optional 值或原子引用值,
写入时编解码器会接收并负责处理 null。读取时,适用设置为 SET 时由编解码器负责 null。
属性的 onNullRead 和 Fory 容器的 onContentNullRead 配置为 SKIP 或 FAIL 时,
会在调用选定的子编解码器之前处理字面量 null。编解码器消费非 null token 后返回的 Java null 不会被过滤。
注册的实例会在并发操作之间共享,因此必须是线程安全的。
完整的自定义容器编解码器负责其内部表示,全局内容设置不会改变它。对于这样的声明容器属性,
显式内容 SET 委托给编解码器;显式 SKIP 或 FAIL 会被拒绝。完整的自定义对象编解码器
同样负责其内部属性处理。普通外围属性设置仍在调用这两种完整编解码器之前生效。
会物化复合对象图所有者的自定义编解码器,在创建每个所有者之前,必须使用应用定义的字节估算值
调用 JsonReader.reserveGraphMemory。这适用于复合应用对象、集合、Map 和引用数组。
在可能使集合或 Map 增长的修改操作之前,必须预留其引用存储空间。自定义标量或其他专用叶子表示,
例如上面的 MoneyCodec,无需预留对象图内存。完整编解码器还必须自行执行应用校验;
Fory JSON 不会使用目标类型的 JsonValidator 方法来包装完整的自定义表示。
为 JsonSubTypes 基类注册自定义编解码器,会取代该基类的子类型注解。对于列出的子类型,
两种包装器包含方式支持注册自定义编解码器,但内联属性包含方式不支持。
自定义空值
对于内置 Java 空值类型之外的非 null 值,NON_EMPTY 调用选定编解码器的
isEmpty(JsonWriter writer, T value)。默认返回 false。如果类型有空状态,可重写它:
// Inside MoneyCodec: treat a missing amount as an empty Money value.
@Override
public boolean isEmpty(JsonWriter writer, Money value) {
return value.amount == null;
}
通过属性上的 @JsonProperty(include = JsonProperty.Include.NON_EMPTY)、
类上的 @JsonInclude(JsonProperty.Include.NON_EMPTY) 或
defaultPropertyInclusion(JsonProperty.Include.NON_EMPTY) 启用省略。
该方法不能写入输出;writer 参数为动态编解码器提供当前操作的上下文。
字符串和其他 Java CharSequence、数组、Java 集合/Map 及 JDK Optional 使用直接空值检查,
不调用自定义 isEmpty。检查针对原始值,与编解码器的表示方式无关。
对于其他类型(包括 Scala Option 和集合),替换内置编解码器也会替换其空值行为。
根值、容器元素及非空包装器的内容不会被递归过滤。
使用 JsonCodec 选择编解码器
在类、record、枚举或接口上使用 @JsonCodec,可声明其默认的完整值编解码器。
位置参数形式是 value 的简写:
import org.apache.fory.json.annotation.JsonCodec;
@JsonCodec(MoneyCodec.class)
public final class Money {}
@JsonCodec(AccountCodec.class)
public interface Account {}
public final class RetailAccount implements Account {}
类型声明会通过超类和接口继承,最具体的声明优先。使用同一编解码器的无关声明彼此一致; 使用不同编解码器的无关声明会直接失败,而不是依赖反射顺序。
在字段或实际生效的普通 getter 上,value 会替换完整的属性值。实际生效的 setter 值参数、
JsonCreator 构造函数或工厂参数同样支持该注解;通过 Java 对字段、访问器和构造函数参数的
注解传播,record 组件也支持该注解:
public final class Invoice {
@JsonCodec(MoneyCodec.class)
public Money total;
private Money tax;
private Money discount;
@JsonCodec(MoneyCodec.class)
public Money getTax() {
return tax;
}
public void setDiscount(@JsonCodec(MoneyCodec.class) Money discount) {
this.discount = discount;
}
@JsonCreator
public Invoice(@JsonProperty("total") @JsonCodec(MoneyCodec.class) Money total) {
this.total = total;
}
}
如果标准容器仍应掌控处理流程,只有其直接子项需要自定义编解码器,请使用子成员配置:
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
public final class InvoiceGroup {
@JsonCodec(elementCodec = MoneyCodec.class)
public List<Money> items;
@JsonCodec(elementCodec = MoneyCodec.class)
public Money[] itemArray;
@JsonCodec(elementCodec = MoneyCodec.class)
public AtomicReferenceArray<Money> atomicItems;
@JsonCodec(contentCodec = MoneyCodec.class)
public Optional<Money> optional;
@JsonCodec(contentCodec = MoneyCodec.class)
public AtomicReference<Money> current;
@JsonCodec(keyCodec = CurrencyKeyCodec.class, valueCodec = MoneyCodec.class)
public Map<Currency, Money> byCurrency;
}
各子成员的含义如下:
| 成员 | 支持的当前值 | 由编解码器处理的直接子项 |
|---|---|---|
elementCodec | Collection<E>、E[]、AtomicReferenceArray<E> | E |
contentCodec | Optional<T>、AtomicReference<T> | T |
keyCodec | Map<K, V> | K 对应的 JSON 成员名称 |
valueCodec | Map<K, V> | 直接的 V 值 |
Kotlin 类型使用位置
Kotlin 使用相同的编解码器注册机制和 JsonCodec 注解。请将属性注解放在显式支持的使用位置,例如:
import org.apache.fory.json.annotation.JsonCodec
data class Ledger(
@field:JsonCodec(value = MoneyCodec::class)
val total: Money,
@field:JsonCodec(elementCodec = MoneyCodec::class)
val entries: List<Money>,
)
完整值编解码器负责整个 JSON 值;子编解码器则保留标准数组、集合、Optional/原子类型或 Map 的外层表示。选定应用编解码器后,Kotlin 可空性仍是该类型使用位置的契约:对于非 null JSON token,编解码器必须返回精确的声明类型,并且不得为非空位置返回 null。
无符号类型或符合条件的值类作为 Map 键时,无需注解即可使用内置成员名映射。如果键需要不同的带标签文本表示,请使用显式 keyCodec 或完整 Map 编解码器。精确的应用注册仍优先于 Kotlin 模块默认映射。
自定义 Map 键编解码器负责在声明的键与 JSON 成员名称之间转换:
import java.util.Locale;
import org.apache.fory.json.codec.MapKeyCodec;
public final class CurrencyKeyCodec implements MapKeyCodec {
@Override
public String toName(Object key) {
return ((Currency) key).name().toLowerCase(Locale.ROOT);
}
@Override
public Object fromName(String name) {
return Currency.valueOf(name.toUpperCase(Locale.ROOT));
}
}
使用已移除的类型使用形式的代码,应将编解码器移至所属声明:
// Before
List<@JsonCodec(MoneyCodec.class) Money> items;
// Now
@JsonCodec(elementCodec = MoneyCodec.class)
List<Money> items;
contentCodec 用于 Optional 或 AtomicReference,valueCodec 用于 Map 值,
而 elementCodec 用于数组或 AtomicReferenceArray 元素。
Iterable<E> 值如果不属于 Collection<E>,就不支持 elementCodec。如果完整编解码器需要负责
此类值,请使用 value。
子项配置有意只深入一层。对于 List<List<Money>>,elementCodec 处理每个完整的
List<Money>;对于 Money[][],它处理每个 Money[]。如需自定义更深层的后代,
请为完整的当前值实现编解码器,并通过 value 选择它。
value 与所有子成员互斥,因为它已经负责完整的当前值。空注解、不受支持的子成员,
或者将外层完整编解码器与子成员组合使用,都会在模型构建期间失败。配置的直接子项必须能
解析为具体类型;原始容器、直接通配符和未解析的直接类型变量均会被拒绝。
JsonAnyProperty 和 JsonAnyGetter 会将其 Map 展开到外层对象中。使用 valueCodec
配置其动态值:
@JsonAnyProperty
@JsonCodec(valueCodec = MoneyCodec.class)
public Map<String, Money> extra;
JsonAnySetter 的第一个参数是 String 属性名称。其第二个参数可以使用
@JsonCodec(value = ...),也可以使用对该参数自身结构有效的其他配置。
编解码器优先级与重复声明
Fory 按以下顺序解析每个当前值:
| 优先级 | 来源 |
|---|---|
| 1 | 当前属性或参数上的 JsonCodec |
| 2 | 精确的 registerCodec 注册 |
| 3 | 应用 Mixin 覆盖后的精确目标类型 JsonCodec |
| 4 | 继承边界上的类型 JsonCodec 声明 |
| 5 | 内置或默认 JSON 映射 |
一个逻辑属性可能从字段、getter、setter 参数、creator 参数或 record 注解传播中公开该注解。 重复配置必须完全相同;Fory 不会合并来自不同声明的局部配置。实际生效且未添加注解的覆盖方法, 会抑制继承的方法注解。
子成员只会替换对应的直接子项。未配置的 Map 同级项仍按正常优先级处理。如果精确注册或 类型声明为外层容器提供了完整编解码器,那么属性子成员将不可达,因此会被拒绝。
Map 键是 JSON 对象成员名称,使用 MapKeyCodec 而不是 JsonValueCodec。自定义键编解码器类
遵循与值编解码器相同的构造规则。null Map 键会被拒绝,解码后的键必须与声明的键类型匹配。
编解码器构造与平台支持
注解指定的编解码器类必须是公共、具体的顶层类或静态嵌套类,并拥有公共无参构造函数。构建后的 ForyJson 会在所有注解位置和并发操作之间共享同一个实例,因此该实例必须线程安全。对于允许注册的类型,如果完整值编解码器需要配置,请使用 registerCodec(Target.class, instance)。
在 GraalVM Native Image 之外,具名 Java 模块必须将编解码器包导出或开放给
org.apache.fory.json。Native Image 会在镜像构建期间准备注解编解码器的构造函数,
因此不需要此包访问权限。如果继承的类型声明编解码器用于更具体的目标,则每个解码值都必须
为 null,或者可赋值给该目标。
在 JVM、Android 和 GraalVM Native Image 上,该注解对 FIELD、METHOD 和 PARAMETER
具有相同行为。普通 Android 类可以省略 JsonType,并提供等效的精确规则。
经过 Android desugar 的 Record(包括 JsonValue Record)需要处理器生成的操作;
这些操作必须来自直接的 JsonType 声明或一对已编译的精确 JsonMixin。
GraalVM 对象模型遵循 GraalVM 指南中的构建时工作流。