开发定时服务应用
在上篇随笔《 Winform开发框架之通用定时服务管理 》介绍了我的框架体系中,通用定时服务管理模块的设计以及一些相关功能的展示。我们在做项目的时候,或多或少需要和其他外部系统或者接口进行数据交互,有些是单向的获取,有些是双向的操作。这个定时操作(可能是间隔的时间,也可以能是定在某一个时刻,也可以能是让它在某天某时刻运行),那么这就需要定时服务程序来管理了,通常我们把他寄宿在Windows服务里面(这也是一种最佳的方式),这种方式最好的地方,就是它的生命周期可以随着电脑的启动而启动,而且很少需要用户干预。
1、通用定时服务管理模块框架设计
首先我们回顾一下上面文章对通用定时服务管理模块的设计思路。
整个定时服务的插件设计框架如下所示。
Windows定时服务-文件视图如下所示:
2、如何进行定时服务应用的开发
虽然看起来还是有那么几个文件,其实由于是整个框架是基于插件式的架构,因此但我们开发定时服务应用的时候(如最底下的有黑框的部分),只需要引用插件模块WHC.MyTimingPlugIn.dll并实现ITimingPlugIn接口即可,如上面的 WarningService.dll 就是一个典型的例子。
这个 WarningService 项目就是一个很好的测试例子,它只有一个类,类实现接口ITimingPlugIn。
public class TestService : ITimingPlugIn
{
#region ITimingPlugIn 成员
/// <summary>
/// 操作进度状态事件
/// </summary>
public event ProgressChangedEventHandler ProgressChanged;
public bool Excute()
{
Thread.Sleep( 1000 );
if (ProgressChanged != null )
{
ProgressChanged( this , new ProgressChangedEventArgs( 20 , " 操作进行中...20% " ));
}
// 实际工作处理1
Thread.Sleep( 1000 );
if (ProgressChanged != null )
{
ProgressChanged( this , new ProgressChangedEventArgs( 50 , " 操作进行中...50% " ));
}
// 实际工作处理2
Thread.Sleep( 1000 );
if (ProgressChanged != null )
{
ProgressChanged( this , new ProgressChangedEventArgs( 80 , " 操作进行中...80% " ));
}
// 实际工作处理3:输出内容,作为处理的记录
LogTextHelper.WriteLine( string .Format( " {0}......{1} " , this .Name, DateTime.Now));
Thread.Sleep( 1000 );
if (ProgressChanged != null )
{
ProgressChanged( this , new ProgressChangedEventArgs( 100 , " 操作完成 " ));
}
return true ;
}
/// <summary>
/// 插件程序名称
/// </summary>
public string Name
{
get ;
set ;
}
/// <summary>
/// 插件详细配置
/// </summary>
public PlugInSetting Setting
{
get ;
set ;
}
#endregion
}
开发编译通过后,我们需要为该定时服务应用,在插件XML配置上增加一个这样的说明,让服务程序能够正常加载并识别。
<? xml version="1.0" ?>
< ArrayOfPlugInSetting >
< PlugInSetting >
<!-- 插件程序名称 -->
< Name > 测试名称 </ Name >
<!-- 插件描述内容 -->
< Description > 测试描述 </ Description >
<!-- 运行同步服务的间隔时间(单位:分钟) -->
< ServiceCycleMinutes > 1 </ ServiceCycleMinutes >
<!-- Windows服务在固定时刻(0~23时刻)运行 -->
< ServiceRunAtHour > 23 </ ServiceRunAtHour >
<!-- Windows服务在每月指定天运行,小时按ServiceRunAtHour的值 -->
< ServiceRunAtDay > 1 </ ServiceRunAtDay >
<!-- 运行模式,0为间隔分钟运行 1为固定时刻运行, 2为按月某天和小时 其他值为禁用 -->
< RunMode > 0 </ RunMode >
<!-- 插件的类型名称:插件类名,程序集名称 -->
< PlugInTypeName > WarningServcie.TestService,WarningServcie </ PlugInTypeName >
</ PlugInSetting >
</ ArrayOfPlugInSetting >
上面工作完成后,在正式部署到服务器正式环境前,我们需要检查开发模块是否正常执行,是否正常运行里面的逻辑。那就用到管理程序了。
运行这个程序,除了它具有安装服务、卸载服务、启动、停止、重新启动、刷新服务等这些服务操作外,还提供了DOS测试的功能,让我们在没有部署到Windows服务前,直接进行逻辑测试,很方便的哦。
运行,我们可以看到DOS窗口的输出内容了(我们在实现内部调用的
if (ProgressChanged != null)
{
ProgressChanged(this, new ProgressChangedEventArgs(20, "操作进行中...20%"));
}
就是为了在DOS或者其他通知界面上显示日志信息的。
当然,为了验证处理逻辑,刚才我们也在内部做了记录,我们查看Log/Log.txt日志文件后,我们看到输出的结果如下所示。我们通过日志进行记录,这样可以详细看到具体的操作了。
以上就是一个简单测试应用的实现和测试过程,其他的定时应用服务也是这样实现插件接口就可以部署了。界面管理、服务管理、插件管理这些都是通用模块负责的工作,不需要变动和修改。
3、基于实际WebService同步程序的开发
上面介绍的是一个简单的例子,在我们实际开发过程中,比这个例子肯定负责很多,不过过程是一样的,下面我们来看看我一个实际定时业务应用的具体开发吧。
和上面例子一样,添加一个项目,应用插件模块dll,然后实现接口的内容,不同的是这里引用了一个WebService进行操作,因为需要把Webservice里面的数据保存到本地数据库里面(也就是所谓的同步接口)。
以上的就是一个定时服务应用的类,它实现ITimingPlugIn接口,关键的部分就是如何实现Execute接口了。
由于Webservice接口返回的数据字段比较多,有些可能不一定是我们需要存储的,在实际中发现,有些如PK字段,还有一些以Specified结尾的字段,好像是自动增加上去的。这些我们可能都不需要写到数据库里面去,而且以后WebService可能会增加一些字段,所以得到下面的结论。
1)不能以反射WebService实体类作为基准字段存储。
2)考虑尽可能通用的保存数据,最好字段不用每次都硬编码到代码,因为很多内容的。
3)可以通过获取数据库表的字段作为基准,如果webService实体类也存在该字段,作为写入依据。
从上面的几个经验总结来看,我们需要写一个以数据库表为基准,编写一个通用的服务保存机制函数,来看看如何实现的。
首先通过WebService定义,创建好需要写入的数据库字段(字段必须和WebService同名哦)
然后引入一个辅助类,来获取数据库表字段的信息列表。
/// <summary>
/// 通过获取表的元数据方式获取字段信息的辅助类
/// </summary>
public class TableSchemaHelper
{
/// <summary>
/// 获取指定表的元数据,包括字段名称、类型等等
/// </summary>
/// <param name="tableName"> 数据库表名 </param>
/// <returns></returns>
private static DataTable GetReaderSchema( string tableName)
{
DataTable schemaTable = null ;
string sql = string .Format( " Select * FROM {0} " , tableName);
Database db = DatabaseFactory.CreateDatabase();
DbCommand command = db.GetSqlStringCommand(sql);
try
{
using (IDataReader reader = db.ExecuteReader(command))
{
schemaTable = reader.GetSchemaTable();
}
}
catch (Exception ex)
{
LogTextHelper.Error(ex);
}
return schemaTable;
}
/// <summary>
/// 获取指定数据表字段
/// </summary>
/// <param name="tableName"> 数据库表名 </param>
/// <returns></returns>
public static List< string > GetColumns( string tableName)
{
DataTable schemaTable = GetReaderSchema(tableName);
List < string > list = new List< string > ();
foreach (DataRow dr in schemaTable.Rows)
{
string columnName = dr[ " ColumnName " ].ToString();
list.Add(columnName);
}
return list;
}
/// <summary>
/// 获取指定数据表字段,并转化为小写列表
/// </summary>
/// <param name="tableName"> 数据库表名 </param>
/// <returns></returns>
public static List< string > GetColumnsLower( string tableName)
{
DataTable schemaTable = GetReaderSchema(tableName);
List < string > list = new List< string > ();
foreach (DataRow dr in schemaTable.Rows)
{
string columnName = dr[ " ColumnName " ].ToString();
list.Add(columnName.ToLower());
}
return list;
}
具体的接口实现函数如下所示。
public bool Excute()
{
ShareAccService service = new ShareAccService();
var list = service.getAccList4Deal(unitLogo, undertaker_c, 1 , 10 );
int count = list.Length;
int i = 1 ;
foreach (shareEmitfileSimpleVO obj in list)
{
int step = Convert.ToInt32( 100 * Convert.ToDouble(i) / list.Length);
// 保存列表信息
SaveToDatabase(obj, " MAYOR_EMIT_LIST " , " ACCID " , obj.accid, step);
// 获取并保存明细信息
shareAccVO accVo = service.getDetailAcc4Deal(unitLogo, undertaker_c, obj.accid, obj.emitid);
SaveToDatabase(accVo, " MAYOR_EMIT_DETAIL " , " ACCID " , accVo.accid, step);
// 保存明细信息里面的复函信息
if (accVo.shareEmitfileVOs != null )
{
foreach (shareEmitfileVO fileVo in accVo.shareEmitfileVOs)
{
SaveToDatabase(accVo, " MAYOR_EMIT_ANSWER " , " id " , fileVo.id, step);
}
}
// 保存附件列表信息
List< string > attachList = new List< string > ();
if (accVo.shareAttachmentVOs != null )
{
foreach (shareAttachmentVO attach in accVo.shareAttachmentVOs)
{
attachList.Add(attach.id);
SaveToDatabase(attach, " MAYOR_EMIT_FILE " , " id " , attach.id, step);
}
}
i ++ ;
}
return true ;
}
我们注意到,关键的逻辑其实就是看SaveToDatabase如何实现通用的数据库保存
首先,我们需要通过反射方法看看具体有哪些实体类字段属性。
PropertyInfo[] proList = ReflectionUtil.GetProperties(obj);
然后看看数据库字段列表
List< string > columnList = TableSchemaHelper.GetColumnsLower(targetTable);
获得这两个信息,我们就可以对他们进行比较,然后确定哪些数据写入数据库里面(安全的写入数据库)。
PropertyInfo[] proList = ReflectionUtil.GetProperties(obj);
Dictionary < string , object > recordField = new Dictionary< string , object > ();
try
{
// 为了避免接口变化以及WebService对象的一些额外字段干扰,以数据库字段为准
#region 组装字段语句
List < string > columnList = TableSchemaHelper.GetColumnsLower(targetTable);
foreach (PropertyInfo info in proList)
{
if (!recordField.ContainsKey(info.Name) && columnList.Contains(info.Name.ToLower()))
{
recordField.Add(info.Name, ReflectionUtil.GetProperty(obj, info.Name));
}
}
其中recordField就是记录了,我们需要写入数据库的字段名称和值,这样我们就方便构建相应的操作语句和参数值了。
参数化语句构建如下所示:
string fields = "" ; // 字段名
string vals = "" ; // 字段值
foreach ( string field in recordField.Keys)
{
fields += string .Format( " {0}, " , field);
vals += string .Format( " :{0}, " , field);
}
fields = fields.Trim( ' , ' ); // 除去前后的逗号
vals = vals.Trim( ' , ' ); // 除去前后的逗号
string sql = string .Format( " INSERT INTO {0} ({1}) VALUES ({2}) " , targetTable, fields, vals);
#endregion
当然,我们写入前,为了避免重复同步,因此需要对以存在的记录进行判断,重复的跳过。
string existSql = string .Format( " Select Count(*) from {0} WHERE {1}='{2}' " , targetTable, primaryKey, keyValue);
Database db = DatabaseFactory.CreateDatabase();
DbCommand command = db.GetSqlStringCommand(existSql);
bool hasExist = Convert.ToInt32(db.ExecuteScalar(command)) > 0 ;
if (! hasExist)
{
在数据库不存在的记录要添加到数据库里面,如下代码所示。
// 不存在则要插入
command = db.GetSqlStringCommand(sql);
foreach ( string field in recordField.Keys)
{
object val = recordField[field];
val = val ?? DBNull.Value;
if (val is DateTime)
{
if (Convert.ToDateTime(val) <= Convert.ToDateTime( " 1753-1-1 " ))
{
val = DBNull.Value;
}
}
db.AddInParameter(command, field, TypeToDbType(val.GetType()), val);
}
bool result = db.ExecuteNonQuery(command) > 0 ;
if (result)
{
string tips = string .Format( " 操作表【{0}】的记录成功,{1}={2} " , targetTable, primaryKey, keyValue);
if (ProgressChanged != null )
{
ProgressChanged( this , new ProgressChangedEventArgs(step, tips));
}
}
完整的SaveToDatabase函数如下所示(为了他人方便,也方便自己日后使用。呵呵...):
/// <summary>
/// 保存指定的记录对象到数据库
/// </summary>
private void SaveToDatabase( object obj, string targetTable, string primaryKey, string keyValue, int step)
{
if ( string .IsNullOrEmpty(targetTable) || string .IsNullOrEmpty(primaryKey) ||
string .IsNullOrEmpty(keyValue))
return ;
if (ProgressChanged != null )
{
ProgressChanged( this , new ProgressChangedEventArgs(step, string .Format( " 正在处理表【{0}】{1} = {2} 的记录。 " , targetTable, primaryKey, keyValue)));
}
if (obj != null )
{
PropertyInfo[] proList = ReflectionUtil.GetProperties(obj);
Dictionary < string , object > recordField = new Dictionary< string , object > ();
try
{
// 为了避免接口变化以及WebService对象的一些额外字段干扰,以数据库字段为准
#region 组装字段语句
List < string > columnList = TableSchemaHelper.GetColumnsLower(targetTable);
foreach (PropertyInfo info in proList)
{
if (!recordField.ContainsKey(info.Name) && columnList.Contains(info.Name.ToLower()))
{
recordField.Add(info.Name, ReflectionUtil.GetProperty(obj, info.Name));
}
}
string fields = "" ; // 字段名
string vals = "" ; // 字段值
foreach ( string field in recordField.Keys)
{
fields += string .Format( " {0}, " , field);
vals += string .Format( " :{0}, " , field);
}
fields = fields.Trim( ' , ' ); // 除去前后的逗号
vals = vals.Trim( ' , ' ); // 除去前后的逗号
string sql = string .Format( " INSERT INTO {0} ({1}) VALUES ({2}) " , targetTable, fields, vals);
#endregion
#region 判断指定键值的记录是否存在并写入新的
string existSql = string .Format( " Select Count(*) from {0} WHERE {1}='{2}' " , targetTable, primaryKey, keyValue);
Database db = DatabaseFactory.CreateDatabase();
DbCommand command = db.GetSqlStringCommand(existSql);
bool hasExist = Convert.ToInt32(db.ExecuteScalar(command)) > 0 ;
if (! hasExist)
{
// 不存在则要插入
command = db.GetSqlStringCommand(sql);
foreach ( string field in recordField.Keys)
{
object val = recordField[field];
val = val ?? DBNull.Value;
if (val is DateTime)
{
if (Convert.ToDateTime(val) <= Convert.ToDateTime( " 1753-1-1 " ))
{
val = DBNull.Value;
}
}
db.AddInParameter(command, field, TypeToDbType(val.GetType()), val);
}
bool result = db.ExecuteNonQuery(command) > 0 ;
if (result)
{
string tips = string .Format( " 操作表【{0}】的记录成功,{1}={2} " , targetTable, primaryKey, keyValue);
if (ProgressChanged != null )
{
ProgressChanged( this , new ProgressChangedEventArgs(step, tips));
}
}
}
#endregion
}
catch (Exception ex)
{
string tips = string .Format( " 操作表【{0}】的记录出错,{1}={2} " , targetTable, primaryKey, keyValue);
if (ProgressChanged != null )
{
ProgressChanged( this , new ProgressChangedEventArgs(step, tips));
}
LogTextHelper.Error(tips);
string description = "" ;
foreach ( string field in recordField.Keys)
{
description += string .Format( " {0}={1}, " , field, recordField[field]);
}
LogTextHelper.Info(description);
LogTextHelper.Error(ex);
}
}
}
开发完成后,我们在添加XML插件配置信息,如下所示
<? xml version="1.0" ?>
< ArrayOfPlugInSetting >
< PlugInSetting >
<!-- 插件程序名称 -->
< Name > 测试名称 </ Name >
<!-- 插件描述内容 -->
< Description > 测试描述 </ Description >
<!-- 运行同步服务的间隔时间(单位:分钟) -->
< ServiceCycleMinutes > 1 </ ServiceCycleMinutes >
<!-- Windows服务在固定时刻(0~23时刻)运行 -->
< ServiceRunAtHour > 23 </ ServiceRunAtHour >
<!-- Windows服务在每月指定天运行,小时按ServiceRunAtHour的值 -->
< ServiceRunAtDay > 1 </ ServiceRunAtDay >
<!-- 运行模式,0为间隔分钟运行 1为固定时刻运行, 2为按月某天和小时 其他值为禁用 -->
< RunMode > 0 </ RunMode >
<!-- 插件的类型名称:插件类名,程序集名称 -->
< PlugInTypeName > WarningServcie.TestService,WarningServcie </ PlugInTypeName >
</ PlugInSetting >
< PlugInSetting >
<!-- 插件程序名称 -->
< Name > 热线数据同步 </ Name >
<!-- 插件描述内容 -->
< Description > 测试描述 </ Description >
<!-- 运行同步服务的间隔时间(单位:分钟) -->
< ServiceCycleMinutes > 1 </ ServiceCycleMinutes >
<!-- Windows服务在固定时刻(0~23时刻)运行 -->
< ServiceRunAtHour > 23 </ ServiceRunAtHour >
<!-- Windows服务在每月指定天运行,小时按ServiceRunAtHour的值 -->
< ServiceRunAtDay > 1 </ ServiceRunAtDay >
<!-- 运行模式,0为间隔分钟运行 1为固定时刻运行, 2为按月某天和小时 其他值为禁用 -->
< RunMode > 1 </ RunMode >
<!-- 插件的类型名称:插件类名,程序集名称 -->
< PlugInTypeName > MayorHotlineService.DownDataService,MayorHotlineService </ PlugInTypeName >
</ PlugInSetting >
</ ArrayOfPlugInSetting >
这样开发完成后,我们需要对服务进行一次测试,确认逻辑正常。
最后利用管理界面的 安装服务 ,把它部署上去就可以了,这样它就以Windows服务的形式进行运行,不用再进行干预了。
主要研究技术:代码生成工具、Visio二次开发、送水管理软件等共享软件开发
专注于 Winform开发框架 、WCF开发框架的研究及应用。
转载请注明出处:
撰写人:伍华聪 http://HdhCmsTestiqidi测试数据
标签: Winform开发框架
作者: Leo_wl
出处: http://HdhCmsTestcnblogs测试数据/Leo_wl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
版权信息