- 后端
- 序列化
【免费下载链接】gson
A Java serialization/deserialization library to convert Java Objects into JSON and back
导读
Gson 官方 GsonDesignDocument.md 是一份面向进阶用户与库开发者的设计文档,记录了项目在诞生初期(约 2007 年)围绕反序列化策略、异常模型、实例创建、字段识别等核心问题做出的取舍与权衡。本文以该设计文档为主线骨架,结合当前仓库中 gson/src/main/java/com/google/gson 下的实际源码实现,逐条还原这些设计决策背后的动机、当时的替代方案以及它们在现代 Gson 代码库中的落点。读完本文,你将理解 Gson 为什么"按目标类型树"反序列化、为什么把大部分类声明为final、为什么用非受检异常报告解析失败,以及GsonBuilder与InstanceCreator等机制各自解决了什么历史难题。
需要说明的是:原文档作者在开头即提示"部分信息已过时,不再反映 Gson 的当前状态,但对理解 Gson 的历史仍有价值"。本文在继承原文档全部论点的基础上,用现行源码佐证其思想脉络的延续与演进,读者请以"设计动机"而非"API 现状"的视角阅读。
反序列化:导航 Json 树还是目标类型树
设计文档的第一个核心问题:把 JSON 字符串反序列化为目标对象时,应该沿输入 JSON 的树结构走,还是沿目标类型的类型树走?
Gson 的选择是后者——按目标对象的类型树导航。这样做的收益是双重的:
- 严格按预期实例化:只实例化你期望出现的类型,本质上等于用期望类型对输入做了一次"schema 校验",类型不符的输入会在解析过程中直接暴露问题;
- 天然忽略多余字段:JSON 输入中未被期望的额外字段会被静默忽略,而不会破坏目标对象的结构。
从源码看,这一思想的当代实现分布在internal包下的多个TypeAdapterFactory中。例如 ReflectiveTypeAdapterFactory.java 的create()方法会拿到目标类型的TypeToken,检查它是否是匿名/局部类、是否受ReflectionAccessFilter限制、是否是 Java Record,然后调用constructorConstructor.get(...)获得对象构造器,并基于目标类型的字段集合getBoundFields(...)构建反射式适配器。反序列化时,只有这些"绑定字段"会与 JSON 成员一一对应,输入中多余的成员自然不会被读取。
而另一种"沿输入导航"的策略,在 Gson 中只被保留给静态类型无法确定的场景——即目标类型是Object时。见 ObjectTypeAdapter.java:它读取时按输入的实际结构把对象造为LinkedHashMap、数组造为ArrayList,叶子值则依据ToNumberStrategy(默认ToNumberPolicy.DOUBLE)转成数字。也就是说,只有当目标类型树"退化"为Object时,Gson 才退回沿输入 JSON 树导航。这正印证了文档的判断:类型树导航让库对实例化的类型保持严格控制。
原文档还提到,作为 Gson 的一部分,作者曾编写过一个通用目的ObjectNavigator,它可以遍历任意对象的字段并回调访问者(visitor)。从当前仓库看,这个早期通用组件已被更精细的TypeAdapter/TypeAdapterFactory体系取代(如 MapTypeAdapterFactory.java、ArrayTypeAdapter.java 各司其职),但其"按对象结构回调访问者"的思想正是今天各工厂遍历字段、逐个成员适配的雏形。
序列化语义为何比反序列化语义更丰富
设计文档指出了一个 Gson 的"不对称"现象:Gson 可以序列化任意(非泛型的)集合,却只能反序列化带泛型参数的集合——在某些情况下,Gson 会无法反序列化它自己写出来的 JSON。
原因在于 Java 类型系统的局限:当面对一个由任意类型元素组成的 JSON 数组时,运行时没有任何信息能推断每个元素的真实类型。以List(裸类型)为例,序列化时toJson可以借助运行时对象本身得到每个元素的getClass()并逐一写出;而反序列化时面对["abc", 123, true],若目标类型只是裸List,Gson 无法知道第一个元素该还原为String还是别的类型。
文档明确表示:他们完全可以为了对称性而把序列化也限制在泛型集合上,但选择不这么做。理由是使用库的用户往往只关心序列化或只关心反序列化中的一者,没必要为了照顾另一方向而人为削弱序列化能力。这个"宁可让序列化更强、也不人为限制"的取舍,在今天的 API 上依然可见:Gson#toJson在多数场景下可以依赖运行时类型自动工作,而fromJson遇到泛型类型则要求调用方显式传入Type或TypeToken(见 Gson.java 的 Javadoc 示例)。
支持无法修改的类:自定义序列化器与反序列化器
许多 JSON 库靠给字段或方法加注解来标记"哪些字段参与 JSON 序列化"。设计文档指出,这种做法的致命缺陷是:它天然排除了 JDK 类和第三方库中你无法修改源码的类。
Gson 的解法是定义自定义序列化器 / 反序列化器(custom serializers and deserializers)的概念。文档也坦承这并非原创:JAX-RPC 技术当年就是用同样的思路解决同一问题。这一机制在现行代码库中具体化为三个接口与一个统一的注册入口:
- JsonSerializer.java:自定义序列化回调,
serialize(T src, Type typeOfSrc, JsonSerializationContext context)返回JsonElement; - JsonDeserializer.java:自定义反序列化回调,
deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)返回目标实例; - InstanceCreator.java:在反序列化时为"没有无参构造器"的类提供临时实例;
- 注册入口 GsonBuilder.registerTypeAdapter(Type, Object):一个对象只要实现上述任一接口即可注册,注册器内部会分别写入
instanceCreators映射、生成TreeTypeAdapter工厂或TypeAdapters工厂。
以JsonSerializer的 Javadoc 示例来说:对Id(clazz, value)类,默认序列化结果是{"clazz":"com.foo.MyObject","value":20},若只想输出20,可以写一个返回new JsonPrimitive(id.getValue())的IdSerializer并通过new GsonBuilder().registerTypeAdapter(Id.class, new IdSerializer()).create()注册。这就是"类不可修改"时仍能完全控制 JSON 形状的标准姿势。
与之配套的还有 TypeAdapterFactory.java:当一批类型共享相近的 JSON 结构时,可用工厂统一产出适配器;工厂按注册顺序取用,先注册者优先,其create()中可以通过gson.getAdapter(...)委托给其他适配器以组合复合类型。设计文档描述的时代尚无TypeAdapter(它是 2.x 引入的流式 API),但"插件式自定义适配器"的架构意图一脉相承。
用非受检异常(Unchecked)报告解析错误
设计文档解释了异常模型的选择:解析失败用非受检异常(unchecked exception,即运行时异常)表示。理由很务实——客户端通常无法从坏输入中恢复,如果强制他们捕获受检异常,最终只会催生一堆空catch()块的敷衍代码。
现行代码中这一决定体现在异常继承体系上:JsonParseException.java 直接继承RuntimeException,其 Javadoc 原样复述了文档的论证:"使用 RuntimeException 可以避免客户端捕获异常却什么都不做的坏实践;解析出错时通常就是希望程序直接失败,因为客户端往往不知道如何从 JsonParseException 中恢复。" 其子类 JsonSyntaxException 用于 JSON 语法层面的错误,而流式解析底层 JsonReader 还会抛出MalformedJsonException(同样是运行时异常)表示非法 JSON 结构。完整路径:JsonParseException (RuntimeException)→JsonSyntaxException,以及流层的MalformedJsonException。这一设计让"输入不可信、直接失败"成为默认语义,调用方无需被迫处理无法恢复的失败。
反序列化时如何创建类实例
Gson 反序列化前必须先造出一个"空壳"实例,再把 JSON 数据灌进其字段。设计文档记录了一个重要的历史权衡:为什么不用 Guice 来拿实例?
- 引入 Guice 会产生不必要的依赖;
- Guice 语义是"返回一个合法可用实例",而 Gson 只需要一个哑实例(dummy instance),二者意图不符;
- 更糟的是,Gson 会用输入数据覆盖该实例的字段,从而污染后续所有 Guice 注入对该实例的引用。
因此 Gson 选择调用无参构造器创建实例,并对原始类型、枚举、集合、Set、Map 和树等类型做特殊处理。对"无法修改、又没有默认构造器"的库类型(文档举了Money类为例),Gson 提供自定义实例创建器(InstanceCreator):注册后,Gson 在需要时向它索要一个哑实例。
这一整套逻辑如今集中在 ConstructorConstructor.java 的get()方法中,其实例获取策略按优先级依次是:
- 类型精确匹配的
InstanceCreator(instanceCreators.get(type)),其次裸类型匹配; - 特殊集合构造器:
EnumSet、EnumMap等没有公共无参构造器的 JDK 类型,见 newSpecialCollectionConstructor; - 默认无参构造器:通过反射调用,期间受
ReflectionAccessFilter约束,见 newDefaultConstructor; - 默认接口实现:为
List接口族选ArrayList、LinkedHashSet、TreeSet、ArrayDeque,为Map接口族选LinkedHashMap、TreeMap、ConcurrentHashMap、ConcurrentSkipListMap,见 newDefaultImplementationConstructor; - JDK Unsafe 分配:绕过构造器直接分配内存(受
useJdkUnsafe开关与访问过滤器控制),见 newUnsafeAllocator。
其中InstanceCreatorConstructor正是文档所述"注册实例创建器"的落地:construct()里调用instanceCreator.createInstance(type)(ConstructorConstructor.java)。InstanceCreator.java 的 Javadoc 还给出完整示例:对一个只有带参构造器的Id<T>类,可以定义IdInstanceCreator implements InstanceCreator<Id>,在createInstance里返回new Id(Object.class, 0L),再通过new GsonBuilder().registerTypeAdapter(Id.class, new IdInstanceCreator()).create()注册。文档特别强调两点:返回实例的字段内容无关紧要(反序列化时会全部覆盖);必须每次new一个新实例,绝不能返回共享的单例,否则后续反序列化会破坏它。
用字段而非 getter 指示 Json 元素
部分 JSON 库靠类型的 getter 来推断 JSON 元素。设计文档明确 Gson 选择字段(field)路线:序列化/反序列化时采用继承层级上所有非 transient、非 static、非 synthetic 的字段。理由有二:
- 并非所有类都写了命名得当的 getter;
getXXX/isXXX可能是语义方法(如isMarried表示状态),而非属性指示器。
这一规则在现代代码中的落点有两处。一处是 Excluder.java,其默认排除修饰符即Modifier.TRANSIENT | Modifier.STATIC——与文档"非 transient、非 static"完全对应;而 synthetic(编译器合成)字段的排除,见 ReflectiveTypeAdapterFactory.java 对ReflectionAccessFilterHelper.canAccess的处理及对匿名/局部类合成字段不可靠性的专门规避(同文件 L117-L140)。另一处是 ReflectiveTypeAdapterFactory.getFieldNames:字段的 JSON 名称默认由FieldNamingStrategy.translateName决定,若标注了@SerializedName则以注解值为准,alternate属性则给出反序列化时接受的其他备选名称。
文档同时承认"支持属性(properties)作为另一种映射也有充分的论据",并预告未来版本将把属性作为指示 JSON 字段的备选映射引入——"就目前而言,Gson 是字段驱动的"。这一判断至今未变:Gson 依旧是以字段为核心的库。
为什么大部分 Gson 类被标记为 final
设计文档解释了一个常被使用者困惑的策略:为什么 Gson 的类大多声明为final?
- Gson 已通过可插拔的序列化器/反序列化器提供了相当可扩展的架构,但类本身并未刻意设计成"可继承扩展";
- 若类是非 final 的,用户可能会合法地继承并扩展 Gson 类,然后期望该行为在后续所有版本中保持有效——这对维护者构成承诺负担;
- 因此选择先
final封死,等出现足够好的用例再放开扩展性; - 附带收益:
final也给 Java 编译器和虚拟机提供了额外的优化机会。
这个"谨慎开放、先封闭后演进"的哲学,可以从 ConstructorConstructor.java(public final class)、ObjectTypeAdapter.java、Excluder.java 等核心内部类上一窥——扩展的入口被刻意收敛在公开的TypeAdapter/TypeAdapterFactory/JsonSerializer等接口,而非类继承。
为什么大量使用内部接口和类
Gson 大量使用内部类,许多公共接口本身就是内部接口——设计文档举的例子是JsonSerializer.Context与JsonDeserializer.Context。作者说明这主要是风格问题:完全可以把它挪成顶层类JsonSerializerContext,但当时选择不这么做;同时也表态,如果能给出足够好的理由,他们也愿意改变这一哲学。
需要说明的是,这一风格在后续演进中已有部分调整:当前仓库里JsonSerializer与JsonDeserializer的序列化上下文已独立为顶层接口 JsonSerializationContext.java 与 JsonDeserializationContext.java。但"内部类承载紧密协作的私有实现"的组织方式依旧保留,例如ConstructorConstructor内部就定义了一组私有静态类(ThrowingObjectConstructor、InstanceCreatorConstructor),用于封装不同实例化策略(ConstructorConstructor.java)。从源码结构看,这正是文档所述"风格取向"的延续:能用内部类就近组织内聚逻辑,就不扩散到顶层命名空间。
为什么提供两种方式构造 Gson
Gson 的构造方式有两种:new Gson()与GsonBuilder。设计文档给出了明确分工:
- 无参构造器服务简单用例:默认选项够用、想立刻上手写代码的场景;
- Builder 模式服务其余场景:需要配置格式化器、版本控制(
Since/Until)等多项可选设置时,Builder 允许逐项指定这些最终会成为 Gson 构造参数的选项。
这条 API 设计延续至今:Gson.java 的 Javadoc 写着"You can create a Gson instance by invoking new Gson() ... You can also use GsonBuilder to build a Gson instance with various configuration options such as versioning support, pretty printing, and custom serialization and deserialization logic";GsonBuilder.java 则承载了诸如registerTypeAdapter、registerTypeAdapterFactory、registerTypeHierarchyAdapter(L818-L829,针对继承体系注册)等配置入口。Gson实例是线程安全的,官方建议可复用同一实例(例如存为static final字段),既节省 TypeAdapter 的缓存重建开销,也符合 Builder 一次配置、长期使用的模式。
与替代方案的对比(org.json 与 org.json.simple)
设计文档附带了与两款当时主流库的对比,并注明这些比较完成于 2007 年中后期,读者应将其作为历史背景理解。
与 org.json 的对比:org.json 是一个低层得多的库,适合在类里手写toJson()方法。如果因平台限制(例如平台禁止反射)而无法直接使用 Gson,可以退回用 org.json 在每个对象中手工编写toJson。言下之意:Gson 的价值在于用反射与适配器机制免去这种逐类手写;而 org.json 的价值在于"零反射"场景下仍然可用。
与 org.json.simple 的对比:org.json.simple 与 org.json 非常相似、同样偏底层。其关键问题是异常处理不佳——某些情况下它似乎直接把异常"吞掉",另一些情况下则抛出Error而非Exception。反观 Gson,正如前文所述,统一使用RuntimeException体系(JsonParseException/JsonSyntaxException/MalformedJsonException)报告解析失败,行为可预期、可区分。
需要强调,这两段对比仅反映 2007 年的局面,不代表今日任何第三方库的现状;文档作者本人也未将其作为持续有效的结论。
结语:设计文档的当代价值
回顾整份设计文档,可以看到 Gson 的多数关键决策并非随性的风格选择,而是围绕**"严格、可控、可扩展、不搞玄学"**四原则做出的工程权衡:类型树导航换取反序列化的严格性与容错、非受检异常换取失败语义的干脆、字段驱动换取对任意类(含不可修改类)的普适支持、final与内部类换取演进自由度、双构造方式换取易用性与可配置性的平衡。今天的 gson/src/main/java/com/google/gson 源码虽然经历了TypeAdapter体系、ReflectionAccessFilter、Strictness、ToNumberStrategy等大量演进,但文档所记录的核心决策仍能在 ConstructorConstructor、Excluder、ReflectiveTypeAdapterFactory、GsonBuilder 等处找到清晰而忠实的实现回声。理解这些"为什么",比记住 API 更能让你在使用 Gson 时做出符合其设计意图的选择。
若你想深入了解具体 API 的使用方法,仓库根目录的 UserGuide.md 提供了更完整的入门与进阶示例;对设计历史感兴趣的读者,则可直接精读本文主体所依据的 GsonDesignDocument.md 原文。
- 后端
- 序列化
【免费下载链接】gson
A Java serialization/deserialization library to convert Java Objects into JSON and back
相关推荐
Gson 设计文档精读:Gson 核心设计决策与源码实现解析
Gson 设计文档精读:Gson 核心设计决策与源码实现解析 导读 本文基于仓库根目录下的 GsonDesignDocument.md https://link
后端YgoMaster卡牌制作与修改教程:创建个性化游戏内容
YgoMaster卡牌制作与修改教程:创建个性化游戏内容 想要在YgoMaster中打造独一无二的游戏王卡组吗?本终极教程将教你如何轻松创建个性化卡牌、修改游戏
后端游戏开发逆向工程终极指南:PDFKit核心类PDFDocument的设计与实现原理
终极指南:PDFKit核心类PDFDocument的设计与实现原理 PDFKit是一个强大的Node.js库,用于生成PDF文档。本文将深入解析其核心类PDFD
后端文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考