基于Quartz.NET构建自己的动态作业调度器
在日常的开发中,运行定时任务基本上已经是很普遍的需求了,可以通过windows服务+timer组件来实现,也可以使用第三方框架来集成,Quartz.NET就是一款从JAVA的Quartz移植过来的一个不错的作业调度组件,但是当我们把作业都写好,并部署完成的时候,管理成为了很麻烦的事情,因此我基于Quartz.NET,又简单做了一下封装,来实现作业动态管理。
首先作业动态管理包含以下几个核心点
- 应用程序动态加载器
- 作业管理(运行)池
- 动态启动/停止/卸载作业
Quzrtz.NET怎么用我这里就不再讲解了,百度上很多。
主要有三个核心模块,Job,Trigger和Schedule,
Job就是每一个作业,Trigger就是作业执行策略(多长时间执行一次等),Schedule则把Job和Tigger装载起来
Job和Tigger可以随意搭配装载到Schedule里面运行
接下来讲解实现的思路
先定义一个类库,类库只包含一个类,BaseJob ,里面只有一个Run()方法
之后我们实现的每一个作业都是继承自这个类,实现Run()方法即可(每个作业都作为一个独立的类库,引用这个只有一个类的类库)
public abstract class BaseJob:MarshalByRefObject,IDisposable { public abstract void Run(); }
接下来建立我们的作业管理核心类库Job.Service nuget安装Quartz.NET
然后新建类JobImplement.cs实现Quartz.NET的IJob接口
这样我们就可以在里面通过我们自己写的作业调度容器获取到动态加载的Job信息,并运行Job的run方法,来实现动态调度了(作业调度容器里的作业如何装载进去的在文章后面讲解)
jobRuntimeInfo是我们自己定义的实体类,里面包含了BaseJob,AppDomain,JobInfo 三个信息
JobInfo是作业在上传到作业动态调度框架时所需要填写的作业基本信息
public class JobImplement : IJob { public void Execute(IJobExecutionContext context) { try { long jobId = context.JobDetail.JobDataMap.GetLong("JobId"); //从作业调度容器里查找,如果找到,则运行 var jobRuntimeInfo = JobPoolManager.Instance.Get(jobId); try { jobRuntimeInfo.Job.TryRun(); } catch (Exception ex) { //写日志,任务调用失败 ConnectionFactory.GetInstance<Provider.JobStateRepository>() .Update(new Provider.Tables.JobState() { JobId = jobId, RunState = (int) Provider.DirectiveType.Stop, UpdateTime = DateTime.Now }); Common.Logging.LogManager.GetLogger(this.GetType()).Error(ex.Message, ex); } } catch (Exception ex) { Common.Logging.LogManager.GetLogger(this.GetType()).Error(ex.Message, ex); //调用的时候失败,写日志,这里错误,属于系统级错误,严重错误 } } }
JobRuntimeInfo
public class JobRuntimeInfo { public AppDomain AppDomain; public BaseJob Job { get; set; } public JobInfo JobModel { get; set; } }
JobInfo
public class JobInfo { public long JobId { get; set; } public string JobName { get; set; }public string TaskCron { get; set; } public string Namespace { get; set; } public string MainDllName { get; set; } public string Remark { get; set; } public string ZipFileName { get; set; } public string Version { get; set; } public DateTime? CreateTime { get; set; } }
接下来我们来讲解这个作业是如何执行的
1.通过一个上传页面把作业类库打包为zip或者rar上传到服务器,并填写Job运行的相关信息,添加到数据库里
2.上传完成之后发布一条广播消息给所有的作业调度框架
3.作业调度框架接收到广播消息,从数据库获取JobInfo,自动根据上传的时候填写的信息(见上面的JobInfo类的属性),自动解压,装载到AppDomain里
public class AppDomainLoader { /// <summary> /// 加载应用程序,获取相应实例 /// </summary> /// <param name="dllPath"></param> /// <param name="classPath"></param> /// <param name="appDomain"></param> /// <returns></returns> public static BaseJob Load(string dllPath, string classPath, out AppDomain appDomain) where T : class { AppDomainSetup setup = new AppDomainSetup(); if (System.IO.File.Exists($"{dllPath}.config")) setup.ConfigurationFile = $"{dllPath}.config"; setup.ShadowCopyFiles = "true"; setup.ApplicationBase = System.IO.Path.GetDirectoryName(dllPath); appDomain = AppDomain.CreateDomain(System.IO.Path.GetFileName(dllPath), null, setup); AppDomain.MonitoringIsEnabled = true; BaseJob obj = (BaseJob) appDomain.CreateInstanceFromAndUnwrap(dllPath, classPath); return obj; } /// <summary> /// 卸载应用程序 /// </summary> /// <param name="appDomain"></param> public static void UnLoad(AppDomain appDomain) { AppDomain.Unload(appDomain); appDomain = null; } }
4.因为作业都继承了BaseJob类,所以AppDomain里的入口程序就是JobInfo.Namespace,反射实例化之后强制转换为BaseJob,然后创建一个JobRuntime对象,添加到JobPoolManager里,JobPoolManager里维护所有的正在运行的Job
5.根据JobInfo.TaskCron(时间表达式)创建Trigger,创建一个JobImplement,并在Context里加一个JobId,保证在JobImplement的Run运行的时候能够从JobPoolManager里获取到Job的基本信息,以及BaseJob的事例,并调用JobRuntime=>BaseJob=>Run()方法来运行实际的作业
public class JobPoolManager:IDisposable { private static ConcurrentDictionary<long, JobRuntimeInfo> JobRuntimePool = new ConcurrentDictionary<long, JobRuntimeInfo>(); private static IScheduler _scheduler; private static JobPoolManager _jobPollManager; private JobPoolManager(){} static JobPoolManager() { _jobPollManager = new JobPoolManager(); _scheduler = StdSchedulerFactory.GetDefaultScheduler(); _scheduler.Start(); } public static JobPoolManager Instance { get { return _jobPollManager; } } static object _lock=new object(); public bool Add(long jobId, JobRuntimeInfo jobRuntimeInfo) { lock (_lock) { if (!JobRuntimePool.ContainsKey(jobId)) { if (JobRuntimePool.TryAdd(jobId, jobRuntimeInfo)) { IDictionary<string, object> data = new Dictionary<string, object>() { ["JobId"]=jobId }; IJobDetail jobDetail = JobBuilder.Create<JobImplement>() .WithIdentity(jobRuntimeInfo.JobModel.JobName, jobRuntimeInfo.JobModel.Group) .SetJobData(new JobDataMap(data)) .Build(); var tiggerBuilder = TriggerBuilder.Create() .WithIdentity(jobRuntimeInfo.JobModel.JobName, jobRuntimeInfo.JobModel.Group); if (string.IsNullOrWhiteSpace(jobRuntimeInfo.JobModel.TaskCron)) { tiggerBuilder = tiggerBuilder.WithSimpleSchedule((simple) => { simple.WithInterval(TimeSpan.FromSeconds(1)); }); } else { tiggerBuilder = tiggerBuilder .StartNow() .WithCronSchedule(jobRuntimeInfo.JobModel.TaskCron); } var trigger = tiggerBuilder.Build(); _scheduler.ScheduleJob(jobDetail, trigger); return true; } } return false; } } public JobRuntimeInfo Get(long jobId) { if (!JobRuntimePool.ContainsKey(jobId)) { return null; } lock (_lock) { if (JobRuntimePool.ContainsKey(jobId)) { JobRuntimeInfo jobRuntimeInfo = null; JobRuntimePool.TryGetValue(jobId, out jobRuntimeInfo); return jobRuntimeInfo; } return null; } } public bool Remove(long jobId) { lock (_lock) { if (JobRuntimePool.ContainsKey(jobId)) { JobRuntimeInfo jobRuntimeInfo = null; JobRuntimePool.TryGetValue(jobId, out jobRuntimeInfo); if (jobRuntimeInfo != null) { var tiggerKey = new TriggerKey(jobRuntimeInfo.JobModel.JobName, jobRuntimeInfo.JobModel.Group); _scheduler.PauseTrigger(tiggerKey); _scheduler.UnscheduleJob(tiggerKey); _scheduler.DeleteJob(new JobKey(jobRuntimeInfo.JobModel.JobName, jobRuntimeInfo.JobModel.Group)); JobRuntimePool.TryRemove(jobId, out jobRuntimeInfo); return true; } } return false; } } public virtual void Dispose() { if (_scheduler != null && !_scheduler.IsShutdown) { foreach (var jobId in JobRuntimePool.Keys) { var jobState = ConnectionFactory.GetInstance<Job.Provider.JobStateRepository>().Get(jobId); if (jobState != null) { jobState.RunState = (int) DirectiveType.Stop; jobState.UpdateTime = DateTime.Now; ConnectionFactory.GetInstance<Job.Provider.JobStateRepository>().Update(jobState); } } _scheduler.Shutdown(); } } }
然后我们除了做了一个web版的上传界面之外,还可以做所有的job列表,用来做Start|Stop|Restart等,思路就是发布一条广播给所有的作业调度框架,作业调度框架根据广播消息来进行作业的装载,启动,停止,卸载等操作。
至此,一个基本的动态作业调度框架就结束了。