【ASP.NET CORE】9.Quartz.NET定时任务调度优化

介绍

上一篇我们已经在 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],框架就会自动扫描、校验并注册任务。配置文件只保留基础开关,任务规则跟随任务类维护,整体结构更清晰,也更适合后续业务长期演进。

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容