最近一个项目中需要做邮件提醒和文件转发的定时服务,由于该系统已经存在N多定时任务,且已安装多个window服务,由于不想修改以前的服务,也不想继续追加越来越来多的服务,于是决定写一个通用的可扩展的windows定时服务框架。下面讲解我如何一步步搭建基于Quartz的可配置可扩展的windows定时服务框架。
首先我们新建一个控制台应用程序,用来安装服务,并且作为windows服务的宿主程序。大致结构如下:
其中ServicHost是我们的windows服务类,Program是程序运行类,App.config用于服务的配置项。首先,需要在Program中加入安装服务的功能,并且作为ServicHost的载体,代码如下:
static void Main(string[] args)
{
//如果传递了"s"参数就启动服务
if (args.Length > 0 && args[0] == "s")
{
//启动服务的代码,可以从其它地方拷贝
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[]
{
new ServiceHost(),
};
ServiceBase.Run(ServicesToRun);
}
else
{
Console.WriteLine("这是Windows应用程序");
Console.WriteLine("请选择,[1]安装服务 [2]卸载服务 [3]退出");
var rs = int.Parse(Console.ReadLine());
switch (rs)
{
case 1:
//取当前可执行文件路径,加上"s"参数,证明是从windows服务启动该程序
var path = Process.GetCurrentProcess().MainModule.FileName + " s";
Process.Start("sc", "create Quartz.ServiceSelf binpath= \"" + path + "\" displayName= 研发中心定时服务 start= auto");
Console.WriteLine("安装成功");
Console.Read();
break;
case 2:
Process.Start("sc", "delete Quartz.ServiceSelf");
Console.WriteLine("卸载成功");
Console.Read();
break;
case 3: break;
}
}
}
假设现在我们的服务中需要加入两项任务,一项是定时发邮件,一项是定时转发文件,提到定时任务我们就不得不提Quartz,Quartz提供了完善的定时任务调度框架。首先我们在项目中引用Quartz.dll,然后在配置文件中加入两项任务的配置项,配置文件如下:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="email" type="System.Configuration.NameValueSectionHandler"/>
<section name="sendfile" type="System.Configuration.NameValueSectionHandler"/>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<appSettings>
<add key="JobNameSpace" value="Quartz.Jobs"/> <!--这个是任务类所在的命名空间,用于后面反射加载任务类-->
<add key="Jobs" value="email,sendfile"/> <!--配置任务列表-->
</appSettings>
<connectionStrings> <!--数据库连接串-->
<add name="cpjs" connectionString="Data Source=172.26.153.216/KFDBCPJSXX;User ID=peis;Password=peis;"/>
</connectionStrings>
<email> <!--邮件定时任务的配置项-->
<add key="Assembly" value="Quartz.Jobs.EmailJob"/>
<add key="StrCron" value="0/10 * * * * ?"/> <!--此处使用的是Quartz的Cron表达式-->
<!--邮件每10秒执行-->
</email>
<sendfile> <!--文件转发任务的配置项-->
<add key="Assembly" value="Quartz.Jobs.SendFileJob"/>
<add key="StrCron" value="0/20 * * * * ?"/>
<!--转发文件每20秒执行一次-->
</sendfile>
<!--下面是日志的配置项-->
<log4net>
<!-- OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL -->
<!-- Set root logger level to ERROR and its appenders -->
<root>
</root>
<!-- Print only messages of level DEBUG or above in the packages -->
<logger name="emailLogger">
<level value="INFO" />
<appender-ref ref="emailAppender"/>
</logger>
<appender name="emailAppender" type="log4net.Appender.RollingFileAppender,log4net">
<param name="File" value="log/email/" />
<param name="AppendToFile" value="true" />
<param name="RollingStyle" value="Date" />
<param name="DatePattern" value=""Logs_"yyyyMMdd".txt"" />
<param name="StaticLogFileName" value="false" />
<layout type="log4net.Layout.PatternLayout,log4net">
<param name="ConversionPattern" value="%d %-5p %c - %m%n" />
</layout>
</appender>
<logger name="sendFileLogger">
<level value="INFO" />
<appender-ref ref="sendFileAppender"/>
</logger>
<appender name="sendFileAppender" type="log4net.Appender.RollingFileAppender,log4net">
<param name="File" value="log/sendFile/" />
<param name="AppendToFile" value="true" />
<param name="RollingStyle" value="Date" />
<param name="DatePattern" value=""Logs_"yyyyMMdd".txt"" />
<param name="StaticLogFileName" value="false" />
<layout type="log4net.Layout.PatternLayout,log4net">
<param name="ConversionPattern" value="%d %-5p %c - %m%n" />
</layout>
</appender>
</log4net>
</configuration>
在配置文件中我们增加了两个任务的相关配置项,一个是email配置项,执行间隔是10秒一次,一个是sendfile配置项,执行间隔为20秒一次。然后我们看ServiceHost服务如何读取改配置文件将两个任务如期加入windows服务中。
partial class ServiceHost : ServiceBase
{
private IScheduler scheduler;
public ServiceHost()
{
InitializeComponent();
ISchedulerFactory factory = new StdSchedulerFactory();
//实例化一个Quartz的调度器
scheduler = factory.GetScheduler();
}
protected override void OnStart(string[] args)
{
// TODO: 在此处添加代码以启动服务。
if (!scheduler.IsStarted)
{
//启动调度器
scheduler.Start();
//读取Job类的命名空间
var nmspace = ConfigurationManager.AppSettings["JobNameSpace"];
Assembly assembly = Assembly.Load(nmspace);
var jobs = ConfigurationManager.AppSettings["Jobs"];
var arrayjobs = jobs.Split(',');
//通过反射取到每个Job的类型并新建任务加入到调度器中
foreach (var item in arrayjobs)
{
NameValueCollection Config = ConfigurationManager.GetSection(item) as NameValueCollection;
var type = Config["Assembly"];
var cron = Config["StrCron"];
Type job_type = assembly.GetType(type);
//新建一个任务
IJobDetail job = JobBuilder.Create(job_type).WithIdentity(job_type.Name, "JobGroup").Build();
//新建一个触发器
ITrigger trigger = TriggerBuilder.Create().StartNow().WithCronSchedule(cron).Build();
//将任务与触发器关联起来放到调度器中
scheduler.ScheduleJob(job, trigger);
}
}
}
protected override void OnStop()
{
if (!scheduler.IsShutdown)
{
scheduler.Shutdown();
}
}
///
/// 暂停
///
protected override void OnPause()
{
scheduler.PauseAll();
base.OnPause();
}
///
/// 继续
///
protected override void OnContinue()
{
scheduler.ResumeAll();
base.OnContinue();
}
}
代码注释写的比较清楚,我们通过读取配置文件中的Job类的命名空间,然后通过反射取得所有Job类并且新建任务加入到Quartz的调度器中,最终将由Quartz根据我们配置的Cron表达式去定时调度这两个任务。然后我们看下具体的Job类的实现,Job类需要实现Quartz的IJob接口并实现:
public class EmailJob : IJob
{
private static readonly log4net.ILog infoLog = log4net.LogManager.GetLogger("emailLogger");
private static string strcon = ConfigurationManager.ConnectionStrings["cpjs"].ConnectionString;
private SqlSugarClient GetInstance()
{
return new SqlSugarClient(strcon);
}
///
/// 任务的具体业务逻辑写在Execute方法中
///
///
public void Execute(IJobExecutionContext context)
{
infoLog.InfoFormat("EmailJob执行了 {0}", DateTime.Now.ToString());
}
}
此处只是讲解例子,不写具体的业务逻辑,EmailJob和SendFileJob都只加入简单的文件日志记录。然后我们在服务中找到我们安装的Quartz.ServiceSelf服务并启动,查看日志记录看看服务的运行状况,如下:
至此,两个任务已经按照我们的配置如期运行了。考虑到具体的业务逻辑中需要使用数据库,我在项目中引用了SqlSugar这个轻量化且高效的ORM框架,增加了一个Models文件夹用于存放数据库实体类以及业务模型类,最终项目结构如下:
加入现在项目又需要其它定时任务,我们如果在这个服务上做扩展呢?很简单,只需要在Quartz.Jobs中加入一个新类继承IJob并实现任务的业务逻辑,然后编译Quartz.Jobs类库后更新到我们的安装程序目录,再在app.config中加入新任务相关的配置项即可,如下:
注意,我们在新加入任务的时候是不需要改动服务类以及其它任务的代码的,只是做一个横向的扩展添加,所以完全不会影响旧任务的稳定性,我们只需要做新任务的单元测试即可。至此,是否感觉要比多个window服务来执行多个任务更简单方便更易于管理呢?由于时间有限,框架并没有做到尽善尽美,需要其它功能的可以下载了自行修改添加。
1.对Quartz的Cron表达式不熟悉的可以参考如下说明及示例:
cron表达式用于配置cronTrigger的实例。cron表达式实际上是由七个子表达式组成。这些表达式之间用空格分隔。
1.Seconds (秒)
2.Minutes(分)
3.Hours(小时)
4.Day-of-Month (天)
5.Month(月)
6.Day-of-Week (周)
7.Year(年)
例:"0 0 12 ? * WED” 意思是:每个星期三的中午12点执行。
表达式例子:
0 * * * * ? 每1分钟触发一次
0 0 * * * ? 每天每1小时触发一次
0 0 10 * * ? 每天10点触发一次
0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
0 30 9 1 * ? 每月1号上午9点半
0 15 10 15 * ? 每月15日上午10:15触发
*/5 * * * * ? 每隔5秒执行一次
0 */1 * * * ? 每隔1分钟执行一次
0 0 5-15 * * ? 每天5-15点整点触发
0 0/3 * * * ? 每三分钟触发一次
0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
2.sqlsugar有提供基于db-first快速生成数据库实体类的方法,执行如下方法即可:
db.ClassGenerating.CreateClassFilesByTableNames(db, “e:/TestModels2”, “Models”, new string[] { “student”, “school” });
分别传入db参数,生成文件路径,命名空间,表名数组。
更多sqlsugar的用法详见:SqlSugar官网