最近一個項目中需要做郵件提醒和文件轉發的定時服務,由於該系統已經存在N多定時任務,且已安裝多個window服務,由於不想修改以前的服務,也不想繼續追加越來越來多的服務,於是決定寫一個通用的可擴展的windows定時服務框架。下面講解我如何一步步搭建基於Quartz的可配置可擴展的windows定時服務框架。
建立window服務宿主程序
首先我們新建一個控制檯應用程序,用來安裝服務,並且作爲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;
}
}
}
運行之後,就是如下效果,輸入1即可安裝服務。
通過Quartz實現定時任務調度
假設現在我們的服務中需要加入兩項任務,一項是定時發郵件,一項是定時轉發文件,提到定時任務我們就不得不提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();
}
}
/// <summary>
/// 暫停
/// </summary>
protected override void OnPause()
{
scheduler.PauseAll();
base.OnPause();
}
/// <summary>
/// 繼續
/// </summary>
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);
}
/// <summary>
/// 任務的具體業務邏輯寫在Execute方法中
/// </summary>
/// <param name="context"></param>
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官網