1. 从“硬编码”到“动态编织”运行时序列化的核心价值在分布式系统、微服务架构乃至游戏开发中数据在不同模块、不同进程、甚至不同机器之间穿梭是家常便饭。我们经常需要把一个内存中的对象转换成一串可以存储或传输的字节流这个过程就是序列化Serialization。反过来从字节流重建对象就是反序列化Deserialization。听起来很简单但魔鬼藏在细节里。你可能用过 JSON、XML或者像 Protobuf、MessagePack 这样的二进制协议。这些工具大多属于“编译时”或“设计时”序列化——你需要预先定义好数据的结构Schema代码生成器会根据这个结构生成专门的序列化/反序列化代码。但有没有遇到过这样的场景你的数据模型是动态的在程序运行之前你根本无法预知它的完整结构。或者你正在开发一个插件系统、一个脚本引擎、一个热更新模块你需要将运行时创建的类型实例进行持久化或网络传输。这时传统的“编译时”序列化方案就捉襟见肘了。这正是“运行时序列化”Run-time Serialization大显身手的地方。它不依赖于预先的代码生成而是在程序运行期间动态地探查对象的结构字段、属性、类型信息并据此完成数据的打包与解包。简单来说它处理的是“活”的对象而不是“死”的蓝图。2. 运行时序列化是如何“看见”对象内部的要实现运行时序列化核心在于“反射”Reflection机制。反射允许程序在运行时检查、修改甚至创建类型、对象和调用方法。这是实现动态序列化的基石。不同语言对反射的支持程度不同这也直接影响了其运行时序列化的实现方式和能力。2.1 基于反射的字段遍历与值提取以 C# 为例其反射机制非常强大。一个典型的运行时序列化器工作流程如下获取类型信息给定一个对象obj首先通过obj.GetType()获取其运行时类型Type对象。获取成员信息通过Type.GetFields()和Type.GetProperties()方法获取该类型所有公开的字段和属性。这里就有讲究了你是只序列化公共成员还是也包括私有成员是否要忽略某些标记了[NonSerialized]或[JsonIgnore]特性的成员一个健壮的序列化器需要提供丰富的配置选项。读取成员值对于每个需要序列化的成员FieldInfo或PropertyInfo调用GetValue(obj)方法获取该成员在当前对象实例上的值。处理值的序列化获取到的值可能是一个简单类型如int,string也可能是一个复杂对象甚至是包含循环引用的对象图。序列化器需要递归地处理这些值简单类型直接转换为目标格式如 JSON 字符串、二进制字节。复杂对象回到步骤1对该值对象进行新一轮的序列化。集合与数组需要特殊处理遍历每个元素进行序列化。循环引用这是运行时序列化的一大难点。对象 A 引用了 BB 又引用了 A如果不加处理序列化会陷入无限递归。高级的序列化器会维护一个“已序列化对象”的字典遇到已处理过的对象时可以选择输出引用标识符如$ref或直接抛出异常。下面是一个极度简化的概念性代码展示这个流程// 注意此为示意代码非生产可用 public string SimpleRuntimeSerialize(object obj) { var type obj.GetType(); var result new Dictionarystring, object(); foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) { // 检查是否需要忽略该字段 if (Attribute.IsDefined(field, typeof(NonSerializedAttribute))) continue; var fieldValue field.GetValue(obj); result[field.Name] SerializeValue(fieldValue); // 递归或转换 } return JsonConvert.SerializeObject(result); // 转换为JSON } private object SerializeValue(object value) { if (value null) return null; var valueType value.GetType(); // 如果是简单类型 if (valueType.IsPrimitive || valueType typeof(string)) return value; // 如果是复杂对象递归序列化 return SimpleRuntimeSerialize(value); }2.2 不同语言生态下的实现差异Java同样依赖强大的反射机制。除了手动实现更常见的做法是使用成熟的库如 Jackson、Gson。它们通过反射和注解如JsonIgnore提供了高度可配置的运行时序列化能力。Java 的反射性能开销相对 C# 更大因此这些库内部做了大量缓存优化如缓存Class对象的Field、Method信息。C由于语言本身缺乏标准的反射支持C17/20 引入了有限的编译期反射但运行时反射仍非标准实现运行时序列化更为复杂。常见做法有手动注册为每个需要序列化的类手动编写序列化/反序列化函数并向一个中央注册表注册类型信息。Unreal Engine 的序列化系统就是此类的典型代表。代码生成通过外部工具如 Protobuf 的protoc或宏在编译时生成反射信息和序列化代码模拟运行时能力。这其实是一种混合模式。第三方库如cereal、Boost.Serialization它们通常要求类提供特定的成员函数如serialize方法库在编译时或运行时利用这些函数进行操作。动态语言Python, JavaScript这些语言的对象本身就可以看作是字典属性表序列化如 Python 的pickle、json.dumps JS 的JSON.stringify天然就是“运行时”的。它们的挑战更多在于安全反序列化任意代码执行和自定义对象的深度控制。注意反射虽然强大但性能是绕不开的坎。频繁调用GetFields、GetValue会带来显著的开销。因此高性能的运行时序列化库如 MessagePack for C#会在首次序列化某个类型时通过反射生成并缓存一个高效的“序列化委托”后续调用直接使用该委托从而避免重复的反射开销。这被称为“发射Emit优化”。3. 核心挑战与高级特性超越简单的字段复制如果运行时序列化只是机械地复制所有字段那它的价值就有限了。在实际项目中我们会遇到一系列复杂情况处理这些情况的能力才是衡量一个运行时序列化方案是否成熟的关键。3.1 版本兼容性与字段演化这是序列化尤其是用于长期存储或网络通信的序列化必须面对的问题。你的数据模型类会随着版本迭代而改变增加新字段、删除废弃字段、重命名字段、修改字段类型。一个健壮的运行时序列化方案需要能优雅地处理这些变化。向前兼容旧代码读新数据新数据里多出来的字段旧的反序列化代码应该能忽略它们而不是直接报错。这通常通过在序列化数据中携带字段名或ID来实现反序列化时只处理自己认识的字段。向后兼容新代码读旧数据旧数据里缺少新版本必需的字段新的反序列化代码应该能为这些字段提供合理的默认值如null、0、空集合。有时还需要通过[DefaultValue]特性或自定义转换逻辑来处理。字段重命名通过给字段附加一个稳定不变的唯一标识符如整数ID或GUID而不是依赖易变的字段名。这样即使类中的字段名从userName改为username只要ID不变序列化数据仍然可读。类型转换当字段类型发生变化时如int改为long需要定义明确的转换规则。一些序列化格式如JSON本身是弱类型的反序列化时可以进行安全的类型转换如数字字符串转数字。而二进制格式则需要更精确的版本映射规则。处理版本兼容性往往需要在序列化数据中嵌入版本号并在反序列化逻辑中根据版本号执行不同的分支代码。3.2 多态与继承关系的处理面向对象编程中我们经常使用基类或接口引用指向子类实例。序列化这样一个引用时必须完整地保存子类的类型信息和所有数据。// 例如有一个动物基类和两个子类 Animal pet new Dog(); // 实际是Dog对象 // 序列化 pet一个朴素的反射序列化器如果只查看Animal类型它只会看到基类的字段Dog特有的字段如Breed会丢失。正确的做法是序列化时不仅序列化对象数据还要序列化对象的实际运行时类型信息如类型的全限定名MyAssembly.Dog, MyAssembly。反序列化时首先根据类型信息使用反射动态创建Dog的实例例如Activator.CreateInstance然后再将数据填充到这个新实例中。这个过程被称为“类型鉴别”Type Discrimination。在 JSON 中通常通过一个特殊的字段如$type来实现。.NET 原生的BinaryFormatter和Json.NETTypeNameHandling设置都支持此功能。警告反序列化时根据字符串动态创建类型是高风险操作因为它可能被用来实例化任何已加载的类型存在安全漏洞。在生产环境中使用此功能时必须严格限制允许反序列化的类型白名单。3.3 性能优化策略如前所述纯反射性能低下。成熟的运行时序列化库会采用以下优化手段表达式树编译在首次处理某个类型时利用反射信息构建一个描述“如何序列化该类型对象”的表达式树Expression Tree然后将其编译成高效的委托。后续序列化直接调用这个委托其性能可以接近硬编码的序列化。IL 代码发射更底层的优化直接生成中间语言IL指令来读写对象字段性能最高。MessagePack-CSharp和protobuf-net当用于未预先标记的类时就采用了这种技术。缓存一切缓存Type对象、字段/属性集合、已编译的委托等。序列化通常是高频操作缓存能极大提升性能。池化缓冲区避免频繁分配和回收用于存储字节流或字符串的缓冲区使用对象池进行复用。4. 实战构建一个简易但功能完整的运行时序列化器为了更深刻地理解上述概念我们尝试用 C# 设计一个支持基础功能的运行时 JSON 序列化器。我们将它命名为FlexiJsonSerializer。我们的目标是能处理嵌套对象、集合、忽略特定字段并初步考虑版本兼容。4.1 定义核心接口与特性首先我们定义一些特性来控制序列化行为。[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public class FlexiIgnoreAttribute : Attribute { } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public class FlexiAliasAttribute : Attribute { public string Name { get; } public FlexiAliasAttribute(string name) Name name; } // 用于标记类提供版本信息 [AttributeUsage(AttributeTargets.Class)] public class FlexiVersionAttribute : Attribute { public int Version { get; } public FlexiVersionAttribute(int version) Version version; }4.2 实现序列化核心我们实现一个静态类包含主要的序列化方法。这里我们使用System.Text.Json的Utf8JsonWriter来生成 JSON因为它性能较好。using System.Collections; using System.Reflection; using System.Text.Json; public static class FlexiJsonSerializer { // 缓存类型对应的序列化动作委托 private static readonly DictionaryType, ActionUtf8JsonWriter, object _serializerCache new(); public static string Serialize(object obj) { using var stream new MemoryStream(); using var writer new Utf8JsonWriter(stream, new JsonWriterOptions { Indented true }); SerializeValue(writer, obj); writer.Flush(); return System.Text.Encoding.UTF8.GetString(stream.ToArray()); } private static void SerializeValue(Utf8JsonWriter writer, object value) { if (value null) { writer.WriteNullValue(); return; } var type value.GetType(); // 处理基础类型和字符串 if (type.IsPrimitive || type typeof(string) || type typeof(decimal)) { WritePrimitive(writer, value); return; } // 处理字典 if (value is IDictionary dictionary) { writer.WriteStartObject(); foreach (DictionaryEntry entry in dictionary) { writer.WritePropertyName(entry.Key.ToString()); SerializeValue(writer, entry.Value); } writer.WriteEndObject(); return; } // 处理集合非字典的IEnumerable if (value is IEnumerable enumerable !(value is string)) { writer.WriteStartArray(); foreach (var item in enumerable) { SerializeValue(writer, item); } writer.WriteEndArray(); return; } // 处理复杂对象 SerializeObject(writer, value, type); } private static void SerializeObject(Utf8JsonWriter writer, object obj, Type type) { writer.WriteStartObject(); // 可选写入类型信息以实现多态反序列化安全考虑此处暂不实现 // writer.WriteString($type, type.AssemblyQualifiedName); // 获取或创建该类型的序列化委托 var serializer GetOrCreateSerializer(type); serializer(writer, obj); writer.WriteEndObject(); } private static ActionUtf8JsonWriter, object GetOrCreateSerializer(Type type) { if (_serializerCache.TryGetValue(type, out var serializer)) return serializer; // 构建序列化逻辑 var fields type.GetFields(BindingFlags.Public | BindingFlags.Instance) .Where(f !Attribute.IsDefined(f, typeof(FlexiIgnoreAttribute))); var properties type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p p.CanRead !Attribute.IsDefined(p, typeof(FlexiIgnoreAttribute))); // 这里我们用一个简单的委托组合实际高性能库会编译表达式树或IL serializer (writer, obj) { foreach (var field in fields) { var fieldName GetMemberName(field); writer.WritePropertyName(fieldName); SerializeValue(writer, field.GetValue(obj)); } foreach (var prop in properties) { var propName GetMemberName(prop); writer.WritePropertyName(propName); SerializeValue(writer, prop.GetValue(obj)); } }; _serializerCache[type] serializer; return serializer; } private static string GetMemberName(MemberInfo member) { var aliasAttr member.GetCustomAttributeFlexiAliasAttribute(); return aliasAttr?.Name ?? member.Name; } private static void WritePrimitive(Utf8JsonWriter writer, object value) { switch (value) { case int i: writer.WriteNumberValue(i); break; case long l: writer.WriteNumberValue(l); break; case float f: writer.WriteNumberValue(f); break; case double d: writer.WriteNumberValue(d); break; case bool b: writer.WriteBooleanValue(b); break; case string s: writer.WriteStringValue(s); break; case decimal dec: writer.WriteNumberValue(dec); break; // ... 其他类型 default: writer.WriteStringValue(value.ToString()); break; } } }4.3 使用示例与踩坑点定义两个测试类[FlexiVersion(1)] public class Person { [FlexiAlias(id)] public int Id { get; set; } [FlexiAlias(full_name)] public string Name { get; set; } [FlexiIgnore] // 这个字段不会被序列化 public string Secret { get; set; } public Liststring Tags { get; set; } new(); public Dictionarystring, object Metadata { get; set; } new(); public Address HomeAddress { get; set; } } public class Address { public string City { get; set; } public string Street { get; set; } }使用我们的序列化器var person new Person { Id 1, Name 张三, Secret password123, Tags new Liststring { developer, blogger }, Metadata new Dictionarystring, object { { level, 10 }, { active, true } }, HomeAddress new Address { City 北京, Street 科技园路 } }; var json FlexiJsonSerializer.Serialize(person); Console.WriteLine(json);输出结果会类似{ id: 1, full_name: 张三, Tags: [developer, blogger], Metadata: { level: 10, active: true }, HomeAddress: { City: 北京, Street: 科技园路 } }可以看到Secret字段被忽略了Id和Name输出了别名。踩坑实录与注意事项性能陷阱我们这个实现为了清晰在每次序列化对象时都递归调用SerializeValue并且没有对嵌套对象的序列化动作进行缓存优化。对于深层嵌套或大量数据的序列化性能会远低于工业级库。生产环境务必使用优化后的方案。循环引用如果Person里有一个Friend属性指向另一个Person而那个Person的Friend又指回来我们的代码会栈溢出。必须在SerializeValue或SerializeObject中引入“已访问对象”的跟踪机制检测到循环引用时选择抛出异常或输出引用标记。类型安全与反序列化我们只实现了序列化。反序列化更为复杂需要根据JSON结构动态构建对象、设置字段值并处理类型转换、构造函数、集合初始化等问题。实现一个完整的、安全的反序列化器是另一个巨大的工程。版本号处理我们定义了[FlexiVersion]特性但在序列化/反序列化逻辑中并未使用。完整的实现需要在序列化数据中写入版本号并在反序列化时根据版本号调整字段映射逻辑。5. 工业级运行时序列化方案选型与对比在真实项目中我们几乎不会从头造轮子。了解主流方案及其适用场景至关重要。方案/库语言核心机制优点缺点适用场景System.Text.Json(反射模式)C#运行时反射支持源码生成优化.NET 官方库性能较好尤其开启源码生成后安全。配置相对复杂多态支持需手动配置。通用的Web API、配置序列化追求性能和现代.NET生态集成。Json.NET (Newtonsoft.Json)C#运行时反射高度可配置功能极其丰富自定义转换器、灵活忽略策略、完美多态支持社区生态庞大。性能略低于System.Text.Json默认反射模式非官方库。遗留项目、需要高度灵活性和复杂功能的场景。MessagePack for C#C#首次反射后通过IL Emit生成高效代码极高的性能极小的序列化体积二进制。二进制格式人类不可读需要预定义Contract或标记特性。游戏网络同步、高性能RPC、内存数据库缓存对性能和带宽敏感的场景。Java JacksonJava运行时反射大量缓存优化功能强大社区标准与Spring生态集成极佳。配置繁多初学者上手有一定门槛。Java Web 开发尤其是Spring Boot的事实标准。PythonpicklePythonPython对象内部结构的直接转储使用简单能序列化几乎所有Python对象包括函数、类。极度不安全只能用于可信数据源。不同Python版本间可能不兼容。临时缓存、进程间通信同一环境、机器学习模型暂存但更推荐joblib。Protocol Buffers(动态消息)多语言通过Descriptor在运行时解析.proto定义跨语言、向前向后兼容性好性能优秀。需要.proto定义文件动态API比静态生成代码的API更繁琐。需要严格Schema管理和跨语言通信的微服务架构。选型建议首选官方/标准库在新的 .NET 项目中优先考虑System.Text.Json在 Java Spring 项目中Jackson 是自然之选。它们维护性好生态兼容性强。极致性能与体积如果序列化/反序列化是性能瓶颈如高频游戏消息、金融交易MessagePack、Protobuf 等二进制协议是更好的选择。动态与无Schema如果你的数据模型完全动态无法预定义那么基于反射的 JSON 库如 Json.NET 的动态对象JObject、ExpandoObject支持或像MessagePack的动态契约解析模式会更合适。安全第一永远不要反序列化来自不可信源的二进制格式如pickle、BinaryFormatter。对于网络数据JSON 通常是更安全的选择配合严格的反序列化类型控制。6. 在游戏开发中的特殊应用Unity与Unreal Engine游戏引擎是运行时序列化的重度用户用于实现存档/读档、场景编辑、网络复制、热更新等功能。Unity Unity 的默认序列化系统是内置的、基于反射的。当你将一个脚本组件挂到 GameObject 上并在 Inspector 中编辑公共字段这些值会被序列化到场景文件.unity或预制体.prefab中。它的特点是支持有限类型主要支持基本类型、Unity内置类型Vector3,Color、数组、列表以及标记了[System.Serializable]的自定义类。不支持多态一个Animal类型的字段如果赋值为Dog序列化后只会保存Animal的数据。性能考虑Unity 的序列化在构建时和运行时都会发生因此对性能有要求。不恰当地使用[SerializeField]暴露大量复杂结构会影响编辑器和运行时性能。自定义序列化通过实现ISerializationCallbackReceiver接口可以在序列化前后执行自定义逻辑用于处理 Unity 默认不支持的类型如字典。Unreal Engine Unreal 的序列化系统属性系统UProperty更为强大和复杂它基于宏如UPROPERTY()在编译时生成反射信息和序列化代码。元数据驱动UPROPERTY()宏不仅暴露变量给编辑器还为其附加了丰富的元数据如编辑条件、复制条件、序列化条件。版本化与默认值Unreal 的序列化系统天然支持版本升级。当类结构改变时旧版本数据中不存在的属性会使用类定义中指定的默认值。网络复制序列化系统与网络复制深度集成标记为Replicated的属性会自动通过网络同步。文本与二进制格式支持将对象序列化为人类可读的文本用于配置、存档和紧凑的二进制格式用于网络传输、快速加载。在游戏开发中实现自定义运行时序列化时一个常见的技巧是使用“序列化代理”模式。即为那些引擎无法直接序列化的复杂第三方类或数据结构编写一个轻量的、可序列化的代理类Surrogate在序列化时将原对象转换为代理对象反序列化时再还原回来。7. 安全红线反序列化漏洞的防御之道运行时反序列化尤其是根据类型名动态创建对象是极其危险的操作。攻击者可以构造恶意的序列化数据诱使你的程序反序列化并执行任意代码。著名的Java反序列化漏洞、.NET BinaryFormatter漏洞、Python pickle漏洞都源于此。防御策略避免不安全的格式绝对不要使用BinaryFormatter、SoapFormatter、Python pickle处理来自网络或不受信任文件的序列化数据。使用安全格式优先使用 JSON、XML 等文本格式它们通常不直接导致代码执行。严格类型控制如果必须使用支持多态反序列化的功能如 Json.NET 的TypeNameHandling务必配置SerializationBinder或类似的类型解析器将其限制在一个明确的白名单内只允许反序列化你预期的少数几种类型。数据校验反序列化后的对象应进行业务逻辑上的有效性校验确保数据在合理范围内。最小权限原则运行反序列化代码的进程应使用尽可能低的权限。例如在 Json.NET 中安全地使用类型名称处理var settings new JsonSerializerSettings { TypeNameHandling TypeNameHandling.Auto, SerializationBinder new MySafeBinder() // 自定义绑定器只允许特定类型 }; public class MySafeBinder : ISerializationBinder { private readonly ListType _allowedTypes new ListType { typeof(Person), typeof(Address) /* ... */ }; public Type BindToType(string assemblyName, string typeName) { var type Type.GetType(${typeName}, {assemblyName}); return _allowedTypes.Contains(type) ? type : throw new SecurityException($Type {typeName} is not allowed.); } public void BindToName(Type serializedType, out string assemblyName, out string typeName) { assemblyName serializedType.Assembly.FullName; typeName serializedType.FullName; } }运行时序列化是一把强大的双刃剑。它提供了无与伦比的灵活性让程序能够应对动态和未知的数据结构。然而这种灵活性是以性能开销和潜在的安全风险为代价的。理解其底层机制反射、核心挑战版本、多态、循环引用以及安全边界是正确、高效、安全使用这项技术的前提。在实际开发中评估需求在灵活、性能、安全之间做出权衡选择最合适的工具或设计才是工程师价值的体现。我个人在构建插件系统或数据驱动工具时会倾向于采用混合策略核心固定结构用编译时序列化如 Protobuf保证性能和协议稳定需要高度动态扩展的部分则用一个经过严格安全加固的、基于反射的运行时序列化组件来处理并为其划定清晰的边界。