Quartz.NET作為一個開源的作業排程庫,廣泛應用於.NET應用程式中,以實現複雜的定時任務,本次記錄利用Quartz.NET實現HTTP作業排程,透過自定義HTTP作業,實現對外部API的定時呼叫和如何管理這些作業,包括建立、修改、暫停、恢復和刪除作業。
1.首先定義了一個HttpJob
類,該類實現了IJob
介面,用於執行HTTP請求。利用了RestRequest
來構建請求,並透過靜態字典Delegates
儲存每個作業的配置資訊,如URL、請求方法和請求頭等
public class HttpJob : IJob { public static readonly Dictionary<string, HttpJobInfo> Delegates = new(); public async Task Execute(IJobExecutionContext context) { var delegateKey = context.JobDetail.JobDataMap.GetString("delegateKey"); if (delegateKey != null && Delegates.TryGetValue(delegateKey, out var func)) { var requestBody = new RestRequest(); if (func.Headers != null) { foreach (var header in func.Headers) { requestBody.AddHeader(header.Key, header.Value); } } var content = HttpHelper.HttpRequest(func.Url, func.Request, requestBody); JobLogHelper.AddJobLog(new JobLog() { JobName = context.JobDetail.Key.Name, GroupName = context.JobDetail.Key.Group, RunTime = DateTime.Now, RunResult = content }); UpdateLastExecutionTime(context.JobDetail.Key.Name, context.JobDetail.Key.Group, DateTime.Now); } await Task.CompletedTask; } }
2.作業資訊的持久化:為了持久化作業資訊,定義了JobInfo
類來儲存作業的基本資訊,如名稱、組名、Cron表示式等,並將這些資訊儲存在本地的JSON檔案中。
public class JobInfo { public required string JobName { get; set; } public required string GroupName { get; set; } public required string CronExpression { get; set; } public DateTime LastExecutionTime { get; set; } public JobStatus Status { get; set; } public required HttpJobInfo HttpJob { get; set; } }
3.實現了QuartzHelper
類,用於管理作業的生命週期。這包括載入作業資訊、建立作業、排程作業、暫停/恢復作業以及刪除作業等功能。
public class QuartzHelper { private IScheduler scheduler; private List<JobInfo> jobInfos; private string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "jobs.json"); /// <summary> /// 建構函式,初始化定時工作管理員 /// </summary> public QuartzHelper() { ISchedulerFactory schedulerFactory = new StdSchedulerFactory(); scheduler = schedulerFactory.GetScheduler().Result; scheduler.Start().Wait(); LoadJobInfosApi().Wait(); } /// <summary> /// 儲存作業資訊到本地 JSON 檔案 /// </summary> private void SaveJobInfos() { string json = JsonConvert.SerializeObject(jobInfos); File.WriteAllText(filePath, json); } /// <summary> /// 載入本地 JSON 檔案中的作業資訊 /// </summary> private async Task LoadJobInfosApi() { if (File.Exists(filePath)) { string json = File.ReadAllText(filePath); jobInfos = JsonConvert.DeserializeObject<List<JobInfo>>(json) ?? new List<JobInfo>(); foreach (var jobInfo in jobInfos) { // 建立委託的唯一鍵 var delegateKey = Guid.NewGuid().ToString(); // 將委託儲存在靜態字典中 HttpJob.Delegates[delegateKey] = jobInfo.HttpJob; // 建立並排程作業 IJobDetail job = JobBuilder.Create<HttpJob>() .WithIdentity(jobInfo.JobName, jobInfo.GroupName).UsingJobData("delegateKey", delegateKey) // 將委託的鍵新增到JobDataMap .Build(); ITrigger trigger = TriggerBuilder.Create() .WithIdentity(jobInfo.JobName, jobInfo.GroupName) .WithCronSchedule(jobInfo.CronExpression) //.StartNow() .Build(); await scheduler.ScheduleJob(job, trigger); // 根據任務狀態恢復或暫停任務 if (jobInfo.Status == JobStatus.正常執行) { await ResumeJob(jobInfo.JobName, jobInfo.GroupName); } else { await PauseJob(jobInfo.JobName, jobInfo.GroupName); } } } else { jobInfos = new List<JobInfo>(); } } #region 執行普通任務時使用,傳委託時可以參考此方法 ///// <summary> ///// 新建任務並立即執行 ///// </summary> //[Obsolete("執行普通任務時使用,可以傳委託使用")] //public async Task AddJob(string jobName, string groupName, string cronExpression, Func<bool> func, string description = "") //{ // if (jobInfos.Any(c => c.JobName == jobName && c.GroupName == groupName)) // { // return; // } // // 建立委託的唯一鍵 // var delegateKey = Guid.NewGuid().ToString(); // // 將委託儲存在靜態字典中 // // MyJobClass.Delegates[delegateKey] = func; // // 建立作業資訊並儲存到列表 需要將func 加入到jobInfo 中做作業持久化!!!! // var jobInfo = new JobInfo { JobName = jobName, GroupName = groupName, CronExpression = cronExpression, Status = JobStatus.正常執行, Description = description, JobCreateTime = DateTime.Now }; // jobInfos.Add(jobInfo); // SaveJobInfos(); // // 建立Quartz作業和觸發器 // IJobDetail job = JobBuilder.Create<MyJobClass>() // .WithIdentity(jobName, groupName) // .UsingJobData("delegateKey", delegateKey) // 將委託的鍵新增到JobDataMap // .Build(); // ITrigger trigger = TriggerBuilder.Create() // .WithIdentity(jobName + "Trigger", groupName) // .StartNow() // .WithCronSchedule(cronExpression).WithDescription(description) // .Build(); // await scheduler.ScheduleJob(job, trigger); //} #endregion /// <summary> /// 新建任務並立即執行 /// </summary> public async Task AddJobApi(string jobName, string groupName, string cronExpression, HttpJobInfo httpJobInfo, string description = "") { if (jobInfos.Any(c => c.JobName == jobName && c.GroupName == groupName)) { return; } // 建立委託的唯一鍵 var delegateKey = Guid.NewGuid().ToString(); // 將委託儲存在靜態字典中 HttpJob.Delegates[delegateKey] = httpJobInfo; // 建立作業資訊並儲存到列表 需要將func 加入到jobInfo 中做作業持久化!!!! var jobInfo = new JobInfo { JobName = jobName, GroupName = groupName, CronExpression = cronExpression, HttpJob = httpJobInfo, Status = JobStatus.正常執行, Description = description, JobCreateTime = DateTime.Now }; jobInfos.Add(jobInfo); SaveJobInfos(); // 建立Quartz作業和觸發器 IJobDetail job = JobBuilder.Create<HttpJob>() .WithIdentity(jobName, groupName) .UsingJobData("delegateKey", delegateKey) // 將委託的鍵新增到JobDataMap .Build(); ITrigger trigger = TriggerBuilder.Create() .WithIdentity(jobName + "Trigger", groupName) .StartNow() .WithCronSchedule(cronExpression).WithDescription(description) .Build(); await scheduler.ScheduleJob(job, trigger); } /// <summary> /// 暫停任務 /// </summary> public async Task PauseJob(string jobName, string groupName) { await scheduler.PauseJob(new JobKey(jobName, groupName)); var job = jobInfos.FirstOrDefault(j => j.JobName == jobName && j.GroupName == groupName); if (job != null) { job.Status = JobStatus.暫停; SaveJobInfos(); } } /// <summary> /// 開啟任務 /// </summary> public async Task ResumeJob(string jobName, string groupName) { await scheduler.ResumeJob(new JobKey(jobName, groupName)); var job = jobInfos.FirstOrDefault(j => j.JobName == jobName && j.GroupName == groupName); if (job != null) { job.Status = JobStatus.正常執行; SaveJobInfos(); } } /// <summary> /// 立即執行任務 /// </summary> public async Task TriggerJob(string jobName, string groupName) { await scheduler.TriggerJob(new JobKey(jobName, groupName)); var job = jobInfos.FirstOrDefault(j => j.JobName == jobName && j.GroupName == groupName); if (job != null) { job.LastExecutionTime = DateTime.Now; SaveJobInfos(); } } /// <summary> /// 修改任務 /// </summary> public async Task ModifyJob(string jobName, string groupName, string cronExpression, HttpJobInfo httpJobInfo, string description = "") { await DeleteJob(jobName, groupName); await AddJobApi(jobName, groupName, cronExpression, httpJobInfo, description); } /// <summary> /// 刪除任務 /// </summary> public async Task DeleteJob(string jobName, string groupName) { await scheduler.DeleteJob(new JobKey(jobName, groupName)); jobInfos.RemoveAll(j => j.JobName == jobName && j.GroupName == groupName); SaveJobInfos(); } /// <summary> /// 獲取當前所有任務列表 /// </summary> public List<JobInfo> GetAllJobs() { if (File.Exists(filePath)) { string json = File.ReadAllText(filePath); jobInfos = JsonConvert.DeserializeObject<List<JobInfo>>(json) ?? new List<JobInfo>(); return jobInfos; } else return null; } }
4.為了跟蹤作業的執行情況,設計了JobLog
類和JobLogHelper
類,用於記錄和查詢作業執行日誌。
public class JobLogHelper { private static string _filePath; /// <summary> /// 根據作業名稱和組名稱獲取當日的作業執行日誌 /// </summary> /// <param name="jobName"></param> /// <param name="groupName"></param> /// <returns></returns> public static List<JobLog> GetJobLog(string jobName, string groupName) { _filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"jobsLog-{DateTime.Now:yyyyMMdd}.json"); // 檢查檔案是否存在 if (!File.Exists(_filePath)) { return new List<JobLog>(); } var jsonText = $"[{File.ReadAllText(_filePath)}]"; var list = JsonConvert.DeserializeObject<List<JobLog>>(jsonText); if (list != null) { var result = list.Where(c => c.JobName == jobName && groupName == c.GroupName).OrderByDescending(c => c.RunTime).ToList(); return result; } return null; } /// <summary> ///獲取所有的 作業執行日誌 //可以從這裡擴充其他查詢條件 /// </summary> /// <returns></returns> public static List<JobLog> GetAllLogs() { List<JobLog> jobLogs = new List<JobLog>(); var logFilePaths = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "jobsLog-*.json"); logFilePaths.ToList().ForEach(c => { var jsonText = $"[{File.ReadAllText(_filePath)}]"; var list = JsonConvert.DeserializeObject<List<JobLog>>(jsonText); if (list != null) jobLogs.AddRange(list); }); return jobLogs; } /// <summary> /// 新增作業執行日誌 /// </summary> /// <param name="jobLog"></param> public static void AddJobLog(JobLog jobLog) { _filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"jobsLog-{DateTime.Now:yyyyMMdd}.json"); string json = JsonConvert.SerializeObject(jobLog) + ",\n"; File.AppendAllText(_filePath, json); } }
5.最後,透過ASP.NET Core的Controller提供了一系列Web API介面,以便於透過HTTP請求管理作業。這些介面包括獲取作業列表、新增作業、修改作業、刪除作業、暫停作業、恢復作業和立即執行作業等。
[Route("api/[controller]")] [ApiController] public class QuartzController : ControllerBase { private readonly QuartzHelper _quartzHelper; public QuartzController(QuartzHelper quartzHelper) { _quartzHelper = quartzHelper; } [HttpGet] [Route("job/GetJobs")] public object GetJobs() { return Ok(new {code=200,data = _quartzHelper.GetAllJobs() }); } [HttpGet] [Route("job/GetJobLog")] public object GetJobLog(string jobName, string groupName) { return Ok(new { code = 200, data = JobLogHelper.GetJobLog(jobName, groupName) }); } [HttpGet] [Route("job/GetJobLogs")] public object GetJobLogs() { return Ok(new { code = 200, data = JobLogHelper.GetAllLogs() }); } [HttpPost] [Route("job/AddJob")] public async Task<object> Add(JobInfo jobInfo) { try { await _quartzHelper.AddJobApi(jobInfo.JobName, jobInfo.GroupName, jobInfo.CronExpression, jobInfo.HttpJob, jobInfo.Description); return Ok(new { code = 200, msg = "建立成功!" }); } catch (Exception ex) { return Ok(new { code = 500, msg = ex.Message }); } } [HttpPost] [Route("job/ModifyJob")] public async Task<object> Edit(JobInfo jobInfo) { try { await _quartzHelper.ModifyJob(jobInfo.JobName, jobInfo.GroupName, jobInfo.CronExpression, jobInfo.HttpJob, jobInfo.Description); return Ok(new { code = 200, msg = "修改成功!" }); } catch (Exception ex) { return Ok(new { code = 500, msg = ex.Message }); } } [HttpGet] [Route("job/DeleteJob")] public async Task<object> Delete(string jobName, string groupName) { try { await _quartzHelper.DeleteJob(jobName, groupName); return Ok(new { code = 200, msg = "刪除成功!" }); } catch (Exception ex) { return Ok(new { code = 500, msg = ex.Message }); } } [HttpGet] [Route("job/PauseJob")] public async Task<object> PauseJob(string jobName, string groupName) { try { await _quartzHelper.PauseJob(jobName, groupName); return Ok(new { code = 200, msg = "暫停成功!" }); } catch (Exception ex) { return Ok(new { code = 500, msg = ex.Message }); } } [HttpGet] [Route("job/ResumeJob")] public async Task<object> ResumeJob(string jobName, string groupName) { try { await _quartzHelper.ResumeJob(jobName, groupName); return Ok(new { code = 200, msg = "開啟任務成功!" }); } catch (Exception ex) { return Ok(new { code = 500, msg = ex.Message }); } } [HttpGet] [Route("job/TriggerJob")] public async Task<object> TriggerJob(string jobName, string groupName) { try { await _quartzHelper.TriggerJob(jobName, groupName); return Ok(new { code = 200, msg = "立即執行任務命令已執行!" }); } catch (Exception ex) { return Ok(new { code = 500, msg = ex.Message }); } } }
原始碼地址:https://github.com/yycb1994/Quartz.Net