介绍
上一篇我们已经在 HQServer 中完成了 Quartz.NET 定时任务的基础集成,把配置读取、任务定义、任务注册和 HostedService 启动统一封装到了 HQ.Common 层。
第一版可以正常使用,但它更偏向“配置驱动”:新增任务时,除了要写一个实现 IJob 的任务类,还需要在配置文件中维护任务名称、任务类型、Cron 表达式等信息。任务少的时候问题不大,一旦任务多起来,配置文件会越来越长,类型名也容易写错。
所以这一篇不是重新从零集成 Quartz,而是接着上一篇,对第一篇的代码做一次优化改造:把原来的配置式任务注册,升级为注解/特性驱动注册。后续新增任务时,只需要在任务类上标记 [QuartzJob],框架启动时就会自动扫描并注册。
为什么要优化注册方式
第一篇的基础封装解决了“能统一接入 Quartz”的问题,但在日常开发中还会遇到几个维护点:
- 新增任务时,需要同时修改任务类和配置文件。
- 配置文件中的任务类型字符串容易写错,运行前不够直观。
- 任务规则和任务代码分散在两个地方,阅读成本偏高。
- 任务越来越多后,配置文件会变成任务清单,不利于维护。
- 一些固定任务本来就和代码绑定,没必要每次都靠配置清单声明。
因此第二篇的优化方向很明确:让任务类自己携带调度元数据,配置文件只保留 Quartz 模块级开关和基础选项。
优化后的目标
- 保留第一篇的 Quartz 基础封装和统一注册入口。
- 新增
QuartzJobAttribute,让任务类通过注解声明调度规则。 - 新增
PreferAttributeRegistration,明确默认采用注解注册。 - 新增
AutoRegister,允许个别任务关闭自动注册。 - 启动时扫描程序集中的
IJob实现类。 - 只自动注册标记了
[QuartzJob]且启用的任务。 - 启动阶段校验 Cron 表达式,尽早发现配置错误。
调整 QuartzOptions
第一步先调整 Quartz 基础配置。这里不再把具体 Jobs 列表作为主要注册入口,而是保留模块开关和注册偏好:
namespace HQ.Common.Scheduling;
public class QuartzOptions
{
public bool Enabled { get; set; } = true;
public bool PreferAttributeRegistration { get; set; } = true;
}
PreferAttributeRegistration 表示框架优先使用注解注册方式。这样配置文件不再承担任务清单职责,而是只控制 Quartz 是否启用以及基础行为。
新增 QuartzJobAttribute
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class QuartzJobAttribute : Attribute
{
public QuartzJobAttribute(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("任务名称不能为空", nameof(name));
Name = name;
}
public string Name { get; }
public string? Group { get; set; }
public string? Cron { get; set; }
public string? Description { get; set; }
public bool Enabled { get; set; } = true;
public bool AutoRegister { get; set; } = true;
public bool RunOnStart { get; set; } = false;
}
其中 AutoRegister 用来控制任务是否参与自动扫描注册。大多数业务任务保持默认值即可,如果某个任务需要手动注册或临时排除,可以设置为 false。
扫描带注解的任务类
第一篇中任务来源主要依赖配置。优化后,任务来源改为扫描程序集中的 IJob 实现类,并读取类上的 QuartzJobAttribute:
var jobTypes = assemblies
.Where(a => a != null)
.Distinct()
.SelectMany(a => a.GetTypes())
.Where(t => t.IsClass && !t.IsAbstract && typeof(IJob).IsAssignableFrom(t));
foreach (var jobType in jobTypes)
{
var attribute = jobType.GetCustomAttribute<QuartzJobAttribute>();
if (attribute == null || !attribute.Enabled || !attribute.AutoRegister)
continue;
if (string.IsNullOrWhiteSpace(attribute.Cron))
throw new InvalidOperationException($"Quartz 任务 {attribute.Name} 未配置 Cron 表达式");
if (!CronExpression.IsValidExpression(attribute.Cron))
throw new InvalidOperationException($"Quartz 任务 {attribute.Name} 的 Cron 表达式无效:{attribute.Cron}");
definitions.Add(new QuartzJobDefinition
{
Name = attribute.Name,
Group = attribute.Group,
JobType = jobType,
Cron = attribute.Cron,
Description = attribute.Description,
RunOnStart = attribute.RunOnStart
});
}
这样做以后,任务类、任务名称、Cron 表达式、任务描述都放在一起,代码阅读时更加直观。启动阶段也会校验 Cron 表达式,避免任务运行后才发现规则写错。
统一注册入口保持不变
外部使用方式仍然保持第一篇的思路,通过 AddHQQuartz 统一接入。只是内部的任务发现方式从配置清单优化成了注解扫描:
builder.Services.AddHQQuartz(builder.Configuration, typeof(Program).Assembly);
这里传入 typeof(Program).Assembly,表示扫描当前应用程序集中的任务类。如果后续任务分布在多个程序集,也可以继续传入更多程序集。
appsettings 简化
优化后,配置文件中不再需要维护具体 Jobs 列表,只保留基础开关:
"Quartz": {
"Enabled": true,
"PreferAttributeRegistration": true
}
这也是第二篇优化的核心:任务调度规则跟随任务类走,而不是集中堆在配置文件中。
改造 TestQuartzJob
using HQ.Common.Scheduling;
using Quartz;
namespace HQ.Application;
[QuartzJob(
"TestQuartzJob",
Group = "Default",
Cron = "0/30 * * * * ?",
Description = "Quartz基础测试任务,每30秒执行一次",
RunOnStart = true)]
public class TestQuartzJob : IJob
{
private readonly ILogger<TestQuartzJob> _logger;
public TestQuartzJob(ILogger<TestQuartzJob> logger)
{
_logger = logger;
}
public Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Quartz 测试任务执行成功,执行时间:{ExecuteTime}", DateTimeOffset.Now);
return Task.CompletedTask;
}
}
现在任务类本身就能说明:它叫什么、属于哪个分组、多久执行一次、是否启动后立即执行。后续维护时,不需要再跳到配置文件中找任务规则。
改造前后对比
- 改造前:任务类只负责执行逻辑,任务名称、任务类型、Cron 表达式放在配置文件中。
- 改造后:任务类同时包含执行逻辑和调度元数据,通过
[QuartzJob]自动参与注册。 - 改造前:新增任务需要同时改代码和配置。
- 改造后:新增任务只需要新增
IJob类并标记注解。 - 改造前:配置文件会随着任务数量增加而变长。
- 改造后:配置文件只保留 Quartz 基础开关。
使用方式
[QuartzJob(
"ReportGenerateJob",
Group = "Report",
Cron = "0 0 1 * * ?",
Description = "每天凌晨1点生成业务报表")]
public class ReportGenerateJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
// 生成业务报表
return Task.CompletedTask;
}
}
框架启动时会自动扫描到这个任务,并按 Cron 表达式注册到 Quartz 调度器中。
总结
这一篇是对上一篇 Quartz 基础集成的优化改造,不是重新集成一套新的调度体系。第一篇解决的是“如何把 Quartz.NET 接入 HQServer 并统一封装”,这一篇解决的是“如何让任务注册更简单、更贴近代码、更容易维护”。
改造后,HQServer 的 Quartz 定时任务从配置式注册升级为注解式注册。开发者只需要新增任务类并标记 [QuartzJob],框架就会自动扫描、校验并注册任务。配置文件只保留基础开关,任务规则跟随任务类维护,整体结构更清晰,也更适合后续业务长期演进。










暂无评论内容