ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Quartz.NET 持久化存储的 System.Text.Json 序列化:从配置、自定义到迁移实战指南

Quartz.NET 持久化存储的 System.Text.Json 序列化:从配置、自定义到迁移实战指南 任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载导读本文聚焦 Quartz.NET 官方文档 docs/documentation/quartz-3.x/packages/system-text-json.md 所讲解的 System.Text.Json 序列化方案说明如何为基于 ADO.NET 的持久化 JobStore 接入 JSON 序列化、如何从旧版二进制序列化平滑迁移、如何自定义序列化选项与扩展自定义 Trigger / Calendar 类型。读完本文你将掌握从属性配置与 SchedulerBuilder 配置、混合迁移序列化器到自定义ICalendarSerializer的完整实战链路并理解 Quartz.NET 4.x 中该序列化器内置于Quartz主包、成为默认方案的底层原理。::: tip 对于全新项目JSON 是官方推荐的持久化格式提示框原文JSON is the recommended persistent format for greenfield projects。同时官方强烈建议开启useProperties即把 JobDataMap 的键值限制为字符串以保持存储格式的简单与稳定。 :::一、为什么需要 JSON 序列化从二进制到 JSONQuartz.NET 的持久化 JobStore如JobStoreTX、JobStoreCMT见 JobStore 相关实现把 Trigger、Calendar、JobDataMap 等对象以字节流的形式写入数据库 BLOB 字段。早期的持久化格式基于 .NET 的二进制序列化BinaryObjectSerializer而 JSON 序列化SystemTextJsonObjectSerializer提供了更好的可读性、跨平台兼容性与长期可维护性。在 Quartz.NET 4.x 中System.Text.Json 序列化器已折叠进Quartz主包并成为默认序列化方案。这一点在 src/Quartz.Serialization.SystemTextJson/README.md 中说明得很清楚该独立包在 4.x 下是空包仅为了依赖机器人dependency bot能对Quartz与本包做分组升级而保留发布实际使用时应移除对Quartz.Serialization.SystemTextJson的引用保留Quartz包本身JsonSerializationException也已经移入Quartz包。3.x 时代则需额外安装独立包这正是本文所依据的 3.x 文档的默认前提。二、安装3.x 时代通过 NuGet 安装独立包Install-Package Quartz.Serialization.SystemTextJson如果使用 4.x 版本则无需该包——序列化器内置在Quartz包中见 Quartz.Serialization.SystemTextJson.csproj 中的说明Folded into Quartz in 4.0。三、配置持久化 JobStore 使用 System.Text.Json3.1 经典属性式配置Classic property-based configuration通过NameValueCollection指定 JobStore 类型与序列化器类型var properties new NameValueCollection { [quartz.jobStore.type] Quartz.Impl.AdoJobStore.JobStoreTX, Quartz, [quartz.serializer.type] stj }; ISchedulerFactory schedulerFactory new StdSchedulerFactory(properties);其中属性键取值说明quartz.jobStore.typeQuartz.Impl.AdoJobStore.JobStoreTX, Quartz使用基于 ADO.NET 的事务型 JobStore也可用JobStoreCMT或LocalTransactionJobStorequartz.serializer.typestj指定 System.Text.Json 序列化器关于stj别名的底层解析见 QuartzPropertyBridge.cs 的ApplySerializer方法stj与json都会映射到SystemTextJsonObjectSerializer类型newtonsoft则加载Quartz.Serialization.Newtonsoft中的序列化器其他任意值被当作程序集限定类型名直接加载。需要特别注意的是binary值会直接抛出SchedulerException提示Binary serialization is not supported anymore. Use JSON serialization instead.——也就是说二进制序列化在当前版本已不被支持。3.2 使用 SchedulerBuilder 配置推荐var config SchedulerBuilder.Create(); config.UsePersistentStore(store { // its generally recommended to stick with // string property keys and values when serializing store.UseProperties true; store.UseGenericDatabase(dbProvider, db db.ConnectionString my connection string ); store.UseSystemTextJsonSerializer(); }); ISchedulerFactory schedulerFactory config.Build();要点解读store.UseProperties true将 JobDataMap 的键值限定为字符串StoreJobDataAsStrings避免存储复杂对象导致的反序列化问题。这一配置在 Quartz 4.x 属性绑定中同时兼容旧拼写JobStore:UseProperties与新拼写JobStore:StoreJobDataAsStrings见 ConfigurationIsNeverSilentlyDroppedTest.cs。store.UseGenericDatabase(dbProvider, db ...)使用数据库 Provider 与连接字符串本文配套的 Weasel 各数据库实现 覆盖 SqlServer、PostgreSQL、MySQL、SQLite、Oracle、Firebird。store.UseSystemTextJsonSerializer()核心注册入口由 SystemTextJsonConfigurationExtensions.cs 实现。若未提供回调序列化器会读取容器级注册表若提供了回调则回调所填充的SystemTextJsonSerializerRegistry会被该 Scheduler 独占不会与其他 Scheduler 共享。四、从二进制序列化迁移Migrating from binary serialization官方明确说明不存在一刀切的官方迁移方案因为每个环境的既有数据各不相同。文档给出的可落地配方是配置一个自定义序列化器如MigratorSerializer让它读二进制格式、写 JSON 格式让系统在运行过程中逐步完成迁移或编写一个独立程序把所有已序列化资产加载出来再写回数据库。混合序列化器示例using System.Text.Json; using Quartz.Simpl; using Quartz.Spi; namespace Quartz; public sealed class MigratorSerializer : IObjectSerializer { private readonly BinaryObjectSerializer binarySerializer; private readonly SystemTextJsonObjectSerializer jsonSerializer; public MigratorSerializer() { binarySerializer new BinaryObjectSerializer(); // you might need custom configuration, see sections about customizing // in documentation jsonSerializer new SystemTextJsonObjectSerializer(); } public T DeSerializeT(byte[] data) where T : class { try { // Attempt to deserialize data as JSON return jsonSerializer.DeSerializeT(data)!; } catch (JsonException) { // Presumably, the data was not JSON, we instead use the binary serializer var binaryData binarySerializer.DeSerializeT(data); if (binaryData is JobDataMap jobDataMap) { // make sure we mark the map as dirty so it will be serialized as JSON next time jobDataMap[SchedulerConstants.ForceJobDataMapDirty] true; } return binaryData!; } } public void Initialize() { binarySerializer.Initialize(); jsonSerializer.Initialize(); } public byte[] SerializeT(T obj) where T : class { return jsonSerializer.Serialize(obj); } }工作原理与关键细节IObjectSerializer是序列化器契约SerializeT/DeserializeT两个方法见 IObjectSerializer.cs写入一律走 JSON读取时先尝试 JSON 反序列化捕获JsonException后回退到二进制反序列化从而同时兼容新旧两种 BLOB 数据。脏标记dirty flag机制当读取到的是二进制格式的JobDataMap时向其中写入SchedulerConstants.ForceJobDataMapDirty键其常量值为QRTZ_FORCE_JOB_DATAMAP_DIRTY见 SchedulerConstants.cs。JobStore 在回写该 Map 时会强制写入 BLOB从而把这份数据“顺带”升级为 JSON 格式。与之配套的写入侧处理见 AdoJobStoreBase.Store.cs先写入该键、再移除它仅用于强制触发BLOB 重写而不会把脏标记本身持久化到数据中JobDataMap.cs 明确该键不会被拷贝进新 Map。若数据无法被 JSON 反序列化说明它很可能是二进制 BLOB用BinaryObjectSerializer读回后由 JobStore 在后续更新中自动重写为 JSON从而实现渐进式迁移数据在读写循环中被逐步转换。五、自定义序列化选项Customizing serialization options要微调序列化行为可子类化SystemTextJsonObjectSerializer并重写CreateSerializerOptions向其JsonSerializerOptions追加自定义 Converter、命名策略等。class CustomJsonSerializer : SystemTextJsonObjectSerializer { protected override JsonSerializerOptions CreateSerializerOptions() { var options base.CreateSerializerOptions(); options.Converters.Add(new MyCustomConverter()); return options; } }配置该自定义序列化器两种方式任选store.UseSerializerCustomJsonSerializer(); // or quartz.serializer.type MyProject.CustomJsonSerializer, MyProject源码级佐证SystemTextJsonObjectSerializerSystemTextJsonObjectSerializer.cs的CreateSerializerOptions默认会调用AddQuartzConverters注册 Quartz 的 7 个 Converter——CalendarConverter、CronExpressionConverter、JobDataMapConverter、JobKeyConverter、TriggerKeyConverter、NameValueCollectionConverter、TriggerConverter见 SystemTextJsonConfigurationExtensions.cs再叠加由QuartzStoreJsonContext生成的元数据解析器链。选项对象采用首次使用时的惰性构建Options属性带锁见同一文件 L46-L61因此重写CreateSerializerOptions不会受构造函数时序影响。自定义 Converter 追加在 Quartz 自带 Converter 之后由System.Text.Json的 Converter 列表顺序决定优先级。一个实战细节若你的自定义序列化器需要接收容器注入的SystemTextJsonSerializerRegistry应显式声明(SystemTextJsonSerializerRegistry registry) : base(registry)构造函数否则只有内置类型被识别——完整示例见 SystemTextJsonSamples.cs。六、自定义 Calendar 序列化Customizing calendar serialization自定义 Calendar 需要对应的ICalendarSerializer实现。官方基类CalendarSerializerTCalendar让序列化器保持强类型类型参数在编译期即与 Calendar 类型绑定配错会直接产生编译错误而非运行时InvalidCastException见 CalendarSerializer.cs。自定义 Calendar 与序列化器using System; using System.Runtime.Serialization; using System.Text.Json; using Quartz.Impl.Calendar; using Quartz.Serialization.SystemTextJson; [Serializable] public sealed class CustomCalendar : BaseCalendar { public CustomCalendar() { } // binary serialization support private CustomCalendar(SerializationInfo info, StreamingContext context) : base(info, context) { SomeCustomProperty info?.GetBoolean(SomeCustomProperty) ?? true; } public bool SomeCustomProperty { get; set; } true; // binary serialization support public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info?.AddValue(SomeCustomProperty, SomeCustomProperty); } } // JSON serialization support public sealed class CustomCalendarSerializer : CalendarSerializerCustomCalendar { protected override CustomCalendar Create(JsonElement jsonElement, JsonSerializerOptions options) { return new CustomCalendar(); } protected override void SerializeFields(Utf8JsonWriter writer, CustomCalendar calendar, JsonSerializerOptions options) { writer.WriteBoolean(SomeCustomProperty, calendar.SomeCustomProperty); } protected override void DeserializeFields(CustomCalendar calendar, JsonElement jsonElement, JsonSerializerOptions options) { calendar.SomeCustomProperty jsonElement.GetProperty(CustomProperty).GetBoolean(); } public override string CalendarTypeName CustomCalendar; }结构拆解CalendarTypeName是写入/读取时的判别符discriminator写侧写入 JSON 负载读侧据此匹配对应的序列化器SystemTextJsonSerializerRegistry.GetCalendarSerializer按此名查找见 SystemTextJsonSerializerRegistry.cs。Create先构造一个空 Calendar 实例再由DeserializeFields把字段读入SerializeFields只负责写自定义字段——类型、描述、时区、基 Calendar 等公共字段由CalendarConverter统一处理见ICalendarSerializer接口注释同一文件。示例同时保留了二进制序列化所需的SerializationInfo构造函数与GetObjectData重写兼容从二进制格式迁移过来的场景。注意DeserializeFields中的jsonElement.GetProperty(CustomProperty)与写侧SomeCustomProperty字段名不一致——这是原文档示例的笔误实战中应保证两处字段名一致否则读取时会抛KeyNotFoundException。配置自定义 Calendar 序列化器var config SchedulerBuilder.Create(); config.UsePersistentStore(store { store.UseSystemTextJsonSerializer(json { json.AddCalendarSerializerCustomCalendar(new CustomCalendarSerializer()); }); }); // or just globally which is what above code calls SystemTextJsonObjectSerializer.AddCalendarSerializerCustomCalendar(new CustomCalendarSerializer());注册的两种途径局部注册推荐UseSystemTextJsonSerializer回调中的AddCalendarSerializerTCalendar把序列化器注册到该 Scheduler 专属的SystemTextJsonSerializerRegistry实例上见 SystemTextJsonConfigurationExtensions.cs 的注释注册表被闭包捕获而非发布到容器从而保证多个 Scheduler 不会互相污染自定义序列化器。全局注册文档注释说明上述代码最终等价于调用静态的SystemTextJsonObjectSerializer.AddCalendarSerializerTCalendar(...)。不过在 4.x 源码中该静态入口已演进为容器级注册表——注册一个新实例SystemTextJsonSerializerRegistry()为单例见 SystemTextJsonSamples.cs未传回调的UseSystemTextJsonSerializer()会自动读取容器注册表这样自定义序列化器同时作用于 JobStore、HTTP API 与 Dashboard而回调式注册则保持调度器级隔离PerSchedulerSerializers示例展示了两个 Scheduler 各自注册不同 Trigger 序列化器的用法见同一文件 L103-L120。按 4.x 现状使用回调式注册即可满足本节的配置自定义 Calendar 序列化器需求。七、触发器的自定义序列化与注册扩展阅读与原文档中 Calendar 自定义序列化对称4.x 还提供触发器自定义能力便于你在实践中完整落地自定义类型services.AddQuartz(q q.UsePersistentStore(store { store.UseSqlServer(my connection string); store.UseSystemTextJsonSerializer(json { json.AddCalendarSerializerCustomCalendar(new CustomCalendarSerializer()); json.AddTriggerSerializerCustomTrigger(new CustomTriggerSerializer()); }); }));自定义触发器序列化器继承TriggerSerializerTTriggerTriggerSerializer.cs它要求实现TriggerTypeName判别符、CreateScheduleBuilder与SerializeFields。内置序列化器SimpleTriggerSerializer、CronTriggerSerializer、CalendarIntervalTriggerSerializer、DailyTimeIntervalTriggerSerializer、RecurrenceTriggerSerializer刻意保持public且未密封自定义触发器若继承自内置触发器HasAdditionalProperties返回true其序列化器可同样继承内置序列化器仅重写SerializeFields/DeserializeFields并调用基类以保持内置字段的既有存储形状。SystemTextJsonSerializerRegistry构造时自动注册了上述 5 种内置 Trigger 与 7 种内置 CalendarBaseCalendar、AnnualCalendar、CronCalendar、DailyCalendar、HolidayCalendar、MonthlyCalendar、WeeklyCalendar因此自定义类型的注册是追加而非替换见 SystemTextJsonSerializerRegistry.cs。八、JobDataMap 值类型约束与 AOT / 裁剪场景理解 STJ 序列化在持久化存储中的行为还有一个关键约束值得掌握写入侧收口JobDataValues.RefuseJobDataValues.cs规定 JobDataMap 中可接受的值为——string、bool、char、int、long、float、double、decimal、DateTime、DateTimeOffset、TimeSpan、Guid、DateOnly、TimeOnly、枚举或Dictionarystring, string除此之外的类型如Liststring在写入前就会抛出JsonSerializationException避免写入成功、读回失败的坑。结构化的自有对象应由 Job 自行序列化为字符串再存储。这也是文档开头建议开启useProperties的深层原因。读侧封闭读侧只能还原出字符串、布尔、int/long/double、null 或Dictionarystring, string见JobDataValues.Read同一文件 L173-L217与写侧形成闭环。AOT / 裁剪发布PublishTrimmed或PublishAot会关闭反射序列化此时由QuartzStoreJsonContextQuartzStoreJsonContext.cs一个[JsonSourceGenerationOptions(GenerationMode JsonSourceGenerationMode.Metadata)]标记的JsonSerializerContext为存储所需的所有类型提供编译期元数据应用自有 JobData 值类型则通过json.AddTypeInfoResolver(JobDataContext.Default)声明示例见 SystemTextJsonSamples.cs。自定义 Trigger / Calendar 类型由注册表静态已知类型自动应答无需额外声明。九、小结以 docs/documentation/quartz-3.x/packages/system-text-json.md 为骨架结合当前仓库源码可以确认JSON 是当前持久化存储的推荐格式4.x 中SystemTextJsonObjectSerializer已内置进Quartz主包并成为默认独立包保留为空壳以支持分组依赖升级两种配置入口属性式quartz.serializer.type stj由 QuartzPropertyBridge 解析与 SchedulerBuilder 链式配置UseSystemTextJsonSerializer()后者推荐配合UseProperties true二进制迁移采用混合序列化器 脏标记强制重写策略渐进完成ForceJobDataMapDirtyQRTZ_FORCE_JOB_DATAMAP_DIRTY是触发 BLOB 重写升级的关键自定义扩展统一通过重写CreateSerializerOptions全局选项、继承CalendarSerializerTCalendar/TriggerSerializerTTrigger并注册到SystemTextJsonSerializerRegistry类型判别符驱动读写两条路径完成注册可做到调度器级隔离或容器级共享。如需深入阅读可继续查看SystemTextJsonSerializerRegistry.cs、JobDataValues.cs、SystemTextJsonObjectSerializer.cs、配套示例 SystemTextJsonSamples.cs以及 4.x 文档目录 docs/documentation/quartz-4.x。赞分享任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载相关推荐LokiJS 持久化适配器完全指南从内存数据库到磁盘、IndexedDB 与自定义存储的序列化方案LokiJS 持久化适配器完全指南从内存数据库到磁盘、IndexedDB 与自定义存储的序列化方案 LokiJS 是一款 JavaScript 嵌入式内存数据数据库后端CommentCoreLibrary数据格式完全指南AcFun、Bilibili、CommonDanmaku格式解析CommentCoreLibrary数据格式完全指南AcFun、Bilibili、CommonDanmaku格式解析 CommentCoreLibrary是一音视频前端boardgame.io 存储适配层完全指南从 FlatFile 到自定义持久化适配器boardgame.io 存储适配层完全指南从 FlatFile 到自定义持久化适配器 导读 boardgame.io 是一个面向回合制游戏的状态管理 游戏开发上一篇深度解析FunASR多线程并发架构5大关键技术提升语音识别吞吐量300%下一篇掌握Devbox微服务依赖管理服务间通信配置的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进