ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Quartz.NET 4.x JobDataMap 完全指南:合并规则、类型化访问器与持久化存储

Quartz.NET 4.x JobDataMap 完全指南:合并规则、类型化访问器与持久化存储 任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载JobDataMap是 Quartz.NET 中把输入数据从调度定义传送到IJob执行的官方通道它既可以挂在IJobDetail上让所有触发共享也可以挂在ITrigger上让同一任务在不同时刻携带不同输入。本文以 Quartz.NET 4.x 官方教程 job-data-map.md 为主线结合仓库源码src/Quartz/JobDataMap.cs、src/Quartz/DataMapExtensions.cs等深入讲解合并优先级、17 个类型化访问器与 3 个泛型读取器的转换规则、PutAsString的字符串格式、StoreJobDataAsStrings字符串模式、属性注入、IJobTInput类型化输入以及跨次执行的状态持久化最后给出什么不该放进 job data的安全红线。读完本文你将能设计出可迁移、可序列化、线程安全的 Quartz.NET 任务数据方案。两张 Map 与一次合并Quartz.NET 中存在两张相互独立的JobDataMap各自存放于不同的调度对象上Map存放位置用途IJobDetail.JobDataMap任务job无论哪个触发器触发任务每次都看到同一份数据ITrigger.JobDataMap触发器trigger多个触发器驱动同一个任务时各自携带不同的输入这一设计的核心价值在于一个任务可以被多个触发器以不同参数驱动而无需复制任务定义。源码层面的佐证见 JobDataMap.cs 的类注释任务数据在任务加入调度器时存储一次带PersistJobDataAfterExecutionAttribute的任务则在每次执行后重新持久化触发器的数据用于同一个任务、多次独立触发、每次提供不同输入的场景。MergedJobDataMap合并副本触发器胜出IJobExecutionContext.MergedJobDataMap是任务的 Map 叠加触发器的 Map之后的结果对于相同键触发器优先trigger wins。它采用惰性构建每次触发firing只构建一次。任务应当读取它public sealed class ReportJob : IJob { public ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken default) { JobDataMap data context.MergedJobDataMap; string region data.GetString(region)!; int lookbackDays data.GetInt(lookbackDays); // ... return default; } }合并后的 Map 是每次触发独立的副本向它写入的值不会回写任务的原始 Map。若要在多次执行间持久化状态必须使用[PersistJobDataAfterExecution]作用于任务自身的 Map见下文跨次执行持久化。从实现上看MergedJobDataMap在 JobExecutionContextImpl.cs 中构建每次触发克隆任务与触发器的 Map 并叠加因此并发触发之间天然隔离见线程安全一节。4.x 变更警告SchedulerContext 不再参与合并::: warning Changed in 4.x 在 3.x 中context.MergedJobDataMap还会携带SchedulerContext中的全部条目一个调度器级别的键可能静默遮蔽或被遮蔽任务自身的键。4.x 起合并只是任务在上、触发器在上、别无其他SchedulerContext的值请通过context.Scheduler.Context读取。 :::这一变更同样体现在任务工厂侧PropertySettingJobFactory.BuildJobDataMap在 4.0 之前会把调度器上下文一并合并进去导致 DI 容器注入的IServiceProvider条目被塞进每一次触发PropertySettingJobFactory.cs。4.x 的默认实现只合并任务与触发器两张 Map。写入数据UsingJobData 的三种形态JobBuilderTJob与TriggerBuilderTJob提供完全相同的三种UsingJobData写法IJobDetail job JobBuilder.CreateReportJob() .WithIdentity(nightly, reports) .UsingJobData(region, emea) // key 和 value .UsingJobData(j j.LookbackDays, 30) // 用属性名做 key不用手写字符串 .UsingJobData(existingMap) // 合并一整张 map .Build();其中UsingJobData(j j.LookbackDays, 30)是 4.x 推荐的强类型写法用属性名作为键、用属性类型推断值的类型。属性一旦重命名或改类型编译期就会报错而不是在触发时静默无效silent no-op。它与下文属性注入配合使用命名属性的同时另一侧由PropertySettingJobFactory把同名键注入属性。单次触发数据无需触发器临时手动触发任务时数据可以直接通过TriggerJob传入不需要为此专门创建一个触发器await scheduler.TriggerJob(jobKey, new JobDataMap { [reason] manual re-run }, cancellationToken);这类数据只对本次触发生效体现了MergedJobDataMap每次触发独立构建的设计。读取数据类型化访问器JobDataMap实现了IDictionarystring, object?支持索引器、TryGetValue、ContainsKey、Remove、Count、Keys、Values、Clear此外还有ContainsValue与IsEmpty。除此之外它还通过 DataMapExtensions.cs 提供17 个类型化访问器两个扩展块读取行为完全一致。七种命名类型抛异常族与 Try 族家族成员键缺失或值不可读时抛异常读取器GetInt、GetLong、GetFloat、GetDouble、GetBoolean、GetString、GetDateTimeOffset抛异常Try 读取器TryGetInt、TryGetLong、TryGetFloat、TryGetDouble、TryGetBoolean、TryGetString、TryGetDateTimeOffset返回false每个访问器都接受值本身类型或不可变区域设置invariant-culture的字符串两种存储形态转换顺序固定为三步先匹配已存储的类型stored type若是字符串则用CultureInfo.InvariantCulture解析其他任何存储类型回退到Convert语义。因此同一段任务代码无论存储里保留的是int类型的30还是字符串30读出来的结果一致。这一点在StoreJobDataAsStrings true字符串模式下尤其关键——所有值都以字符串落库访问器必须能解析它们。两个细节GetString在键缺失时返回string?即null而不是抛异常其余访问器抛异常。当键是可选时使用TryGet…形态。两种形态没有性能差异——TryGetInt并不比GetInt快它们只是错误处理方式不同。从源码看这两族访问器只是共享转换核心coercion core的一行桥GetInt内部调用TryCoerceIntOrThrow把TryGetValue的结果交给同一套TryCoerceInt逻辑DataMapExtensions.cs。实现刻意声明在具体的JobDataMap与SchedulerContext类型上而非IReadOnlyDictionarystring, object?接口上以免把扩展方法嫁接到任何using Quartz;文件里的所有字符串键字典上。三个泛型读取器覆盖其余一切类型对于七个命名类型未覆盖的类型Guid、TimeSpan、decimal、DateOnly、枚举、自定义类使用泛型读取器访问器条目缺失条目无法读为T条目可读为TTryGetT(key, out T value)falsefalsetruevalue 输出GetT(key)抛KeyNotFoundException抛InvalidCastException同时点名两种类型返回值GetValueOrDefaultT(key, defaultValue)返回defaultValue返回defaultValue返回值可读readable的定义先匹配已存储类型然后尝试 Quartz 为每个PutAsString可写类型定义的不可变字符串形式枚举额外支持不区分大小写按名称解析。例如GetGuid(batchId)能读取以字符串形式存储的GuidGetTimeSpan(window)能读取06:00:00GetDayOfWeek(day)能读取Monday。没有任何 Quartz 字符串形式的类型比如自定义类则退化为纯粹的类型测试。// 条目缺失、条目是其他类型时都返回 false if (data.TryGetReportOptions(options, out ReportOptions? options)) { // ... } // 缺失抛 KeyNotFoundException、类型不对抛 InvalidCastException —— // 两种错误被明确区分而 TryGet 对两者都只回答 false ReportOptions required data.GetReportOptions(options); // 不抛异常也不区分缺失和类型错误都返回回退值 ReportOptions effective data.GetValueOrDefault(options, new ReportOptions());使用建议条目必须存在时用GetT它能把缺失和类型不对两种错误区分开。不要在必须察觉类型错误的地方使用GetValueOrDefaultT它对两者一律返回回退值会掩盖问题。源码中的TryCoerceT顺序与命名访问器完全一致已存储类型优先省去不必要的解析无字符串形式的类型落到类型测试DataMapExtensions.cs。值得注意的是TryParseAsT用一串typeof(T) 编译期折叠比较而非反射实现因此GetGuid编译后只剩下 Guid 分支天然支持裁剪trimming与原生 AOTDataMapExtensions.cs。::: tipSchedulerContext拥有相同的读取器同一个扩展块还提供了对SchedulerContext的 17 个访问器。但PutAsString写入器只属于JobDataMap因为它参与 Map 的变更跟踪dirty tracking——写入任务 Map 不只是存一个值还会把该 Map 标记为已变更这正是调度器决定是否在[PersistJobDataAfterExecution]任务执行后持久化它的依据DataMapExtensions.cs。SchedulerContext是进程内状态、没有任何存储会回写它所以它只有读访问器。 :::4.x 访问器迁移对照::: warning Changed in 4.xGet*Value/Get*ValueFromString访问器对已移除可空访问器GetNullableInt等也已移除。每个类型现在只有一对Get…/TryGet…。访问器还从StringKeyDirtyFlagMap上移走——该类型连同DirtyFlagMap一起变为 internalQuartz.Util命名空间不复存在。调用点无需改动map.GetString(…)依然可编译但不应再指名任何旧类型。 :::较少见的命名访问器统一收敛为GetT由它执行转换3.x4.xGetGuidGetGuidGetTimeSpanGetTimeSpanGetDecimalGetdecimalGetCharGetcharGetDateTimeGetDateTimeGetDateOnlyGetDateOnlyGetTimeOnlyGetTimeOnlyGetEnumTGetT每个TryGet…的迁移方式相同。读取行为不变包括PutAsString写出的字符串形式。以字符串形式存储PutAsStringPutAsString把值写成任何存储都能保存的字符串形式JobDataMap data new(); data.PutAsString(runAt, DateTimeOffset.UtcNow); // O 格式2026-08-22T09:15:00.000000000:00 data.PutAsString(window, TimeSpan.FromHours(6)); // 不可变 06:00:00 data.PutAsString(batchId, Guid.NewGuid()); data.PutAsString(lookbackDays, 30); // 任何 IFormattable各重载的写入格式重载写入形式PutAsString(string, DateTime)round-tripO不可变区域设置PutAsString(string, DateTimeOffset)round-tripO不可变区域设置PutAsString(string, DateOnly)round-tripOyyyy-MM-ddPutAsString(string, TimeOnly)round-tripOPutAsString(string, TimeSpan)不可变默认格式PutAsString(string, Guid)不可变默认格式PutAsString(string, bool)不可变默认格式PutAsString(string, char)不可变默认格式PutAsStringT(string, T) where T : IFormattable不可变默认格式每个格式都能通过对应的访问器往返读取PutAsString(runAt, offset)之后调用GetDateTimeOffset(runAt)得到相同的时刻与偏移量。GetDateTime以 round-trip 语义解析因此以O写入的DateTime会保留其原始Kind而不是变成未指定的本地时间。GetGuid、GetTimeSpan、GetDateOnly、GetTimeOnly均能读回PutAsString写出的内容。源码层面PutAsString是JobDataMap的实例成员JobDataMap.cs而非扩展方法——因为写入必须参与脏标记。两个设计细节值得注意泛型重载约束是IFormattable而非旧的IConvertible这让所有现代数值类型包括未实现IConvertible的类型都能写入bool和char不是IFormattable因此各有专属重载。Guid以N格式写出省略连字符。为什么字符串安全存储很重要持久化存储persistent store读回任务数据有两种路径序列化器或字符串模式。序列化器路径类型即版本承诺默认情况下持久化存储会序列化整张 Map。此时必须遵守每个值都必须能被所配置的序列化器序列化。类型形状变化后必须仍可读存储的 options 类一旦重命名属性任务下一次触发就会抛异常——这可能在部署几个月之后才发生。标准框架类型是安全的你自己的类型是一种版本化承诺。持久化存储只接受访问器覆盖的类型string、bool、char、各数值类型、DateTime、DateTimeOffset、TimeSpan、Guid、DateOnly、TimeOnly、枚举外加Dictionarystring, string。存储任务时遇到其他类型直接拒绝而不是写下一个将来加载失败的 blob。两套序列化器System.Text.Json 与 Newtonsoft拒绝同一集合、以同一方式写入因此可以相互切换。字符串 Map 的条目不能使用$type这个键名Json.NET 会把值的类型信息写在那里两个读取器都把它当作元数据含此键名的 Map 会被拒绝。要存储自定义类型三条路任选其一在默认序列化器上通过SystemTextJsonSerializerRegistry.AddTypeInfoResolver声明这也是裁剪/原生 AOT 发布所需的同一注册在 Newtonsoft 序列化器上通过NewtonsoftJsonSerializerRegistry.AddJobDataValueTypeT()声明自己序列化然后以字符串形式存储。注意已声明的类型只能由写它的那套序列化器读回所以存字符串才是可移植的选择。字符串模式StoreJobDataAsStringsAdoJobStoreOptions.StoreJobDataAsStrings扁平配置键quartz.jobStore.useProperties把 Map 存成键/值字符串对而不是序列化 blob。其全部存储选项见 配置参考。配置示例q.UsePersistentStore(s { s.UseSqlServer(connectionString); s.ConfigureStore(o o.StoreJobDataAsStrings true); });字符串模式消除版本化问题并让QRTZ_JOB_DETAILS.JOB_DATA在查询工具中直接可读。但规则也随之而来每个值都必须是字符串。在字符串模式下存储带DateTimeOffset的 Map 会失败——请用PutAsString写入访问器在两种模式下读取行为一致。::: tipStoreJobDataAsStrings应该在项目初期就打开而不是中途切换。在表里已有数据的情况下切换开关会留下存储读不出来的行。 :::属性注入另一条读取通道如果任务类具有与合并 Map 中键同名的可设置属性默认任务工厂会在Execute运行之前完成注入public sealed class ReportJob : IJob { public string Region { get; set; } ; public int LookbackDays { get; set; } public ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken default) { // Region 和 LookbackDays 已被注入完成 return default; } }要点转换遵循访问器的规则Map 中的30可以设置int LookbackDays。键没有匹配属性、或值无法转换时行为由PropertySettingJobFactory.PropertyMismatchBehavior决定Ignore、Warn或Throw。开发阶段建议Warn否则属性会静默停留在默认值。UsingJobData(j j.LookbackDays, 30)是与之配对的写入侧命名属性得到键。源码佐证PropertySettingJobFactory的ApplyProperties使用BuildJobDataMap构建合并 Map 后调用SetObjectProperties通过反射按属性名首字母自动大写查找 setter 并做类型转换PropertySettingJobFactory.cs。PropertyMismatchBehavior默认值为Ignore——因为 Map 允许携带任务自己读取的额外值。工厂注释还特别提醒自 4.0 起合并 Map 不再包含SchedulerContext否则 DI 注入的服务提供者条目会让Throw模式下的每次容器托管触发都失败。类型化输入第三条读取通道IJob 如果一个任务的数据本质上是一个载荷一条消息、一个命令、一个事件可以让任务声明IJobTInput载荷直接以参数到达public sealed record SendEmail(string To, string Subject); public sealed class SendEmailJob : IJobSendEmail { public ValueTask Execute(IJobExecutionContext context, SendEmail input, CancellationToken cancellationToken default) { // input.To, input.Subject —— 没有键、没有访问器、没有转换 return default; } } public static class TypedInputScheduling { public static async ValueTask Schedule(IScheduler scheduler, CancellationToken cancellationToken) { await scheduler.ScheduleJob( JobBuilder.CreateSendEmailJob() .WithIdentity(welcome, email) .Build(), TriggerBuilder.CreateSendEmailJob() .WithIdentity(welcome-3401, email) .StartNow() .UsingInput(new SendEmail(someoneexample.org, Welcome)) .Build(), cancellationToken: cancellationToken); } }行为细节UsingInput出现在JobBuilderTJob、TriggerBuilderTJob以及AddJob/AddTrigger配置器上。它只对有输入声明的任务开放用在其他任务上是编译错误JobInputBuilderExtensions.cs中where TJob : IJobTInput约束由编译器强制。值存放在普通JobDataMap的保留键SchedulerConstants.JobInputQRTZ_JOB_INPUT下SchedulerConstants.cs。任务或触发器被存储时调度器把它序列化为字符串因此它能挺过StoreJobDataAsStrings、序列化器的类型检查、blob 列与 HTTP API。触发器上的输入覆盖任务上的输入。非IJobTInput的任务可用context.GetInputSendEmail()读取载荷无输入时返回nullTryGetInputTInput还能区分载荷反序列化为 null与完全没有载荷两种情形JobInput.cs。输入缺失时IJobTInput任务会以一条指名键的SchedulerException使本次触发失败而不是用默认载荷运行JobInput.cs。输入类型从实参推断以基类类型持有的payload会按基类存储和读取。当静态类型不是你想表达的类型时请显式给出类型参数UsingInputSendEmailJob, SendEmail(payload)。每次触发的输入放在触发器上[PersistJobDataAfterExecution]任务每次触发后会重存自己的 Map放在任务上的输入会被反复回写——这无害它已是字符串但按触发区分输入的需求应由触发器承载。调度器的IJobInputSerializer负责写出载荷。默认实现是SystemTextJsonJobInputSerializer基于与存储序列化器相同的注册表构建详见 JSON 序列化。裁剪/原生 AOT 应用需用SystemTextJsonSerializerRegistry.AddTypeInfoResolver声明载荷类型与声明 job data 值类型一致。源码结构补充IJobTInput之所以是带默认接口方法的接口而不是基类是因为运行时所有路径任务工厂、JobRunShell、监听器看到的都是IJobTInput只存在于IJob.Execute的默认实现中从那里以运行时类型做泛型分发是裁剪/原生编译无法支持的IJob.cs。跨次执行持久化状态默认情况下任务的存储 Map 写一次、读多次。加上[PersistJobDataAfterExecution]后任务自身的JobDataMap会在每次执行后保存计数器或水位线因此得以存活[PersistJobDataAfterExecution] [DisallowConcurrentExecution] public sealed class IncrementalSyncJob : IJob { public ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken default) { JobDataMap data context.JobDetail.JobDataMap; data.PutAsString(lastSyncedAt, DateTimeOffset.UtcNow); return default; } }务必同时加上[DisallowConcurrentExecution]。否则两个并发触发会读到同一张 Map、各自写入其中一次写入会丢失。Map 会跟踪变更dirty tracking只在真正变更时才写库。若发生了 Map 无法检测的原地修改比如就地改变了某个已存储对象的内容需要强制写库可设置保留键data[SchedulerConstants.ForceJobDataMapDirty] true;该保留键常量为QRTZ_FORCE_JOB_DATAMAP_DIRTYSchedulerConstants.cs。底层实现JobDataMap内部由DirtyFlagMap支撑PutAsString等写入会置脏从已有 Map 构造新 Map 时则清除脏标记这是任务存储从数据库加载数据时的常规路径JobDataMap.cs。4.0 起JobDataMap.Equals比较键与值而非 3.x 只比较键集因此赋值一个内容不同但键相同的嵌套 Map也会正确标记外层 Map 脏避免静默跳过存储重写JobDataMap.cs。线程安全JobDataMap不是线程安全的。没有[DisallowConcurrentExecution]的任务可能同时运行多个触发而这些触发共享存储的IJobDetail上的 Map。并发读取没问题但不要从可能与自己并发运行的任务中修改它。每次触发获得独立的MergedJobDataMap因此每次执行的数据天然隔离——临时数据写进合并 Map 不会泄漏到其他触发。什么不该放进 JobDataMapJob data 是持久化的在持久化存储上它存在于QRTZ_JOB_DETAILS.JOB_DATA与QRTZ_TRIGGERS.JOB_DATA、每一份备份、已触发历史、以及任何能读取任务详情的人都能看到的 Dashboard 与 HTTP API 中。以下内容绝对不要放进去凭据、令牌、连接字符串应注册到容器DI中。官方自带的SendMailJob就是范例——它的选项类型刻意不包含用户名与密码字段。参见 把 SMTP 凭据挡在 Job Data 之外该模式适用于任何需要密钥的任务。具体做法是把凭据以CredentialCache绑定到具体服务器注册为ICredentialsByHost单例smtp_host本身作为 job data 由调度方决定quartz-jobs.md。大载荷Job data 每次触发都要读取在[PersistJobDataAfterExecution]下每次触发还要写入。正确做法是存一个标识符由任务在运行时取回载荷。活对象DbConnection、HttpClient、logger 这类应该通过构造函数从容器注入任务而不是塞进 Map。一句话原则Job data 只存放用来区分一个调度实例与另一个调度实例的输入。小结Quartz.NET 4.x 的JobDataMap体系可以概括为四个读写层次写入UsingJobData三形态键值对、属性表达式、整 Map 合并PutAsString负责写出任何存储都能保存的字符串形式。读取7 对命名访问器 3 个泛型读取器统一执行先类型、再 invariant 字符串、最后Convert的三步转换GetT区分缺失与类型错误。进阶通道属性注入PropertySettingJobFactory与类型化输入IJobTInputUsingInput后者以保留键QRTZ_JOB_INPUT存于普通 Map天然兼容字符串模式与 HTTP API。持久化与安全[PersistJobDataAfterExecution][DisallowConcurrentExecution]保存跨次状态StoreJobDataAsStrings在项目初期启用凭据、大载荷与活对象一律不进 Map。更多相关阅读More About JobsJobDataMap 引入处、配置参考StoreJobDataAsStrings及全部存储选项、JSON 序列化AddTypeInfoResolver注册。赞分享任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载相关推荐Quartz.NET 4.x JSON 序列化完全指南Newtonsoft 接入、JobDataMap 类型边界与二进制迁移实战Quartz.NET 4.x JSON 序列化完全指南Newtonsoft 接入、JobDataMap 类型边界与二进制迁移实战 JSON 是 Quartz.任务调度后端Quartz.NET 4.x 配置参考强类型选项、持久化 Job Store 与旧版 quartz.* 键迁移完全指南Quartz.NET 4.x 配置参考强类型选项、持久化 Job Store 与旧版 quartz. 键迁移完全指南 本篇技术指南以 Quartz.NET 4任务调度后端Quartz.NET 作业存储Job Stores完全指南RAMJobStore 与 ADO.NET 持久化存储实战Quartz.NET 作业存储Job Stores完全指南RAMJobStore 与 ADO.NET 持久化存储实战 Job Store 是 Quartz任务调度后端上一篇使用Docker部署exatorrent私有文件服务器指南下一篇Path of Building PoE2流放之路2角色构建与伤害计算的终极解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表