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缓存要执行的所有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.dllFileName>
      <TypeName>ScheduleJob.DailyInfoTypeName>
      <ConstructArgs />
      <MethodName>SendDailyInfoMethodName>
      <Args>Args>
    Dll>
    <ExecutionTime Type="Monthly">
      <Day>AllDay>
      <Time>08:00Time>
    ExecutionTime>
    <EmailAddress>jiaxin606@163.comEmailAddress>
  Job>
  <Job Id="141" Name="WeeklyRoutineJob">
    <Dll>
      <FileName>ScheduleJob.dllFileName>
      <TypeName>ScheduleJob.WeeklyInfoTypeName>
      <ConstructArgs />
      <MethodName>SendWeeklyInfoMethodName>
      <Args>Args>
    Dll>
    <ExecutionTime Type="Monthly">
      <Day>1Day>
      <Time>08:00Time>
    ExecutionTime>
    <EmailAddress> jiaxin606@163.comEmailAddress>
  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。
        /// 
        /// Timer执行方法
/// /// 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.Change(getDueTime(nextExeTime), Timeout.Infinite); //每次执行后根据下次执行时间计算并改变Timer DueTime 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.

        /// 
        /// Windows Service Reset Timer
        /// 
        /// 128-255
        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();
            }
        }

        /// 
        /// Reset Timer
        /// 
        /// 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);
            }
        }

阅读更多

没有更多推荐了,返回首页