好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

开发定时服务应用

开发定时服务应用

在上篇随笔《 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://www.iqidi.com  
    

 

 

标签:  Winform开发框架

作者: Leo_wl

    

出处: http://www.cnblogs.com/Leo_wl/

    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权信息

查看更多关于开发定时服务应用的详细内容...

  阅读:69次