前段時間給公司項目升級.net框架,把原先的任務管理平台用.net core實現,現做如下整理:
一、實現思路
之前的實現也是參考了博客園中其他文章實現的思路:
- 一個任務定義一個實現IJob接口的類,通過單獨的dll管理;
- 通過數據庫持久化、維護任務,便於服務重啟時任務的恢復;
- 定義一個管理任務的基礎服務,輪詢數據庫中的任務,根據任務的狀態維護任務的執行;
- 新增任務時,需要在數據庫中添加一條記錄,並且在任務管理的dll中添加一個實現IJob的類,基礎服務通過反射dll來構建任務的實例添加到調度器中
由於業務代碼會頻繁調整,我們業務代碼從任務執行中拆分出來,獨立部署成http服務,任務的執行就是調用一個http請求,這樣不同的任務就是請求的url不一樣,查看官方文檔( https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/more-about-jobs.html#job-instances )發現,我們可以通過只創建一個基礎任務類,創建多個該任務類的實例來實現構建多個任務,IJobDetail中可以用JobDataMap對象來存儲Job實例的參數,所以我們通過JobDataMap將請求url傳遞到任務的Execute()方法中,我們只需要在數據庫中補充任務請求的url信息就可以了,不需要單獨的dll去定義任務。
二、項目結構
根據上面思路,我們只需要一個管理任務的基礎服務、一個Web管理平台就可以實現,為了保持項目簡單,把任務管理無關的功能合並在一個項目里,並且盡量排除無關的框架和功能點,最終程序包含3個項目:
- JobManage.Service:控制台程序,管理任務的基礎服務,通過Topshelf部署成windows服務,如何部署參考: https://www.cnblogs.com/podolski/p/10054286.html
- JobManage.Web:Web應用程序,管理平台,新增、暫停、恢復、刪除任務,查看任務運行日志;
- JobManage.Core:類庫,提供業務基礎服務,如數據庫操作等
動態添加任務:
IJobDetail jobDetail = JobBuilder.Create<BaseJob>()
.WithIdentity(jobKey)
.UsingJobData("RequestUrl", job.RequestUrl)
.Build();
ITrigger trigger = TriggerBuilder.Create()
.WithIdentity(group, name)
.StartNow()
.WithCronSchedule(job.CronExpression)
.Build();
await context.Scheduler.ScheduleJob(jobDetail, trigger);
基礎任務類BaseJob.cs的Execute()方法:
public async Task Execute(IJobExecutionContext context)
{
var url = context.JobDetail.JobDataMap.GetString("RequestUrl");
var client = _clientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, url);
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
await response.Content.ReadAsStringAsync();
}
}
三、任務狀態管理
這里定義7個任務狀態:待執行、執行中、待暫停、已暫停、待恢復、待刪除、已刪除
web管理平台維護任務(新增、暫停、恢復、刪除)時將任務狀態更新為待處理狀態(待執行、待暫停、待恢復、待刪除),任務管理基礎服務定時遍歷業務任務,根據數據庫中任務當前的狀態修改任務的執行,並且將數據庫中待處理任務狀態更新為已處理狀態(執行中、已暫停、已刪除)
四、任務依賴注入服務
在任務類中我們用到了http服務,我們需要在任務類中獲取http服務,我們通過.Net Core注入和獲取服務的方式來實現,這里主要是要自定義任務類實例的創建和獲取,官方文檔( https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/miscellaneous-features.html#jobfactory )中說明可以通過實現 IJobFactory 接口,並且修改 IScheduler.JobFactory的屬性來實現:
//自定義任務實例獲取
public class JobFactory : IJobFactory
{
private readonly IServiceProvider _serviceProvider;
public JobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public virtual IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
IJobDetail jobDetail = bundle.JobDetail;
Type jobType = jobDetail.JobType;
return _serviceProvider.GetService(jobType) as IJob;
}
public virtual void ReturnJob(IJob job)
{
var disposable = job as IDisposable;
disposable?.Dispose();
}
}
//修改IScheduler.JobFactory屬性
_scheduler.JobFactory = serviceProvider.GetService<JobFactory>();
官方文檔中也提供了依賴注入的示例: https://www.quartz-scheduler.net/documentation/quartz-3.x/packages/microsoft-di-integration.html#di-aware-job-factories
五、任務監聽
我們需要記錄任務執行的情況,Quartz.Net提供了任務監聽功能,我們可以自己實現IJobListener接口,也可以繼承Quartz.Net框架中IJobListener的實現類JobListenerSupport來完成任務的監聽,繼承JobListenerSupport 類時重寫對應的方法來實現我們需要的操作,如下實現記錄任務上次執行時間、下次執行時間、執行時長、執行異常錯誤信息
//監聽實現
public class JobListener : JobListenerSupport
{
private readonly JobRepository _jobRepository;
private readonly JobRunLogRepository _jobRunLogRepository;
public JobListener(JobRepository jobRepository, JobRunLogRepository jobRunLogRepository)
{
_jobRepository = jobRepository;
_jobRunLogRepository = jobRunLogRepository;
}
public override string Name
{
get { return "jobListener"; }
}
public override async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, CancellationToken cancellationToken = default)
{
string group = context.JobDetail.Key.Group;
string name = context.JobDetail.Key.Name;
DateTime fireTimeUtc = TimeZoneInfo.ConvertTimeFromUtc(context.FireTimeUtc.DateTime, TimeZoneInfo.Local);
DateTime? nextFireTimeUtc = null;
if (context.NextFireTimeUtc != null)
{
nextFireTimeUtc = TimeZoneInfo.ConvertTimeFromUtc(context.NextFireTimeUtc.Value.DateTime, TimeZoneInfo.Local);
}
if (!JobHelper.IsBaseJob(group, name))
{
//更新任務執行情況
await _jobRepository.UpdateExecuteAsync(group, name, fireTimeUtc, nextFireTimeUtc);
//記錄運行日志
double totalSeconds = context.JobRunTime.TotalSeconds;
bool succ = true;
string exception = string.Empty;
if (jobException != null)
{
succ = false;
exception = jobException.ToString();
}
JobRunLog log = new JobRunLog(group, name, totalSeconds, fireTimeUtc, succ, exception);
await _jobRunLogRepository.InsertAsync(log);
}
}
}
//注冊監聽器
JobListener listener = serviceProvider.GetService<JobListener>();
_scheduler.ListenerManager.AddJobListener(listener, GroupMatcher<JobKey>.AnyGroup());
六、總結
上述內容只是記錄了搭建任務管理平台時的思路和幾個關鍵的點,沒有對Quartz.Net基礎功能、MongoDB操作做說明,官方文檔中包含了完整的說明,官方提供的源碼中也有完整的示例,建議閱讀官方文檔源碼來實現更高級的功能。
項目完整代碼地址:https://github.com/zhrong92/JobManage
項目截圖: