- 任务调度
- 后端
【免费下载链接】quartznet
Quartz Enterprise Scheduler .NET
导读:本文聚焦 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 序列化:从二进制到 JSON
Quartz.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.Json
3.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.type | Quartz.Impl.AdoJobStore.JobStoreTX, Quartz | 使用基于 ADO.NET 的事务型 JobStore(也可用JobStoreCMT或LocalTransactionJobStore) |
quartz.serializer.type | stj | 指定 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 => { // it's 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 DeSerialize<T>(byte[] data) where T : class { try { // Attempt to deserialize data as JSON return jsonSerializer.DeSerialize<T>(data)!; } catch (JsonException) { // Presumably, the data was not JSON, we instead use the binary serializer var binaryData = binarySerializer.DeSerialize<T>(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[] Serialize<T>(T obj) where T : class { return jsonSerializer.Serialize(obj); } }工作原理与关键细节:
IObjectSerializer是序列化器契约(Serialize<T>/Deserialize<T>两个方法,见 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.UseSerializer<CustomJsonSerializer>(); // or "quartz.serializer.type" = "MyProject.CustomJsonSerializer, MyProject"源码级佐证:SystemTextJsonObjectSerializer(SystemTextJsonObjectSerializer.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实现。官方基类CalendarSerializer<TCalendar>让序列化器保持强类型:类型参数在编译期即与 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 : CalendarSerializer<CustomCalendar> { 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.AddCalendarSerializer<CustomCalendar>(new CustomCalendarSerializer()); }); }); // or just globally which is what above code calls SystemTextJsonObjectSerializer.AddCalendarSerializer<CustomCalendar>(new CustomCalendarSerializer());注册的两种途径:
- 局部注册(推荐):
UseSystemTextJsonSerializer回调中的AddCalendarSerializer<TCalendar>把序列化器注册到该 Scheduler 专属的SystemTextJsonSerializerRegistry实例上(见 SystemTextJsonConfigurationExtensions.cs 的注释:注册表被闭包捕获而非发布到容器,从而保证多个 Scheduler 不会互相污染自定义序列化器)。 - 全局注册:文档注释说明上述代码最终等价于调用静态的
SystemTextJsonObjectSerializer.AddCalendarSerializer<TCalendar>(...)。不过在 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.AddCalendarSerializer<CustomCalendar>(new CustomCalendarSerializer()); json.AddTriggerSerializer<CustomTrigger>(new CustomTriggerSerializer()); }); }));自定义触发器序列化器继承TriggerSerializer<TTrigger>(TriggerSerializer.cs),它要求实现TriggerTypeName判别符、CreateScheduleBuilder与SerializeFields。内置序列化器(SimpleTriggerSerializer、CronTriggerSerializer、CalendarIntervalTriggerSerializer、DailyTimeIntervalTriggerSerializer、RecurrenceTriggerSerializer)刻意保持public且未密封:自定义触发器若继承自内置触发器(HasAdditionalProperties返回true),其序列化器可同样继承内置序列化器,仅重写SerializeFields/DeserializeFields并调用基类,以保持内置字段的既有存储形状。SystemTextJsonSerializerRegistry构造时自动注册了上述 5 种内置 Trigger 与 7 种内置 Calendar(BaseCalendar、AnnualCalendar、CronCalendar、DailyCalendar、HolidayCalendar、MonthlyCalendar、WeeklyCalendar),因此自定义类型的注册是"追加"而非"替换"(见 SystemTextJsonSerializerRegistry.cs)。
八、JobDataMap 值类型约束与 AOT / 裁剪场景
理解 STJ 序列化在持久化存储中的行为,还有一个关键约束值得掌握:
- 写入侧收口:
JobDataValues.Refuse(JobDataValues.cs)规定 JobDataMap 中可接受的值为——string、bool、char、int、long、float、double、decimal、DateTime、DateTimeOffset、TimeSpan、Guid、DateOnly、TimeOnly、枚举,或Dictionary<string, string>;除此之外的类型(如List<string>)在写入前就会抛出JsonSerializationException,避免"写入成功、读回失败"的坑。结构化的自有对象应由 Job 自行序列化为字符串再存储。这也是文档开头建议开启useProperties的深层原因。 - 读侧封闭:读侧只能还原出字符串、布尔、int/long/double、null 或
Dictionary<string, string>(见JobDataValues.Read,同一文件 L173-L217),与写侧形成闭环。 - AOT / 裁剪发布:
PublishTrimmed或PublishAot会关闭反射序列化,此时由QuartzStoreJsonContext(QuartzStoreJsonContext.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; - 二进制迁移采用"混合序列化器 + 脏标记强制重写"策略渐进完成,
ForceJobDataMapDirty(QRTZ_FORCE_JOB_DATAMAP_DIRTY)是触发 BLOB 重写升级的关键; - 自定义扩展统一通过重写
CreateSerializerOptions(全局选项)、继承CalendarSerializer<TCalendar>/TriggerSerializer<TTrigger>并注册到SystemTextJsonSerializerRegistry(类型判别符驱动读写)两条路径完成,注册可做到调度器级隔离或容器级共享。
如需深入阅读,可继续查看:SystemTextJsonSerializerRegistry.cs、JobDataValues.cs、SystemTextJsonObjectSerializer.cs、配套示例 SystemTextJsonSamples.cs,以及 4.x 文档目录 docs/documentation/quartz-4.x。
- 任务调度
- 后端
【免费下载链接】quartznet
Quartz Enterprise Scheduler .NET
相关推荐
LokiJS 持久化适配器完全指南:从内存数据库到磁盘、IndexedDB 与自定义存储的序列化方案
LokiJS 持久化适配器完全指南:从内存数据库到磁盘、IndexedDB 与自定义存储的序列化方案 LokiJS 是一款 JavaScript 嵌入式内存数据
数据库后端CommentCoreLibrary数据格式完全指南:AcFun、Bilibili、CommonDanmaku格式解析
CommentCoreLibrary数据格式完全指南:AcFun、Bilibili、CommonDanmaku格式解析 CommentCoreLibrary是一
音视频前端boardgame.io 存储适配层完全指南:从 FlatFile 到自定义持久化适配器
boardgame.io 存储适配层完全指南:从 FlatFile 到自定义持久化适配器 导读 boardgame.io 是一个面向回合制游戏的"状态管理 +
游戏开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考