在开发中会遇到一些需要定时执行的需求,例如发过期邮件,状态更新等等,这里就会需要一个定时服务挂在那在特定的时间去执行特定的代码需求,以下就是我在工作中使用到的Topshelf(服务)+Quartz(任务调度,定时)。
首先创建一个控制台程序,使用nuget包管理器把Quartz,Quartz.Jobs,Quartz.Plugins添加到项目中,后两个是包是要通过xml配置工作jon和执行时间需要的包。
接下来需要配置两个配置文件一个是xml文件,另外一个是配置工作job以及执行时间的config文件start-time表示北京时间2012年4月1日上午8:00开始执行,注意服务启动或重启时都会检测此属性,若没有设置此属性,服务会根据cron-expression的设置执行任务调度;若start-time设置的时间比当前时间较早,则服务启动后会忽略掉cron-expression设置,立即执行一次调度,之后再根据cron-expression执行任务调度;若设置的时间比当前时间晚,则服务会在到达设置时间相同后才会应用cron-expression,根据规则执行任务调度,一般若无特殊需要请不要设置此属性
# You can configure your scheduler in either
<quartz>
configuration section
# or in quartz properties file
# Configuration section has precedence
quartz.scheduler.instanceName = TaskPlanEmailService
# configure thread pool info
quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz
quartz.threadPool.threadCount = 10
quartz.threadPool.threadPriority = Normal
# job initialization plugin handles our xml reading, without it defaults are used
quartz.plugin.xml.type = Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz.Plugins
quartz.plugin.xml.fileNames = ~/quartz_jobs.xml
# export this server to remoting context
#quartz.scheduler.exporter.type = Quartz.Simpl.RemotingSchedulerExporter, Quartz
#quartz.scheduler.exporter.port = 555
#quartz.scheduler.exporter.bindName = QuartzScheduler
#quartz.scheduler.exporter.channelType = tcp
#quartz.scheduler.exporter.channelName = httpQuartz
<?xml version="1.0" encoding="utf-8"?>
<job-scheduling-data xmlns="http://quartznet.sourceforge.net/JobSchedulingData">
<processing-directives>
<overwrite-existing-data>true</overwrite-existing-data>
</processing-directives>
<schedule>
<job>
<name>InternalEHSWinServiceJob</name><!--任务名称,同一个group中多个job的name不能相同-->
<group>InternalEHSWinServiceJob</group> <!--任务所属分组,用于标识任务所属分组-->
<description>法规同步服务</description><!--任务说明-->
<job-type>InternalEHSWinService.InternalEHSWinServiceJob,InternalEHSWinService</job-type><!--任务的具体类型及所属程序集-->
<durable>true</durable> <!--表示该作业是否长久的,默认为true-->
<recover>false</recover>
</job>
<trigger>
<cron>
<name>InternalEHSWinServiceJob</name><!--触发器名称,同一个 group 中作业名称不能相同-->
<group>InternalEHSWinServiceJob</group><!--触发器分组名称,表示该触发器所属分组-->
<job-name>InternalEHSWinServiceJob</job-name><!--要调度的作业名称,必须与 job 节点中的 name 相同-->
<job-group>InternalEHSWinServiceJob</job-group><!--要调度的作业分组名称,必须与 job 节点中的 group 相同-->
<start-time>2020-01-01T00:00:00+08:00</start-time><!--开始作业的 utc 时间-->
<!--每天1点30执行一次-->
<cron-expression>0 30 1 * * ?</cron-expression><!--执行时间-->
</cron>
</trigger>
</schedule>
</job-scheduling-data>
接下来创建服务代理类ServiceProxy,首先得使用Nuget添加Topshelf(服务)Serilog(结构化日志)带运行程序中,然后写启动停止重启代码
/// <summary>
/// 服务代理
/// </summary>
public class ServiceProxy : ServiceControl, ServiceSuspend
{
/// <summary>调度器对象</summary>
private IScheduler _scheduler;
public ServiceProxy()
{
this.Init();
}
/// <inheritdoc />
public bool Start(HostControl hostControl)
{
this._scheduler.Start();
Log.Logger.Information("启动 Windows 服务");
return true;
}
/// <inheritdoc />
public bool Stop(HostControl hostControl)
{
this._scheduler.Shutdown(false);
Log.Logger.Information("停止 Windows 服务");
return true;
}
/// <inheritdoc />
public bool Pause(HostControl hostControl)
{
this._scheduler.PauseAll();
Log.Logger.Information("暂停 Windows 服务");
return true;
}
/// <inheritdoc />
public bool Continue(HostControl hostControl)
{
this._scheduler.ResumeAll();
Log.Logger.Information("重启 Windows 服务");
return true;
}
/// <summary>
/// 初始化
/// </summary>
private async void Init()
{
this._scheduler = await StdSchedulerFactory.GetDefaultScheduler();
}
}
然后在运行程序的Main方法里面配置和运行宿主服务
HostFactory.Run(x => //1
{
x.Service<ServiceProxy>();
x.RunAsLocalSystem();
x.SetDescription("InternalEHSWinService");
x.SetDisplayName("InternalEHSWinService");
x.SetServiceName("InternalEHSWinService");
x.EnablePauseAndContinue();
});
接下里新建一个作业类,继承Ijob接口,然后实现接口里面Execute输出方法,这个方法是一个异步方法,方法里面是需要定时执行的代码。
最后关于配置Serilog结构化日志,先使用nuget把Serilog以及Serilog.Sinks.Console, Serilog.Sinks.Async,Serilog.Sinks.File,Topshelf.Serilog添加到运行项目中,
class Program
{
private static readonly string SerilogOutputTemplate = "{NewLine}{NewLine}Date:{Timestamp:yyyy-MM-dd HH:mm:ss.fff}{NewLine}LogLevel:{Level}{NewLine}Message:{Message}{NewLine}{Exception}" + new string('-', 100);
/// <summary>
/// 获取日志保存文件夹路径
/// </summary>
/// <param name="level">日志等级</param>
/// <returns></returns>
private static string LogFilePath(LogEventLevel level)
{
return $@"{AppDomain.CurrentDomain.BaseDirectory}runLog\{level}\.log";
}
static void Main(string[] args)
{
//日志配置
Log.Logger = new LoggerConfiguration().Enrich.FromLogContext()
.MinimumLevel.Debug() // 所有Sink的最小记录级别
.WriteTo.Console()
.WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(p => p.Level == LogEventLevel.Debug)
.WriteTo.Async(a => a.File(LogFilePath(LogEventLevel.Debug), rollingInterval: RollingInterval.Day, outputTemplate: SerilogOutputTemplate)))
.WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(p => p.Level == LogEventLevel.Information)
.WriteTo.Async(a => a.File(LogFilePath(LogEventLevel.Information), rollingInterval: RollingInterval.Day, outputTemplate: SerilogOutputTemplate)))
.WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(p => p.Level == LogEventLevel.Warning)
.WriteTo.Async(a => a.File(LogFilePath(LogEventLevel.Warning), rollingInterval: RollingInterval.Day, outputTemplate: SerilogOutputTemplate)))
.WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(p => p.Level == LogEventLevel.Error)
.WriteTo.Async(a => a.File(LogFilePath(LogEventLevel.Warning), rollingInterval: RollingInterval.Day, outputTemplate: SerilogOutputTemplate)))
.WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(p => p.Level == LogEventLevel.Fatal)
.WriteTo.Async(a => a.File(LogFilePath(LogEventLevel.Fatal), rollingInterval: RollingInterval.Day, outputTemplate: SerilogOutputTemplate)))
.CreateLogger();
// 配置和运行宿主服务
TopshelfExitCode topshelfExitCode = HostFactory.Run(x => //1
{
x.UseSerilog(Log.Logger);
x.Service<ServiceProxy>();
x.RunAsLocalSystem();//表示以本地系统账号运行,可选的还有网络服务和本地服务账号
x.SetDescription("1");//设置服务的描述
x.SetDisplayName("2");//设置服务的显示名称
x.SetServiceName("3");//设置服务的名称
x.EnablePauseAndContinue();// 启用暂停和恢复功能
});
}
}