diff --git a/GFramework.Game.Tests/Serializer/JsonSerializerTests.cs b/GFramework.Game.Tests/Serializer/JsonSerializerTests.cs index b6eb1a49..a9f7e962 100644 --- a/GFramework.Game.Tests/Serializer/JsonSerializerTests.cs +++ b/GFramework.Game.Tests/Serializer/JsonSerializerTests.cs @@ -49,6 +49,19 @@ public sealed class JsonSerializerTests }); } + [Test] + public void Settings_And_Converters_Should_Expose_Live_Configuration_Instance() + { + var settings = new JsonSerializerSettings(); + var serializer = new GameJsonSerializer(settings); + + Assert.Multiple(() => + { + Assert.That(serializer.Settings, Is.SameAs(settings)); + Assert.That(serializer.Converters, Is.SameAs(settings.Converters)); + }); + } + [Test] public void Converters_Should_Be_Used_For_Serialization_And_Deserialization() { @@ -174,4 +187,4 @@ public sealed class JsonSerializerTests }; } } -} \ No newline at end of file +} diff --git a/GFramework.Game/README.md b/GFramework.Game/README.md index 9bc7f8ff..2b0cf825 100644 --- a/GFramework.Game/README.md +++ b/GFramework.Game/README.md @@ -180,12 +180,14 @@ using GFramework.Core.Abstractions.Storage; using GFramework.Game.Serializer; using GFramework.Game.Storage; -ISerializer serializer = new JsonSerializer(); +var serializer = new JsonSerializer(); IStorage storage = new FileStorage("GameData", serializer); await storage.WriteAsync("player/profile", new { Name = "Alice", Level = 3 }); ``` +这里的 `JsonSerializer` 建议在组合根只创建并配置一次;如果需要自定义 `JsonSerializerSettings` 或 converters,请在把它注册给 `IStorage`、`DataRepository` 或架构 utility 之前完成。 + 如果你需要逻辑隔离,再包一层 `ScopedStorage`: ```csharp diff --git a/GFramework.Game/Serializer/JsonSerializer.cs b/GFramework.Game/Serializer/JsonSerializer.cs index 3e498840..53c45268 100644 --- a/GFramework.Game/Serializer/JsonSerializer.cs +++ b/GFramework.Game/Serializer/JsonSerializer.cs @@ -4,8 +4,12 @@ using Newtonsoft.Json; namespace GFramework.Game.Serializer; /// -/// JSON序列化器实现类,用于将对象序列化为JSON字符串或将JSON字符串反序列化为对象 +/// 基于 Newtonsoft.Json 的运行时 JSON 序列化器。 /// +/// +/// 该类型会直接持有并复用外部提供的 实例及其转换器集合,而不会在构造时复制配置。 +/// 请在组合根或启动阶段完成全部配置,并在注册给其他组件后将这些配置视为只读;否则在并发调用期间同时修改设置或转换器集合可能导致不可预测行为。 +/// public sealed class JsonSerializer : IRuntimeTypeSerializer { @@ -14,7 +18,10 @@ public sealed class JsonSerializer /// /// 初始化 JSON 序列化器。 /// - /// 可选的 Newtonsoft.Json 配置;不提供时使用默认配置。 + /// + /// 可选的 Newtonsoft.Json 配置实例;不提供时使用默认配置。 + /// 传入的实例会被当前序列化器直接复用,后续对该实例的修改会影响所有后续序列化与反序列化调用。 + /// public JsonSerializer(JsonSerializerSettings? settings = null) { _settings = settings ?? new JsonSerializerSettings(); @@ -23,11 +30,19 @@ public sealed class JsonSerializer /// /// 获取当前序列化器使用的 Newtonsoft.Json 配置实例。 /// + /// + /// 返回的是当前序列化器持有的活动配置实例,适合在启动阶段补充 contract resolver、格式化策略或 converter。 + /// 一旦该序列化器被共享给其他组件,应避免再修改返回值,以免破坏调用方对并发读行为的假设。 + /// public JsonSerializerSettings Settings => _settings; /// /// 获取当前序列化器使用的自定义转换器集合。 /// + /// + /// 该集合与 引用相同。 + /// 请在注册序列化器前完成 converter 配置,并避免在序列化器已经发布后继续增删转换器。 + /// public IList Converters => _settings.Converters; /// @@ -115,4 +130,4 @@ public sealed class JsonSerializer return result; } -} \ No newline at end of file +} diff --git a/ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md b/ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md index b0fb361d..a35c7333 100644 --- a/ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md +++ b/ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md @@ -13,7 +13,8 @@ - 已将根目录 legacy `local-plan/settings-persistence-serialization-tracking.md` 迁入 `ai-plan/public/data-repository-persistence/` - 第一轮 settings / persistence / serialization 修复、测试与文档同步已完成,并收入主题内 `archive/` - - 下一轮需要继续评估 `JsonSerializer` 配置说明、迁移模型统一抽象与 codec / persistence pipeline 边界 + - 当前正在补齐 `JsonSerializer` 的配置生命周期、只读约束与线程安全说明 + - 下一轮需要继续评估迁移模型统一抽象与 codec / persistence pipeline 边界 ## 当前状态摘要 @@ -26,11 +27,11 @@ - 原 `local-plan` 只有一份混合 tracking 文件,没有独立的 `todos/` 与 `traces/` - 详细历史已拆分迁入主题内 `archive/`,active tracking / trace 只保留当前恢复点、风险与下一步 - 历史已验证结果包括 `GFramework.Game.Tests` 的定向与全量通过,以及 `docs/zh-CN/game/*` 的同步更新 +- `GFramework.Game.Serializer.JsonSerializer` 当前直接暴露活动中的 `JsonSerializerSettings` 与 converters 集合,配置不会被复制 +- `docs/zh-CN/game/serialization.md` 现有“序列化器本身线程安全”表述与源码契约不一致,需要在本轮修正 ## 当前风险 -- 只读配置 / 线程安全说明缺口:`JsonSerializer` 新增 settings 与 converter 扩展后,若不补充约束说明,后续容易被误用 - - 缓解措施:下一轮先核对源码与文档,必要时补 XML docs 或采用文档 - 迁移模型分叉风险:`SettingsModel`、`DataRepository` 与 `SaveRepository` 的版本演进机制仍可能继续分叉 - 缓解措施:在新增更多 persistence feature 前,先评估能否抽出统一的 migration abstraction - Active 入口回膨胀风险:若后续把实现细节继续堆回 active 文档,会重新退化成旧 `local-plan` @@ -45,9 +46,12 @@ - 旧混合 `local-plan` 已拆分迁入主题内 archive - active 跟踪文件已按 `ai-plan` 治理规则精简为当前恢复入口 +- 已补充 `JsonSerializer` XML docs、文档示例与最小契约测试 +- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~JsonSerializerTests"` 已通过(9/9) +- 本次定向验证过程中出现的 analyzer warning 来自仓库既有代码,不属于本轮新增问题 ## 下一步 -1. 先评估 `JsonSerializer` 的只读配置、线程安全与实例级 converter 使用说明是否需要补足 -2. 再评估设置 / 通用仓库 / 存档仓库的迁移模型是否要统一抽象 -3. 最后评估压缩 / 加密 / 元数据策略是否应落入更明确的 codec / persistence pipeline +1. 再评估设置 / 通用仓库 / 存档仓库的迁移模型是否要统一抽象 +2. 最后评估压缩 / 加密 / 元数据策略是否应落入更明确的 codec / persistence pipeline +3. 若进入下一轮实现,先确定是否需要新的 dedicated recovery point 以避免 RP-001 active 入口继续膨胀 diff --git a/ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md b/ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md index 1f26c3ff..e18a51a2 100644 --- a/ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md +++ b/ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md @@ -28,3 +28,20 @@ 1. 后续继续该主题时,只从 `ai-plan/public/data-repository-persistence/` 进入,不再恢复 `local-plan/` 2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/` + +## 2026-04-20 + +### 阶段:JsonSerializer 配置契约补充(RP-001) + +- 复核 `GFramework.Game/Serializer/JsonSerializer.cs` 后确认:当前实现直接复用传入的 `JsonSerializerSettings`,并通过 `Settings` / `Converters` 暴露活动配置对象 +- 复核 `docs/zh-CN/game/serialization.md` 后确认:现有 FAQ 把 `JsonSerializer` 写成“本身线程安全”,与当前可变配置契约不一致 +- 决定本轮只补齐契约说明而不改变运行时行为: + - 在源码 XML docs 中说明 settings / converters 的生命周期与并发约束 + - 在定向单测中固定“序列化器暴露活动配置实例”的当前契约 + - 在 `docs/zh-CN/game/serialization.md`、`docs/zh-CN/game/index.md` 与 `GFramework.Game/README.md` 中同步修正接入建议 + +### 下一步 + +1. `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~JsonSerializerTests"` 已通过(9/9) +2. 验证过程中出现的 analyzer warning 为仓库既有 warning,未在本轮扩大 +3. 下一步回到 migration abstraction 与 codec / persistence pipeline 的后续评估 diff --git a/docs/zh-CN/game/index.md b/docs/zh-CN/game/index.md index 7beee74b..ef71e1e4 100644 --- a/docs/zh-CN/game/index.md +++ b/docs/zh-CN/game/index.md @@ -683,6 +683,8 @@ public class GameDataSerializer public GameDataSerializer() { + // 在构造阶段完成全部 JsonSerializerSettings / Converter 配置, + // 后续把 _serializer 视为共享只读实例。 _serializer = new JsonSerializer(new JsonSerializerSettings { Formatting = Formatting.Indented, diff --git a/docs/zh-CN/game/serialization.md b/docs/zh-CN/game/serialization.md index b1a827bb..fdc24ba1 100644 --- a/docs/zh-CN/game/serialization.md +++ b/docs/zh-CN/game/serialization.md @@ -55,7 +55,8 @@ public interface IRuntimeTypeSerializer : ISerializer ### JSON 序列化器 -`JsonSerializer` 是基于 Newtonsoft.Json 的实现: +`JsonSerializer` 是基于 Newtonsoft.Json 的实现。它会直接复用构造时提供的 +`JsonSerializerSettings` 与 `Converters` 集合,因此推荐在组合根统一完成配置,再把同一个实例注册到架构或仓库中复用: ```csharp public sealed class JsonSerializer : IRuntimeTypeSerializer @@ -81,8 +82,10 @@ public class GameArchitecture : Architecture { protected override void Init() { - // 注册 JSON 序列化器 + // 在启动阶段一次性完成配置,后续将该实例视为只读 var jsonSerializer = new JsonSerializer(); + jsonSerializer.Converters.Add(new PlayerDataJsonConverter()); + RegisterUtility(jsonSerializer); RegisterUtility(jsonSerializer); } @@ -166,6 +169,40 @@ public void SerializeRuntimeType() } ``` +### 配置生命周期约束 + +`JsonSerializer` 不会复制 `JsonSerializerSettings`。这意味着: + +- 传给构造函数的 settings 会被原样保留 +- `serializer.Settings` 与 `serializer.Converters` 返回的都是活动配置对象 +- 一旦序列化器实例已经注册给其他模块或开始被并发调用,就不应继续修改这些配置 + +推荐模式: + +```csharp +var settings = new JsonSerializerSettings +{ + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore +}; + +settings.Converters.Add(new Vector2JsonConverter()); + +var serializer = new JsonSerializer(settings); + +architecture.RegisterUtility(serializer); +architecture.RegisterUtility(serializer); +``` + +不推荐模式: + +```csharp +var serializer = architecture.GetUtility(); + +// 已经共享给运行时后再修改配置,容易让并发调用得到不稳定行为 +((JsonSerializer)serializer).Converters.Add(new LateBoundConverter()); +``` + ## 高级用法 ### 与存储系统集成 @@ -463,10 +500,11 @@ public PlayerDataV2 LoadWithMigration(string json) ## 最佳实践 -1. **使用接口而非具体类型**:依赖 `ISerializer` 接口 +1. **优先依赖接口,在组合根统一实例化**:业务代码依赖 `ISerializer`,具体 `JsonSerializer` 在启动阶段配置一次即可 ```csharp ✓ var serializer = this.GetUtility(); - ✗ var serializer = new JsonSerializer(); // 避免直接实例化 + ✓ var serializer = new JsonSerializer(settings); // 仅在组合根/启动阶段配置 + ✗ var serializer = new JsonSerializer(); // 避免在业务逻辑中临时创建 ``` 2. **为数据类提供默认值**:确保反序列化的健壮性 @@ -731,10 +769,26 @@ public async Task LoadEncrypted(string key) ### 问题:序列化器是线程安全的吗? **解答**: -`JsonSerializer` 本身是线程安全的,但建议通过架构的 Utility 系统访问: +`JsonSerializer` 的并发读行为只在“配置不再变化”这个前提下才安全。当前实现会暴露活动中的 +`JsonSerializerSettings` 与 `Converters` 集合,因此: + +- 可以在启动阶段创建并配置一个共享实例 +- 可以在配置冻结后把该实例注册为 Utility 或注入到仓库 +- 不应在序列化器已经被多个调用方使用时继续修改 settings、contract resolver 或 converters + +推荐按下面的方式在启动阶段完成配置,然后只做读操作: ```csharp -// 线程安全的访问方式 +// 启动阶段完成全部配置 +var serializer = new JsonSerializer(new JsonSerializerSettings +{ + NullValueHandling = NullValueHandling.Ignore +}); +serializer.Converters.Add(new GameDataJsonConverter()); + +architecture.RegisterUtility(serializer); + +// 运行阶段只复用,不再修改配置 public async Task ParallelSave() { var tasks = Enumerable.Range(0, 10).Select(async i =>