很多的軟件項目中都會使用到定時任務、定時輪詢數據庫同步,定時郵件通知等功能。.NET Framework具有“內置”定時器功能,通過System.Timers.Timer類。在使用Timer類需要面對的問題:計時器沒有持久化機制;計時器具有不靈活的計劃(僅能設置開始時間和重復間隔,沒有基於日期,時間等);計時器不使用線程池(每個定時器一個線程);計時器沒有真正的管理方案 - 你必須編寫自己的機制,以便能夠記住,組織和檢索任務的名稱等。
如果需要在.NET實現定時器的功能,可以嘗試使用以下這款開源免費的組件Quartz.Net組件。目前Quartz.NET版本為3.0,修改了原來的一些問題:修復由於線程本地存儲而不能與AdoJobStore協同工作的調度器信令;線程局部狀態完全刪除;quartz.serializer.type是必需的,即使非序列化RAMJobStore正在使用;JSON序列化錯誤地稱為序列化回調。
一.Quart.NET概述:
Quartz是一個作業調度系統,可以與任何其他軟件系統集成或一起使用。作業調度程序是一個系統,負責在執行預處理程序時執行(或通知)其他軟件組件 - 確定(調度)時間到達。Quartz是非常靈活的,並且包含多個使用范例,可以單獨使用或一起使用,以實現您所需的行為,並使您能夠以您的項目看起來最“自然”的方式編寫代碼。組件的使用非常輕便,並且需要非常少的設置/配置 - 如果您的需求相對基礎,它實際上可以使用“開箱即用”。Quartz是容錯的,並且可以在系統重新啟動之間保留(記住)您的預定作業。盡管Quartz對於在給定的時間表上簡單地運行某些系統進程非常有用,但當您學習如何使用Quartz來驅動應用程序的業務流程時,Quartz的全部潛能可以實現。
Quartz是作為一個小的動態鏈接庫(.dll文件)分發的,它包含所有的核心Quartz功能。 此功能的主要接口(API)是調度程序接口。 它提供簡單的操作,如調度/非調度作業,啟動/停止/暫停調度程序。如果你想安排你自己的軟件組件執行,他們必須實現簡單的Job接口,它包含方法execute()。 如果希望在計劃的觸發時間到達時通知組件,則組件應實現TriggerListener或JobListener接口。主要的Quartz'進程'可以在您自己的應用程序或獨立應用程序(使用遠程接口)中啟動和運行。
二.Quartz.NET主體類和方法解析:
1.StdSchedulerFactory類:創建QuartzScheduler實例。
/// <summary> /// 返回此工廠生成的調度程序的句柄。 /// </summary> /// <remarks> ///如果<see cref =“Initialize()”/>方法之一沒有先前調用,然後是默認(no-arg)<see cref =“Initialize()”/>方法將被這個方法調用。 /// </remarks> public virtual IScheduler GetScheduler() { if (cfg == null) { Initialize(); } SchedulerRepository schedRep = SchedulerRepository.Instance; IScheduler sched = schedRep.Lookup(SchedulerName); if (sched != null) { if (sched.IsShutdown) { schedRep.Remove(SchedulerName); } else { return sched; } } sched = Instantiate(); return sched; }
public interface ISchedulerFactory { /// <summary> /// Returns handles to all known Schedulers (made by any SchedulerFactory /// within this app domain.). /// </summary> ICollection<IScheduler> AllSchedulers { get; } /// <summary> /// Returns a client-usable handle to a <see cref="IScheduler" />. /// </summary> IScheduler GetScheduler(); /// <summary> /// Returns a handle to the Scheduler with the given name, if it exists. /// </summary> IScheduler GetScheduler(string schedName); }
2.JobDetailImpl:傳遞給定作業實例的詳細信息屬性。
/// <summary> /// 獲取或設置與<see cref =“IJob”/>相關聯的<see cref =“JobDataMap”/>。 /// </summary> public virtual JobDataMap JobDataMap { get { if (jobDataMap == null) { jobDataMap = new JobDataMap(); } return jobDataMap; } set { jobDataMap = value; } }
3.JobKey:鍵由名稱和組組成,名稱必須是唯一的,在組內。 如果只指定一個組,則默認組將使用名稱。
[Serializable] public sealed class JobKey : Key<JobKey> { public JobKey(string name) : base(name, null) { } public JobKey(string name, string group) : base(name, group) { } public static JobKey Create(string name) { return new JobKey(name, null); } public static JobKey Create(string name, string group) { return new JobKey(name, group); } }
4.StdSchedulerFactory.Initialize():
/// <summary> /// 使用初始化<see cref =“ISchedulerFactory”/> ///給定鍵值集合對象的內容。 /// </summary> public virtual void Initialize(NameValueCollection props) { cfg = new PropertiesParser(props); ValidateConfiguration(); } protected virtual void ValidateConfiguration() { if (!cfg.GetBooleanProperty(PropertyCheckConfiguration, true)) { // should not validate return; } // determine currently supported configuration keys via reflection List<string> supportedKeys = new List<string>(); List<FieldInfo> fields = new List<FieldInfo>(GetType().GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy)); // choose constant string fields fields = fields.FindAll(field => field.FieldType == typeof (string)); // read value from each field foreach (FieldInfo field in fields) { string value = (string) field.GetValue(null); if (value != null && value.StartsWith(ConfigurationKeyPrefix) && value != ConfigurationKeyPrefix) { supportedKeys.Add(value); } } // now check against allowed foreach (string configurationKey in cfg.UnderlyingProperties.AllKeys) { if (!configurationKey.StartsWith(ConfigurationKeyPrefix) || configurationKey.StartsWith(ConfigurationKeyPrefixServer)) { // don't bother if truly unknown property continue; } bool isMatch = false; foreach (string supportedKey in supportedKeys) { if (configurationKey.StartsWith(supportedKey, StringComparison.InvariantCulture)) { isMatch = true; break; } } if (!isMatch) { throw new SchedulerConfigException("Unknown configuration property '" + configurationKey + "'"); } } }
三.Quartz.NET的基本應用:
下面提供一些較為通用的任務處理代碼:
1.任務處理幫助類:
/// <summary> /// 任務處理幫助類 /// </summary> public class QuartzHelper { public QuartzHelper() { } public QuartzHelper(string quartzServer, string quartzPort) { Server = quartzServer; Port = quartzPort; } /// <summary> /// 鎖對象 /// </summary> private static readonly object Obj = new object(); /// <summary> /// 方案 /// </summary> private const string Scheme = "tcp"; /// <summary> /// 服務器的地址 /// </summary> public static string Server { get; set; } /// <summary> /// 服務器的端口 /// </summary> public static string Port { get; set; } /// <summary> /// 緩存任務所在程序集信息 /// </summary> private static readonly Dictionary<string, Assembly> AssemblyDict = new Dictionary<string, Assembly>(); /// <summary> /// 程序調度 /// </summary> private static IScheduler _scheduler; /// <summary> /// 初始化任務調度對象 /// </summary> public static void InitScheduler() { try { lock (Obj) { if (_scheduler != null) return; //配置文件的方式,配置quartz實例 ISchedulerFactory schedulerFactory = new StdSchedulerFactory(); _scheduler = schedulerFactory.GetScheduler(); } } catch (Exception ex) { throw new Exception(ex.Message); } } /// <summary> /// 啟用任務調度 /// 啟動調度時會把任務表中狀態為“執行中”的任務加入到任務調度隊列中 /// </summary> public static void StartScheduler() { try { if (_scheduler.IsStarted) return; //添加全局監聽 _scheduler.ListenerManager.AddTriggerListener(new CustomTriggerListener(), GroupMatcher<TriggerKey>.AnyGroup()); _scheduler.Start(); //獲取所有執行中的任務 List<TaskModel> listTask = TaskHelper.GetAllTaskList().ToList(); if (listTask.Count > 0) { foreach (TaskModel taskUtil in listTask) { try { ScheduleJob(taskUtil); } catch (Exception e) { throw new Exception(taskUtil.TaskName,e); } } } } catch (Exception ex) { throw new Exception(ex.Message); } } /// <summary> /// 啟用任務 /// <param name="task">任務信息</param> /// <param name="isDeleteOldTask">是否刪除原有任務</param> /// <returns>返回任務trigger</returns> /// </summary> public static void ScheduleJob(TaskModel task, bool isDeleteOldTask = false) { if (isDeleteOldTask) { //先刪除現有已存在任務 DeleteJob(task.TaskID.ToString()); } //驗證是否正確的Cron表達式 if (ValidExpression(task.CronExpressionString)) { IJobDetail job = new JobDetailImpl(task.TaskID.ToString(), GetClassInfo(task.AssemblyName, task.ClassName)); //添加任務執行參數 job.JobDataMap.Add("TaskParam", task.TaskParam); CronTriggerImpl trigger = new CronTriggerImpl { CronExpressionString = task.CronExpressionString, Name = task.TaskID.ToString(), Description = task.TaskName }; _scheduler.ScheduleJob(job, trigger); if (task.Status == TaskStatus.STOP) { JobKey jk = new JobKey(task.TaskID.ToString()); _scheduler.PauseJob(jk); } else { List<DateTime> list = GetNextFireTime(task.CronExpressionString, 5); foreach (var time in list) { LogHelper.WriteLog(time.ToString(CultureInfo.InvariantCulture)); } } } else { throw new Exception(task.CronExpressionString + "不是正確的Cron表達式,無法啟動該任務!"); } } /// <summary> /// 初始化 遠程Quartz服務器中的,各個Scheduler實例。 /// 提供給遠程管理端的後台,用戶獲取Scheduler實例的信息。 /// </summary> public static void InitRemoteScheduler() { try { NameValueCollection properties = new NameValueCollection { ["quartz.scheduler.instanceName"] = "ExampleQuartzScheduler", ["quartz.scheduler.proxy"] = "true", ["quartz.scheduler.proxy.address"] =string.Format("{0}://{1}:{2}/QuartzScheduler", Scheme, Server, Port) }; ISchedulerFactory sf = new StdSchedulerFactory(properties); _scheduler = sf.GetScheduler(); } catch (Exception ex) { throw new Exception(ex.StackTrace); } } /// <summary> /// 刪除現有任務 /// </summary> /// <param name="jobKey"></param> public static void DeleteJob(string jobKey) { try { JobKey jk = new JobKey(jobKey); if (_scheduler.CheckExists(jk)) { //任務已經存在則刪除 _scheduler.DeleteJob(jk); } } catch (Exception ex) { throw new Exception(ex.Message); } } /// <summary> /// 暫停任務 /// </summary> /// <param name="jobKey"></param> public static void PauseJob(string jobKey) { try { JobKey jk = new JobKey(jobKey); if (_scheduler.CheckExists(jk)) { //任務已經存在則暫停任務 _scheduler.PauseJob(jk); } } catch (Exception ex) { throw new Exception(ex.Message); } } /// <summary> /// 恢復運行暫停的任務 /// </summary> /// <param name="jobKey">任務key</param> public static void ResumeJob(string jobKey) { try { JobKey jk = new JobKey(jobKey); if (_scheduler.CheckExists(jk)) { //任務已經存在則暫停任務 _scheduler.ResumeJob(jk); } } catch (Exception ex) { throw new Exception(ex.Message); } } /// <summary> /// 獲取類的屬性、方法 /// </summary> /// <param name="assemblyName">程序集</param> /// <param name="className">類名</param> private static Type GetClassInfo(string assemblyName, string className) { try { assemblyName = FileHelper.GetAbsolutePath(assemblyName + ".dll"); Assembly assembly = null; if (!AssemblyDict.TryGetValue(assemblyName, out assembly)) { assembly = Assembly.LoadFrom(assemblyName); AssemblyDict[assemblyName] = assembly; } Type type = assembly.GetType(className, true, true); return type; } catch (Exception ex) { throw new Exception(ex.Message); } } /// <summary> /// 停止任務調度 /// </summary> public static void StopSchedule() { try { //判斷調度是否已經關閉 if (!_scheduler.IsShutdown) { //等待任務運行完成 _scheduler.Shutdown(true); } } catch (Exception ex) { throw new Exception(ex.Message); } } /// <summary> /// 校驗字符串是否為正確的Cron表達式 /// </summary> /// <param name="cronExpression">帶校驗表達式</param> /// <returns></returns> public static bool ValidExpression(string cronExpression) { return CronExpression.IsValidExpression(cronExpression); } /// <summary> /// 獲取任務在未來周期內哪些時間會運行 /// </summary> /// <param name="CronExpressionString">Cron表達式</param> /// <param name="numTimes">運行次數</param> /// <returns>運行時間段</returns> public static List<DateTime> GetNextFireTime(string CronExpressionString, int numTimes) { if (numTimes < 0) { throw new Exception("參數numTimes值大於等於0"); } //時間表達式 ITrigger trigger = TriggerBuilder.Create().WithCronSchedule(CronExpressionString).Build(); IList<DateTimeOffset> dates = TriggerUtils.ComputeFireTimes(trigger as IOperableTrigger, null, numTimes); List<DateTime> list = new List<DateTime>(); foreach (DateTimeOffset dtf in dates) { list.Add(TimeZoneInfo.ConvertTimeFromUtc(dtf.DateTime, TimeZoneInfo.Local)); } return list; } public static object CurrentTaskList() { throw new NotImplementedException(); } /// <summary> /// 獲取當前執行的Task 對象 /// </summary> /// <param name="context"></param> /// <returns></returns> public static TaskModel GetTaskDetail(IJobExecutionContext context) { TaskModel task = new TaskModel(); if (context != null) { task.TaskID = Guid.Parse(context.Trigger.Key.Name); task.TaskName = context.Trigger.Description; task.RecentRunTime = DateTime.Now; task.TaskParam = context.JobDetail.JobDataMap.Get("TaskParam") != null ? context.JobDetail.JobDataMap.Get("TaskParam").ToString() : ""; } return task; } }
2.設置執行中的任務:
public class TaskBll { private readonly TaskDAL _dal = new TaskDAL(); /// <summary> /// 獲取任務列表 /// </summary> /// <param name="pageIndex"></param> /// <param name="pageSize"></param> /// <returns></returns> public PageOf<TaskModel> GetTaskList(int pageIndex, int pageSize) { return _dal.GetTaskList(pageIndex, pageSize); } /// <summary> /// 讀取數據庫中全部的任務 /// </summary> /// <returns></returns> public List<TaskModel> GetAllTaskList() { return _dal.GetAllTaskList(); } /// <summary> /// /// </summary> /// <param name="taskId"></param> /// <returns></returns> public TaskModel GetById(string taskId) { throw new NotImplementedException(); } /// <summary> /// 刪除任務 /// </summary> /// <param name="taskId"></param> /// <returns></returns> public bool DeleteById(string taskId) { return _dal.UpdateTaskStatus(taskId, -1); } /// <summary> /// 修改任務 /// </summary> /// <param name="taskId"></param> /// <param name="status"></param> /// <returns></returns> public bool UpdateTaskStatus(string taskId, int status) { return _dal.UpdateTaskStatus(taskId, status); } /// <summary> /// 修改任務的下次啟動時間 /// </summary> /// <param name="taskId"></param> /// <param name="nextFireTime"></param> /// <returns></returns> public bool UpdateNextFireTime(string taskId, DateTime nextFireTime) { return _dal.UpdateNextFireTime(taskId, nextFireTime); } /// <summary> /// 根據任務Id 修改 上次運行時間 /// </summary> /// <param name="taskId"></param> /// <param name="recentRunTime"></param> /// <returns></returns> public bool UpdateRecentRunTime(string taskId, DateTime recentRunTime) { return _dal.UpdateRecentRunTime(taskId, recentRunTime); } /// <summary> /// 根據任務Id 獲取任務 /// </summary> /// <param name="taskId"></param> /// <returns></returns> public TaskModel GetTaskById(string taskId) { return _dal.GetTaskById(taskId); } /// <summary> /// 添加任務 /// </summary> /// <param name="task"></param> /// <returns></returns> public bool Add(TaskModel task) { return _dal.Add(task); } /// <summary> /// 修改任務 /// </summary> /// <param name="task"></param> /// <returns></returns> public bool Edit(TaskModel task) { return _dal.Edit(task); } }
3.任務實體:
/// <summary> /// 任務實體 /// </summary> public class TaskModel { /// <summary> /// 任務ID /// </summary> public Guid TaskID { get; set; } /// <summary> /// 任務名稱 /// </summary> public string TaskName { get; set; } /// <summary> /// 任務執行參數 /// </summary> public string TaskParam { get; set; } /// <summary> /// 運行頻率設置 /// </summary> public string CronExpressionString { get; set; } /// <summary> /// 任務運頻率中文說明 /// </summary> public string CronRemark { get; set; } /// <summary> /// 任務所在DLL對應的程序集名稱 /// </summary> public string AssemblyName { get; set; } /// <summary> /// 任務所在類 /// </summary> public string ClassName { get; set; } public TaskStatus Status { get; set; } /// <summary> /// 任務創建時間 /// </summary> public DateTime? CreatedTime { get; set; } /// <summary> /// 任務修改時間 /// </summary> public DateTime? ModifyTime { get; set; } /// <summary> /// 任務最近運行時間 /// </summary> public DateTime? RecentRunTime { get; set; } /// <summary> /// 任務下次運行時間 /// </summary> public DateTime? NextFireTime { get; set; } /// <summary> /// 任務備注 /// </summary> public string Remark { get; set; } /// <summary> /// 是否刪除 /// </summary> public int IsDelete { get; set; } }
4.配置文件:
# You can configure your scheduler in either <quartz> configuration section # or in quartz properties file # Configuration section has precedence quartz.scheduler.instanceName = ExampleQuartzScheduler # configure thread pool info quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz quartz.threadPool.threadCount = 10 quartz.threadPool.threadPriority = Normal # job initialization plugin handles our xml reading, without it defaults are used # quartz.plugin.xml.type = Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz # quartz.plugin.xml.fileNames = ~/quartz_jobs.xml # export this server to remoting context quartz.scheduler.exporter.type = Quartz.Simpl.RemotingSchedulerExporter, Quartz quartz.scheduler.exporter.port = 555 quartz.scheduler.exporter.bindName = QuartzScheduler quartz.scheduler.exporter.channelType = tcp quartz.scheduler.exporter.channelName = httpQuartz
四.總結:
在項目中比較多的使用到定時任務的功能,今天的介紹的組件可以很好的完成一些定時任務的要求。這篇文章主要是作為引子,簡單的介紹了組件的背景和組件的使用方式,如果項目中需要使用,可以進行更加深入的了解。