很多的软件项目中都会使用到定时任务、定时轮询数据库同步,定时邮件通知等功能。.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 validatereturn;}// determine currently supported configuration keys via reflectionList<string> supportedKeys = new List<string>();List<FieldInfo> fields = new List<FieldInfo>(GetType().GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy));// choose constant string fieldsfields = fields.FindAll(field => field.FieldType == typeof (string));// read value from each fieldforeach (FieldInfo field in fields){string value = (string) field.GetValue(null);if (value != null && value.StartsWith(ConfigurationKeyPrefix) && value != ConfigurationKeyPrefix){supportedKeys.Add(value);}}// now check against allowedforeach (string configurationKey in cfg.UnderlyingProperties.AllKeys){if (!configurationKey.StartsWith(ConfigurationKeyPrefix) || configurationKey.StartsWith(ConfigurationKeyPrefixServer)){// don"t bother if truly unknown propertycontinue;}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 precedencequartz.scheduler.instanceName = ExampleQuartzScheduler# configure thread pool infoquartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartzquartz.threadPool.threadCount = 10quartz.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 contextquartz.scheduler.exporter.type = Quartz.Simpl.RemotingSchedulerExporter, Quartzquartz.scheduler.exporter.port = 555quartz.scheduler.exporter.bindName = QuartzSchedulerquartz.scheduler.exporter.channelType = tcpquartz.scheduler.exporter.channelName = httpQuartz
四.总结: 在项目中比较多的使用到定时任务的功能,今天的介绍的组件可以很好的完成一些定时任务的要求。这篇文章主要是作为引子,简单的介绍了组件的背景和组件的使用方式,如果项目中需要使用,可以进行更加深入的了解。