ScheduledJob的实现
前言
生产环境中我们经常要按照一定的时间规律定时做一些工作,比如对一些重要的文档进行定时备份;也可能要在每天早上8点钟准时跑出一支报表;也或许你要每周一系统自动发Mail提醒你本周要做的事情…等等如此这般。
对于此类问题解决的方案有很多:可以使用windows task schedule(任务计划)定时运行某个bat文件/exe文件等;也可以使用数据库(如Oracle,MS Sql Server等)自身的特性建立Job,定时执行…而这里要介绍的Schedule Job是根据我们自身生产环境的特点,自行开发的一套更加灵活的解决方案。
技术要点与结构
开发平台:dotNet
开发语言:C#
解决方案:Windows Service
用到组件/技术:Timer, FileSystemWatcher, XML, Thread,Reflection
开发人员只要实现要定时执行的方法并形成DLL组件,而后只需修改对应的XML配置档案即可完成新增/修改Job的配置。
Windows Service运行后,系统随即利用Dictionary<String,Assembly>缓存要执行的所有DLL组件,利用FileSystemWatcher组件(参考 C# FileSystemWatcher 组件应用)监控DLL文件夹(如果DLL有异动,即对缓存信息进行更新),而后根据XML配置档和Timer组件(参考C# Timer应用)配置要执行的job并定时执行。XML配置档主要说明某个JobID要执行的对应的DLL中某个方法,而Windows Service就据此对缓存的DLL进行反射(参考C# Reflection实现反射)处理,找到其中对应方法并执行。
Demo实现
下面通过一个简单的Demo说明上述实现:Scheduled Job实现每天定时Mail提醒
1.新建配置档JobSetting,用于配置每个Job要执行的方法和执行时间和频率等信息
<?xml version="1.0" encoding="utf-8" ?> <Jobs> <Job Id="140" Name="DailyRoutineJob"> <Dll> <FileName>ScheduleJob.dll</FileName> <TypeName>ScheduleJob.DailyInfo</TypeName> <ConstructArgs /> <MethodName>SendDailyInfo</MethodName> <Args></Args> </Dll> <ExecutionTime Type="Monthly"> <Day>All</Day> <Time>08:00</Time> </ExecutionTime> <EmailAddress>jiaxin606@163.com</EmailAddress> </Job> <Job Id="141" Name="WeeklyRoutineJob"> <Dll> <FileName>ScheduleJob.dll</FileName> <TypeName>ScheduleJob.WeeklyInfo</TypeName> <ConstructArgs /> <MethodName>SendWeeklyInfo</MethodName> <Args></Args> </Dll> <ExecutionTime Type="Monthly"> <Day>1</Day> <Time>08:00</Time> </ExecutionTime> <EmailAddress> jiaxin606@163.com</EmailAddress> </Job> </Jobs>
2.新增一个类库专案:ScheduledJob,注意命名空间和类名,方法名需要和上面的配置档保持一致。
namespace ScheduleJob { public class DailyInfo { public void SendDailyInfo(string msg) { //在此分析发送msg给特定的人员} } }
3.建立专案Windows Service:myScheduledJob
在OnStart方法中,启动监控并加载DLL组件至缓存
protected override void OnStart(string[] args) { systemPath = ConfigurationManager.AppSettings["SystemPath"].ToString(); dllForderName = ConfigurationManager.AppSettings["DllForderName"].ToString(); jobSettingName = ConfigurationManager.AppSettings["JobSettingName"].ToString(); logForderPath = ConfigurationManager.AppSettings["LogForderName"].ToString(); writeLog("Assembly Watcher Started at " + DateTime.Now.ToString()); watcherChangedTimes = new Dictionary<string, DateTime>(); assemblyPool = new Dictionary<string, Assembly>(); timerPool = new Dictionary<string, Timer>(); //监控特定文件夹,监控DLL组件的异动 dllWatcher = new FileSystemWatcher(); dllWatcher.Path = systemPath + @"\" + dllForderName; dllWatcher.IncludeSubdirectories = false; dllWatcher.Filter = "*.dll"; dllWatcher.NotifyFilter = NotifyFilters.LastWrite; dllWatcher.EnableRaisingEvents = true; dllWatcher.Changed += new FileSystemEventHandler(dllWatcher_Changed); writeLog("Begin Set TimerPool..."); //加载DLL组件并缓存 setTimerPool(); writeLog("Set TimerPool Completed"); } void dllWatcher_Changed(object sender, FileSystemEventArgs e) { //60秒内同一个文件只处理一次,此时间间隔可根据具体情况修改:用于解决同一文件更新的多次事件触发问题 #region DateTime now = DateTime.Now; int reloadSeconds = 60; if (watcherChangedTimes.ContainsKey(e.FullPath)) { if (now.Subtract(watcherChangedTimes[e.FullPath]).TotalSeconds < reloadSeconds) { return; } else { watcherChangedTimes[e.FullPath] = now; } } else { watcherChangedTimes.Add(e.FullPath, now); } #endregion Thread.Sleep(5000);//等待5秒待文件释放 FileStream fs = null; Assembly assembly = null; try { Monitor.Enter(lockDllLoader); fs = new FileStream(e.FullPath, FileMode.Open); assembly = Assembly.Load(StreamToByte(fs)); writeLog("DLL" + e.FullPath + "was updated at " + DateTime.Now.ToString()); } catch (Exception ex) { writeLog("DLL" + e.FullPath + "can't be updated at " + DateTime.Now.ToString() + ex.Message); } finally { if (fs != null) { fs.Close(); fs.Dispose(); } Monitor.Exit(lockDllLoader); } if (assembly != null) { //更新AssemblyPool assemblyPool[e.FullPath] = assembly; } }
其中setTimerPool方法用于加载现有DLL组件至缓存并并为每个Job启动一个Timer.
private void setTimerPool() { jobSettingPath = systemPath + @"\" + jobSettingName; XmlDocument doc = new XmlDocument(); doc.Load(jobSettingPath); XmlElement xeJobs = doc.DocumentElement; foreach (XmlElement xeJob in xeJobs.GetElementsByTagName("Job")) { try { ExecuteParameter ep = getExecuteJobArgs(xeJob); DateTime nextExeTime = CalculateTime.GetNextExecutionDateTime
(ep.exeType, ep.exeDays, ep.exeTimes, DateTime.Now); ep.nextExeTime = nextExeTime; writeLog(ep.jobName + "\t First Execute Time is " + ep.nextExeTime.ToString("yyyy/MM/dd HH:mm:ss")); TimerCallback tcb = new TimerCallback(execute); Timer timer = new Timer(tcb, ep, getDueTime(nextExeTime), Timeout.Infinite); timerPool.Add(ep.jobID, timer); } catch (Exception ex) { writeLog(xeJob.GetAttribute("Id") + " Fail to start \r\n" + ex.GetBaseException().Message); } } }
/// <summary> /// Timer执行方法
/// </summary> /// <param name="obj"></param> private void execute(object obj) { string exeTime = null; string assignedTime = null; ExecuteParameter ep = obj as ExecuteParameter; if (ep != null) { try { exeTime = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"); assignedTime = ep.nextExeTime.ToString("yyyy/MM/dd HH:mm:ss"); DateTime nextExeTime = CalculateTime.GetNextExecutionDateTime
(ep.exeType, ep.exeDays, ep.exeTimes, ep.nextExeTime); Timer timer = timerPool[ep.jobID]; //每次执行后根据下次执行时间计算并改变Timer DueTimetimer.Change(getDueTime(nextExeTime), Timeout.Infinite);
ep.nextExeTime = nextExeTime; //应用反射机制呼叫执行DLL中特定方法
FileStream fs = null; Assembly a = null; Type type = null; try { //载入Pool中Assembly,如果Pool中不存在,则添加进去
Monitor.Enter(lockFile); if (assemblyPool.ContainsKey(ep.fileName)) { a = assemblyPool[ep.fileName]; } else { fs = new FileStream(ep.fileName, FileMode.Open); a = Assembly.Load(StreamToByte(fs)); assemblyPool.Add(ep.fileName, a); } type = a.GetType(ep.typeName); } catch (Exception ex) { throw ex; } finally { if (fs != null) { fs.Close(); fs.Dispose(); } Monitor.Exit(lockFile); } //通过反射调用方法object o = Activator.CreateInstance(type, ep.constructArgs); Type[] argTypes = new Type[ep.methodArgs.Length]; for (int i = 0; i < argTypes.Length; i++) { argTypes[i] = typeof(string); } MethodInfo mi = type.GetMethod(ep.methodName, argTypes); object objResult = mi.Invoke(o, ep.methodArgs); writeLog(ep.jobID + "\t" + ep.jobName + "Run Successfully. \r\n\t\t\t Start to Execute at "
+ exeTime + " for Assigned Time at " + assignedTime + " \r\n\t\t\t" + "Next Execution Time is " + ep.nextExeTime.ToString("yyyy/MM/dd HH:mm:ss")); } catch (Exception ex) { //write execute successfully message try { string errorMsg = ep.jobID + "\t" + ep.jobName + "\t Fail to Run at " + exeTime + ".\r\n\t" + ex.Message + "\r\n\t" + ex.GetBaseException().Message; writeLog(errorMsg); } catch (Exception e) { string errorMsg = e.Message + "\r\n\t There is something wrong in the setting file, please check it."; writeLog(errorMsg); } } } }
需要注意的是:如果某个Job的配置档有所变更,比如说执行的时间点有变化,那么此job对应的Timer也必须重置,以按照最新的时间点执行。此动作可以利用windows Service的OnCustomCommand方法(参考C# 利用ServiceController控制window service)接收外部指令resetTimer.
/// <summary> /// Windows Service Reset Timer /// </summary> /// <param name="command">128-255</param> protected override void OnCustomCommand(int command) { //YWindow service OnCustomCommand Job Reset if (command >= 128 && command < 255) { string cmdID = command.ToString(); System.Threading.Timer timer = timerPool.ContainsKey(cmdID) ? timerPool[cmdID] : null; try { writeLog("Scheduled Job ID=" + cmdID + " Starts to Reset."); if (timer != null) { timerPool.Remove(cmdID); } resetTimer(cmdID); writeLog("Scheduled Job ID=" + cmdID + " Resets Successfully."); if (timer != null) { timer.Dispose(); } } catch (Exception ex) { if (!timerPool.ContainsKey(cmdID)) { timerPool.Add(cmdID, timer); } writeLog("Scheduled Job ID=" + cmdID + " Fail to Reset. \r\n\t" + ex.Message); } } else if (command == 255) { GC.Collect(); } } /// <summary> /// Reset Timer /// </summary> /// <param name="commandID"></param> private void resetTimer(string commandID) { try { XmlDocument xmlDoc = new XmlDocument(); xmlDoc.Load(jobSettingPath); XmlElement xeJob = (XmlElement)xmlDoc.DocumentElement.SelectSingleNode("Job[@Id='" + commandID + "']"); ExecuteParameter ep = getExecuteJobArgs(xeJob); DateTime nextExeTime = CalculateTime.GetNextExecutionDateTime(ep.exeType, ep.exeDays, ep.exeTimes, DateTime.Now); ep.nextExeTime = nextExeTime; writeLog(ep.jobName + "\t Execution Time is Reset to " + ep.nextExeTime.ToString("yyyy/MM/dd HH:mm:ss")); TimerCallback tcb = new TimerCallback(execute); Timer timer = new Timer(tcb, ep, getDueTime(nextExeTime), Timeout.Infinite); timerPool.Add(ep.jobID, timer); } catch (Exception ex) { writeLog("Unable to reset timer " + commandID + "\r\n" + ex.GetBaseException().Message); } }