在项目开发过程中,由于业务需求,我们需要系统定时自动执行一些业务操作,如每天生产结束时需要自动汇总统计当天的生产情况,记录各个部门的库存情况等,为此,我编写了一个windows服务程序实现了这个功能,因为这个功能只是实现自动定时执行工作任务,具体工作任务的实现代码封装在不同的dll中,就是说该程序和具体业务逻辑是非耦合的,通用性比较强,所以我把它共享出来,希望能为大家以后遇到类似需求时提供一些参考思路。
自动任务类的设计
自动任务类
/// <summary>
/// 自动任务
/// </summary>
public class AutoTask
{
/// <summary>
/// 任务名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 执行周期, 1表示月,2表示周,3表示天,4表示小时,5表示秒
/// </summary>
public int Cycle { get; set; }
/// <summary>
/// 执行频率,当周期为小时时启用,如2表示30隔分钟执行1次,
0.5表示2小时执行1次
/// </summary>
public double Frequency { get; set; }
/// <summary>
/// 周期中第几天执行,周期为月或者周时候启用
/// </summary>
public int Exec_Date { get; set; }
/// <summary>
/// 执行时间范围开始时间,周期为月或者周或者天的时候启用,
如 6:30
/// </summary>
public string RangeStart { get; set; }
/// <summary>
/// 执行时间范围结束时间,周期为月或者周或者天的时候启用,
如 7:00
/// </summary>
public string RangedEnd { get; set; }
/// <summary>
/// 上一次执行时间
/// </summary>
public DateTime? LastExecTime { get; set; }
/// <summary>
/// 执行方法: namespace.class.method
/// </summary>
public MethodInfo ExecMethod { get; set; }
/// <summary>
/// 状态: 1起用,0 停用
/// </summary>
public int Status { get; set; }
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; }
}
执行方法信息类
/// <summary>
/// 自动任务中的执行代码信息
/// </summary>
public class MethodInfo
{
/// <summary>
/// 所在程序集名称
/// </summary>
public string AssemblyName { get; set; }
/// <summary>
/// 类名
/// </summary>
public string ClassName { get; set; }
/// <summary>
/// 方法名称
/// </summary>
public string MethodName { get; set; }
/// <summary>
/// 参数集合
/// </summary>
public object[] Args { get; set; }
}
把我们的自动任务配置到xml文件里。(这里就配置两个作为示例)
AutoTask.xml
<?xml version="1.0"?>
<ArrayOfAutoTask xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<AutoTask>
<Name>WriteInvDayReport</Name>
<Cycle>3</Cycle>
<RangeStart>2</RangeStart>
<RangedEnd>3</RangedEnd>
<LastExecTime>2010-07-23T15:14:10.375+08:00</LastExecTime>
<ExecMethod>
<AssemblyName>BCMES.Business, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</AssemblyName>
<ClassName>BCMES.Business.SchemeService</ClassName>
<MethodName>WriteInvDayReport</MethodName>
</ExecMethod>
<Status>1</Status>
</AutoTask>
<AutoTask>
<Name>RefreshSessions</Name>
<Cycle>4</Cycle>
<Frequency>15</Frequency>
<Exec_Date>0</Exec_Date>
<LastExecTime>2010-12-15T20:09:51</LastExecTime>
<ExecMethod>
<AssemblyName>BCMES.Business, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</AssemblyName>
<ClassName>BCMES.Business.FactAPIHelper</ClassName>
<MethodName>RefreshSession</MethodName>
</ExecMethod>
<Status>1</Status>
</AutoTask>
<AutoTask>
</ArrayOfAutoTask>
总体设计思路:创建一个widows服务程序,以某一频率读取autotask.xml文件,
(读取前设置文件为只读),遍历文件上的所有任务,通过任务的最后执行时间与当前系统时间的比较,同时结合任务定义的执行周期和频率,判断出当前任务是否需要执行,不需执行,跳到下个任务,需要执行,则读取该任务的执行方法信息(程序集,类名,方法名),通过反射调用执行方法,执行完毕以后,修改该任务的最后执行时间为当前时间,遍历完成以后,以覆盖形式把任务重新写回xml文件,并去掉文件只读属性。大致过程如下:
新建windows服务程序,在服务类文件Service1.cs放置一时钟控件 timer,设置时钟触发周期和触发事件代码
Timer timer = new Timer();
public static AutoTask_Exec autoTask = new AutoTask_Exec();
protected override void OnStart(string[] args)
{
timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
timer.Interval = (int)callTaskInterval; //此值可改为从配置文件读取
}
private void timer_Elapsed(object sender, ElapsedEventArgs e)
{
autoTask.Run("AutoTask.xml");
}
AutoTask_Exec类代码如下
public class AutoTask_Exec
{
/// <summary>
/// 对象容器,对于执行频率较高的任务,将其执行方法的实例对象存到该容器里,避免重复新建对象
/// </summary>
private Dictionary<string, object> _objectContainer;
public Dictionary<string, object> ObjectContainer
{
get
{
if (_objectContainer == null)
_objectContainer = new Dictionary<string, object>();
return _objectContainer;
}
set { _objectContainer = value; }
}
public AutoTask CurExecTask { get; set; }
public void Run(string fileName)
{
string xmlFileName = AppDomain.CurrentDomain.BaseDirectory + fileName;
if (!File.Exists(xmlFileName) || File.GetAttributes(xmlFileName) == FileAttributes.ReadOnly)
return;
File.SetAttributes(xmlFileName, FileAttributes.ReadOnly);
int invokeCount = 0;
List<AutoTask> tasks = new List<AutoTask>();
try
{
tasks = XmlSerializerExt.Desrialize<List<AutoTask>>(xmlFileName);
}
catch(Exception ex)
{
WriteLog("autotask", "desrialize failure:" + ex.Message, EventLogEntryType.Error);
System.IO.File.SetAttributes(xmlFileName, FileAttributes.Normal);
return;
}
if (tasks == null || tasks.Count==0)
{
System.IO.File.SetAttributes(xmlFileName, FileAttributes.Normal);
return;
}
foreach (AutoTask task in tasks)
{
if (task.Status == 1 && IsExecTime(task))
{
invokeCount++;
object target = null;
string timeStr = DateTime.Now.ToString();
string taskInfo = string.Format("autotask[{0}]", task.Name);
try
{
if (task.Cycle > 3 && ObjectContainer.ContainsKey(task.ExecMethod.ClassName))
target = ObjectContainer[task.ExecMethod.ClassName];
else
{
target = Activator.CreateInstance(task.ExecMethod.AssemblyName, task.ExecMethod.ClassName).Unwrap();
if (task.Cycle > 3)
ObjectContainer.Add(task.ExecMethod.ClassName, target);
}
CurExecTask = task;
target.GetType().InvokeMember(task.ExecMethod.MethodName, BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public, null, target, task.ExecMethod.Args);
CurExecTask = null;
if (task.Cycle < 4)
{
WriteLog(taskInfo, "success", EventLogEntryType.Information);
}
}
catch (Exception ex)
{
WriteLog(taskInfo, "execute autotask failure:" + ex.Message, EventLogEntryType.Error);
}
finally
{
task.LastExecTime = DateTime.Parse(timeStr);
}
}
}
System.IO.File.SetAttributes(xmlFileName, FileAttributes.Normal);
if (invokeCount > 0)
{
XmlSerializerExt.Serialize(tasks, xmlFileName);
}
}
/// <summary>
/// 判断当前任务是否到了执行时间
/// </summary>
/// <returns></returns>
private bool IsExecTime(AutoTask task)
{
DateTime now = DateTime.Now;
bool result = false;
switch (task.Cycle)
{
case 1:
result =
now.Day == task.Exec_Date
//是否指定了开始时间
&& (string.IsNullOrEmpty(task.RangeStart) || (now >= DateTime.Parse(task.RangeStart) && now <= DateTime.Parse(task.RangedEnd)))
//距离上次执行时间是否隔了一个月
&& (!task.LastExecTime.HasValue || (task.LastExecTime.HasValue && now > task.LastExecTime.Value && now.Month != task.LastExecTime.Value.Month));
break;
case 2:
result =
(int)DateTime.Now.DayOfWeek == task.Exec_Date
&& (string.IsNullOrEmpty(task.RangeStart) || (now >= DateTime.Parse(task.RangeStart) && now <= DateTime.Parse(task.RangedEnd)))
//距离上次执行时间是否隔了一个星期
&& (!task.LastExecTime.HasValue || (task.LastExecTime.HasValue && now.Date > task.LastExecTime.Value.Date ));
break;
case 3:
result =
(string.IsNullOrEmpty(task.RangeStart) || (now >= DateTime.Parse(task.RangeStart) && now <= DateTime.Parse(task.RangedEnd)))
//距离上次执行时间是否隔了一天
&& (!task.LastExecTime.HasValue || (task.LastExecTime.HasValue && now.Date > task.LastExecTime.Value.Date));
break;
case 4:
if (task.LastExecTime.HasValue)
{
DateTime comp1 = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0);
DateTime comp2 = new DateTime(task.LastExecTime.Value.Year, task.LastExecTime.Value.Month, task.LastExecTime.Value.Day, task.LastExecTime.Value.Hour, task.LastExecTime.Value.Minute, 0).AddMinutes(60 / task.Frequency);
result = comp1 >= comp2;
}
else
result = true;
break;
case 5:
result = true;
break;
default:
result = false;
break;
}
return result;
}
private void WriteLog(string source, string msg, EventLogEntryType eventType)
{
EventLog myLog = new EventLog();
myLog.Source = source;
myLog.WriteEntry(msg, eventType);
}
}
里面引用的序列化功能类代码
/// <summary>
/// xml序列化扩展类
/// </summary>
public class XmlSerializerExt
{
/// <summary>
/// 把对象序列化到xml文件
/// </summary>
/// <param name="obj"></param>
public static void Serialize(object obj, string filePath)
{
XmlSerializer xs = new XmlSerializer(obj.GetType());
using (MemoryStream ms = new MemoryStream())
{
System.Xml.XmlTextWriter xtw = new System.Xml.XmlTextWriter(ms, System.Text.Encoding.UTF8);
xtw.Formatting = System.Xml.Formatting.Indented;
xs.Serialize(xtw, obj);
using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
xs.Serialize(fs, obj);
fs.Close();
}
}
}
/// <summary>
/// 反序列化方法
/// </summary>
/// <typeparam name="T">反序列化对象类型</typeparam>
/// <param name="xml">反序列化字符串或者xml文件路径</param>
/// <returns></returns>
public static T Desrialize<T>(string xml)
{
T obj = default(T);
XmlSerializer xs = new XmlSerializer(typeof(T));
TextReader tr;
if (!File.Exists(xml))
{
tr = new StringReader(xml);
}
else
{
tr = new StreamReader(xml);
}
using (tr)
{
obj = (T)xs.Deserialize(tr);
}
return obj;
}
}
如何使用。至此,程序主体代码已经完成,至于如何安装windos服务,若没写过的同学请自行goolgle一下吧,我这里不再累述了。在xml文件上写上自己要自动执行的任务信息,同时把任务执行相关的dll和配置文件拷贝至和服务可执行文件同一目录下,启动服务,就ok了。 在项目实际应用中,我发现有个不足的地方是 由于任务是串行执行的,当其中一个任务的方法调用的执行时间比较长的时候(如处理数据量大,或者代码本身的原因),就会影响后续任务的执行,所以,我在第二版程序中做了改进,将一个xml文件扩充为可任意配置n个xml文件,每个文件单独使用一个时钟控件控制读取频率,这样,每个xml文件不但可以设置不同的读取频率,而且在执行过程中就算”卡”在某一任务上,也仅仅影响该xml文件上的后续任务,不会影响其他的xml文件上的任务执行。再解释一下主体代码处2个地方:
Xml文件读取前后要修改文件属性的原因是为了识别当前xml文件处于运行状态还是非运行状态,如果处于运行状态,则表明前一个时间触发事件还没处理完毕,这时不应该读取xml文件。
时钟控件的时间间隔设置应大于或等于xml文件里的自动任务的执行频率的最大值,假设有个自动任务每一分钟执行一次的话,那么系统至少每分钟就要读取xml文件一次,这容易理解吧。