1 简介
Quartz.Net是一个强大、开源、轻量的作业调度框架,在平时的项目开发当中也会时不时的需要运用到定时调度方面的功能,如果每天需要跑任务的话,你肯定不会写个while循环,里面进行任务作业吧,这样是很耗线程的,很耗资源的。所以就有目前的定时任务框架。
2 项目级配置思路
- 首先第一步我需要配置quartz,数据库里面有几十条的任务,有需要运行的,有需要暂停的,在项目进行更新或者重启的时候需要重新加载我设置的任务信息,做好日志。
- 支持任务的页面配置,如,任务的管理,增删改。执行按钮操作
- 统一的进行方法配置,采用调用api的方式进行执行任务,我们只需要写好api就行了,在数据库配置好url。
3 下载包并在program注入
// 1在api层 下载包 <PackageReference Include="Quartz.AspNetCore" Version="3.5.0" /> //2注入配置 //3 net6中 只有program类,直接在里面Build前面添加以下代码 builder.Services.AddQuartz(q => { // base quartz scheduler, job and trigger configuration }); // ASP.NET Core hosting builder.Services.AddQuartzServer(options => { // when shutting down we want jobs to complete gracefully options.WaitForJobsToComplete = true; });
4 任务的帮助类
第一步增加Corn表达式的帮助类,用于验证Corn输入是否合格
using Quartz.Impl.Triggers; namespace common { public static class QuartzUtil { /// <summary> /// 验证 Cron 表达式是否有效 /// </summary> /// <param name="cronExpression"></param> /// <returns></returns> public static bool IsValidExpression(this string cronExpression) { try { var trigger = new CronTriggerImpl(); trigger.CronExpressionString = cronExpression; var date = trigger.ComputeFirstFireTimeUtc(null); return date != null; } catch //(Exception e) { return false; } } } }
第二步 任务调度服务公共类,主要负责任务的运行和暂停(复制代码,有一些引用的错误,先不要管)
using Quartz; using Quartz.Impl.Matchers; namespace Common { /// <summary> /// 任务调度服务 /// </summary> public class QuartzJobService : ITransientSelfDependency { private readonly ISchedulerFactory _schedulerFactory; private readonly ResultfulApiJobFactory _resultfulApiJobFactory; public QuartzJobService(ISchedulerFactory schedulerFactory, ResultfulApiJobFactory resultfulApiJobFactory) { _schedulerFactory = schedulerFactory; _resultfulApiJobFactory = resultfulApiJobFactory; } /// <summary> /// 开始运行一个调度器 /// </summary> /// <param name="tasks"></param> /// <returns></returns> public async Task<bool> RunAsync(Z_SyncModules tasks) { //1、通过调度工厂获得调度器 var scheduler = await _schedulerFactory.GetScheduler(); var taskName = $"{tasks.Id}>{tasks.ModuleName}"; //2、创建一个触发器 var trigger = TriggerBuilder.Create() .WithIdentity(taskName, taskName) .StartNow() .WithDescription(tasks.Description) // .WithSimpleSchedule(x => x.WithIntervalInSeconds((int)tasks.Frequency).RepeatForever()) 触发表达式 0 0 0 1 1 ? .WithCronSchedule(tasks.SyncTime) .Build(); //3、创建任务 var jobDetail = JobBuilder.Create<ResultfulApiJob>() .WithIdentity(taskName, taskName) .UsingJobData("TasksId", tasks.Id.ToString()) .Build(); //4、写入 Job 实例工厂 解决 Job 中取 ioc 对象 scheduler.JobFactory = _resultfulApiJobFactory; //5、将触发器和任务器绑定到调度器中 await scheduler.ScheduleJob(jobDetail, trigger); //6、开启调度器 await scheduler.Start(); Console.WriteLine("运行成功:" + taskName); return await Task.FromResult(true); } /// <summary> /// 关闭调度器 /// </summary> /// <param name="tasks"></param> /// <returns></returns> public async Task<bool> CloseAsync(Z_SyncModules tasks) { IScheduler scheduler = await _schedulerFactory.GetScheduler(); var taskName = $"{tasks.Id}>{tasks.ModuleName}"; var jobKeys = (await scheduler .GetJobKeys(GroupMatcher<JobKey>.GroupEquals(taskName))) .ToList().FirstOrDefault(); if (jobKeys == null ) { MessageBox.Show($"未找到任务:{taskName}"); } var triggers = await scheduler.GetTriggersOfJob(jobKeys); ITrigger trigger = triggers?.Where(x => x.JobKey.Name == taskName).FirstOrDefault(); if (trigger == null) { MessageBox.Show($"未找到触发器:{taskName}"); } await scheduler.PauseTrigger(trigger.Key); await scheduler.UnscheduleJob(trigger.Key);// 移除触发器 await scheduler.DeleteJob(trigger.JobKey); Console.WriteLine("关闭成功:"+ taskName); return await Task.FromResult(true); } } }
第三步 运行的方法,也就是任务运行的时候,需要指定一个类,继承 IJob,也就是我们都使用这个类去执行方法
using Quartz; using System.Diagnostics; namespace HZY.Quartz.Service.Jobs { /// <summary> /// Resultful 风格 Api Job /// </summary> [DisallowConcurrentExecution] public class ResultfulApiJob : IJob { private readonly ApiRequestService _apiRequestService; private readonly IServiceProvider _provider; private readonly ILogger<ResultfulApiJob> _logger; public ResultfulApiJob(ApiRequestService apiRequestService, ILogger<ResultfulApiJob> logger, IServiceProvider provider) { _apiRequestService = apiRequestService; _logger = logger; _provider = provider; } public async Task Execute(IJobExecutionContext context) { try { Stopwatch _stopwatch = new Stopwatch(); _stopwatch.Restart(); var tasksId = context.MergedJobDataMap.GetString("TasksId")?.ToString(); if (string.IsNullOrWhiteSpace(tasksId)) { _logger.LogError("tasksId 空!"); return; } Z_SyncModules tasks = null; using (var scope = _provider.CreateScope()) { // 解析你的作用域服务 var service = scope.ServiceProvider.GetService<IAdminRepository<Z_SyncModules>>(); tasks = await service.SelectNoTracking.FirstOrDefaultAsync(w=>w.Id==Guid.Parse(tasksId)); } if (tasks == null) { _logger.LogError("tasks 空!"); return; } var time = DateTime.Now; var taskId = tasks?.Id ?? Guid.Empty; var text = $"{tasks.ModuleName}|组={tasks.ModuleName}|{time:yyyy-MM-dd}|StartTime={time: HH:mm:ss:fff}|"; var result = await _apiRequestService.RequestAsync("Post", tasks.ApiUrl); if (!result.IsSuccess) { _logger.LogError($"Web Api RequestAsync(); 请求失败! WebApi 返回结果:{result.Message}"); } _stopwatch.Stop(); var endTime = $"{DateTime.Now:HH:mm:ss:fff}"; //运行结束记录 text += $"EndTime={endTime}|{_stopwatch.ElapsedMilliseconds} 毫秒|结果={result.Message}"; } catch (Exception ex) { var message = $@"Message={ex.Message}\r\n StackTrace={ex.StackTrace}\r\n Source={ex.Source}\r\n"; _logger.LogError(ex, message, null); } } } }
第四步注入IJob实例,方便注入
using Quartz; using Quartz.Spi; namespace HZY.Services.Admin.QuartzJobTask { /// <summary> /// IJob 对象无法构造注入 需要此类实现 返回 注入后得 Job 实例 /// </summary> public class ResultfulApiJobFactory : IJobFactory { private readonly IServiceProvider _serviceProvider; public ResultfulApiJobFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { //Job类型 Type jobType = bundle.JobDetail.JobType; return _serviceProvider.GetService(jobType) as IJob; } public void ReturnJob(IJob job) { var disposable = job as IDisposable; disposable?.Dispose(); } } }
第五步就是webapi的请求,也就是那些post,get请求
我这里直接用的开源框架 Flurl.Http
namespace Common { /// <summary> /// WebApi 请求服务 /// </summary> public class ApiRequestService : ITransientSelfDependency { private readonly ILogger<ApiRequestService> _logger; public ApiRequestService(ILogger<ApiRequestService> logger) { _logger = logger; } /// <summary> /// 请求数据 /// </summary> /// <param name="requsetMode"></param> /// <param name="apiUrl"></param> /// <param name="headerKeyValue"></param> /// <returns></returns> public async Task<(bool IsSuccess, string Message)> RequestAsync(string requsetMode, string apiUrl, string headerKeyValue = null) { try { var headerKey = "HZY.Quartz.Job.Request"; var headerValue = "Success"; if (!string.IsNullOrWhiteSpace(headerKeyValue) && headerKeyValue.Contains("=")) { headerKey = headerKeyValue.Split('=')[0]; headerValue = headerKeyValue.Split('=')[1]; } IFlurlRequest flurlRequest = apiUrl.WithHeader(headerKey, headerValue); if (flurlRequest == null) { return (false, "flurlRequest 空指针!"); } IFlurlResponse flurResponse = default; if (requsetMode == "Post") { flurResponse = await flurlRequest.PostAsync(); } if (requsetMode == "Get") { flurResponse = await flurlRequest.GetAsync(); } if (flurResponse == null) { return (false, "flurResponse 空指针!"); } var result = await flurResponse.GetStringAsync(); if (string.IsNullOrWhiteSpace(result)) { return (false, "result 空指针!"); } return (true, result); } catch (Exception ex) { _logger.LogError(ex, $"接口请求异常【ApiRequestService 》RequestAsync】:{ex.Message}"); return (false, ex.Message); } } } }
5 ioc容器注入
services.AddTransient<ISchedulerFactory, StdSchedulerFactory>(); //Job 实例化工厂 services.AddSingleton<ResultfulApiJobFactory>(); //Reultful 风格 api 请求 任务 services.AddTransient<ResultfulApiJob>();
6 实现项目启动就从数据库里面运行加载任务
写一个 Worker 继承 IHostedService 里面的逻辑就写,读数据库,循环加载任务,加载任务的方法就在上面的RUNAsync() using System.Threading; using System.Threading.Tasks; namespace HZY.Quartz { public class Worker : IHostedService { private readonly ILogger<Worker> _logger; private readonly IServiceProvider _provider; private readonly QuartzJobService _quartzJob; public Worker(ILogger<Worker> logger, QuartzJobService quartzJob, IServiceProvider provider) { _logger = logger; this._quartzJob = quartzJob; _provider = provider; } public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation($"程序服务开始 : {DateTime.Now}"); List<Z_SyncModules> servicelist = new(); using (var scope = _provider.CreateScope()) { // 解析你的作用域服务 var service = scope.ServiceProvider.GetService<xx>(); if (service != null) { servicelist = await service.Where(xx).ToListAsync(); } _logger.LogInformation($"程序服务查询定时任务列表成功 : {DateTime.Now}"); foreach (var item in servicelist) { //自动恢复任务机制a try { var result = await _quartzJob.RunAsync(item); if (result) { _logger.LogInformation($"自动开启任务成功 [{DateTime.Now}] "); } } catch (Exception ex) { _logger.LogError(ex, $"自动开启任务错误 [{DateTime.Now}] : {ex.Message}"); } } } await StopAsync(cancellationToken); //在项目查询运行的时候运行一次 } public Task StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } } }
7 总结
以上就是我之前在项目中使用定时任务的一些逻辑和代码,
当然了我还参照了目前的开源项目 https://gitee.com/hzy6/hzy-quartz 一些设计思想和代码
,基本上在项目中可以随便使用。具体的一些执行逻辑就可以自己去写
8 Corn
Cron表达式生成器1:https://fontawesome.com
Cron表达式生成器2:https://cron.qqe2.com/
0 0/60 * * * ? 每60分钟执行一次
0 55 7 * * ? 每天7:55执行一次
0 0 1 ? * L 每周一凌晨1点执行
0 0 18 18 * ? 每月18号18点执行一次