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实现反射)处理,找到其中对应方法并执行。

123

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); } } }
 
Timer组件执行的方法execute用于反射执行组件中特定的方法并重置Timer组件DueTime。
        /// <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 DueTime
timer.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);
}
}

posted on 2011-02-20 12:10  easy2Dev  阅读(598)  评论(0编辑  收藏  举报

导航