Topshelf+Quartz3.0基于控制台应用程序快速开发可调度windows服务
1.TopShelf
TopShelf是一个开源的跨平台的宿主服务框架。可通过.Net Core/.Net Framwork控制台应用程序快速开发windows服务,更加便于服务调试。
本文基于.Net Core2.2快速开发windows服务
首先,我们创建一个控制台应用程序
然后添加Topshelf Nuget程序包 版本4.2.1
通过Topshelf集成的Log4net管理日志,所以我们这里添加了Topshelf.LogNet4 Nuget程序包
添加log4net.config日志配置文件(需手动新建config文件,复制以下内容即可),一般默认配置就可以,主要是改一下日志路径和控制台日志输出级别
1 <?xml version="1.0" encoding="utf-8" ?> 2 <configuration> 3 <configSections> 4 <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/> 5 </configSections> 6 <log4net> 7 <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender"> 8 <!--日志路径--> 9 <param name= "File" value= "D:\log\"/> 10 <!--是否是向文件中追加日志--> 11 <param name= "AppendToFile" value= "true"/> 12 <!--备份文件的最大切分数量--> 13 <param name= "MaxSizeRollBackups" value= "100"/> 14 <!-- 每个文件的大小限制 --> 15 <param name="MaximumFileSize" value="10MB" /> 16 <!-- RollingStyle Composite 综合 Size 按大小 Date 按时间 --> 17 <param name="RollingStyle" value="Composite" /> 18 <!--最小锁定模式,允许多个进程写入同一个文件--> 19 <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> 20 <!--日志文件名是否是固定不变的--> 21 <param name= "StaticLogFileName" value= "false"/> 22 <!--日志文件名格式为:2008-08-31.log--> 23 <datePattern value="yyyy-MM-dd\\"Log4Net"'.log'" /> 24 <layout type="log4net.Layout.PatternLayout"> 25 <param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" /> 26 </layout> 27 </appender> 28 <!-- 控制台前台显示日志 --> 29 <appender name="ColoredConsoleAppender" type="log4net.Appender.ColoredConsoleAppender"> 30 <mapping> 31 <level value="ERROR" /> 32 <foreColor value="Red, HighIntensity" /> 33 </mapping> 34 <mapping> 35 <level value="Info" /> 36 <foreColor value="Green" /> 37 </mapping> 38 <layout type="log4net.Layout.PatternLayout"> 39 <conversionPattern value="%d [%-5level] %m%n" /> 40 </layout> 41 42 <filter type="log4net.Filter.LevelRangeFilter"> 43 <param name="LevelMin" value="Info" /> 44 <param name="LevelMax" value="Fatal" /> 45 </filter> 46 </appender> 47 48 <root> 49 <!--(高) OFF > FATAL > ERROR > WARN > INFO > DEBUG > ALL (低) --> 50 <level value="INFO" /> 51 <appender-ref ref="ColoredConsoleAppender"/> 52 <appender-ref ref="RollingLogFileAppender"/> 53 </root> 54 </log4net> 55 </configuration>
Topshelf关键点:下面我们开始修改Program文件中程序主函数,如下
1 //使用Log4进行日志管理 2 static ILog log = LogManager.GetLogger(typeof(Program)); 3 public static void Main(string[] args) 4 { 5 try 6 { 7 //Console.WriteLine("Hello World!"); 8 HostFactory.Run(x => 9 { 10 x.UseLog4Net("log4net.config", true);//使用配置文件 11 //指定服务 12 x.Service<MyService>(y => 13 { 14 y.ConstructUsing<MyService>(service => new MyService());//实际业务逻辑处理的地方 15 y.WhenStarted((tc, th) => tc.Start(th));//自定义服务类启动动作,其中th表示自定义服务类实现的ServiceControl接口方法中的入参 16 y.WhenStopped((ts, th) => ts.Stop(th));//自定义服务类结束动作 17 }); 18 19 x.RunAsLocalSystem(); 20 21 // 服务描述信息 22 x.SetDescription(ConfigurationManager.AppSettings["ServiceDescription"]); 23 // 服务显示名称 24 x.SetDisplayName(ConfigurationManager.AppSettings["ServiceDisplayName"]); 25 // 服务名称 26 x.SetServiceName(ConfigurationManager.AppSettings["ServiceName"]); 27 }); 28 } 29 catch (Exception ex) 30 { 31 log.Error("服务异常:" + ex.Message); 32 } 33 }
其中服务的一些基本信息(服务名称,描述信息等)我们通过App.config配置文件进行统一管理。添加App.config文件
1 <?xml version="1.0" encoding="utf-8" ?> 2 <configuration> 3 <appSettings> 4 <!--服务名称--> 5 <add key="ServiceName" value="QuartzService"/> 6 <!--服务显示名称--> 7 <add key="ServiceDisplayName" value="Quartz"/> 8 <!--服务描述--> 9 <add key="ServiceDescription" value="Quartz定时服务"/> 10 <!--操作人--> 11 <add key="OpUser" value="Quartz"/> 12 </appSettings> 13 <connectionStrings> 14 <!--数据库连接字符串>--> 15 </connectionStrings> 16 </configuration>
主函数中我们指定了MyService自定义功能类,它是实际处理业务逻辑的地方,也是程序功能处理的入口。自定义类实现了ServiceControl接口,该接口一共包含两个需实现的方法:
bool Start(HostControl hostControl);
bool Stop(HostControl hostControl);
1 public class MyService: ServiceControl 2 { 3 /// <summary> 4 /// 程序开始入口 5 /// </summary> 6 /// <param name="hostControl"></param> 7 /// <returns></returns> 8 public bool Start(HostControl hostControl) 9 { 10 //do something you want to do here 11 return true; 12 } 13 14 /// <summary> 15 /// 程序结束 16 /// </summary> 17 /// <param name="hostControl"></param> 18 /// <returns></returns> 19 public bool Stop(HostControl hostControl) 20 { 21 return true; 22 } 23 }
至此,我们通过Topshelf快速开发windows服务的功能已基本完成,同学只需要在Start()方法中添加自己的服务处理功能即可。
但是,我们这里想介绍下如何结合Quart任务调度框架使用。
首先简单介绍下Quartz:
Quartz 是一个开源作业调度框架,允许程序开发人员根据时间的间隔来调度作业,实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联。
我们需要明白 Quartz 的几个核心概念,这样理解起 Quartz 的原理就会变得简单了。
- Job 表示一个工作,要执行的具体内容。此接口中只有一个方法,如下:(Quartz3.0版本 方法返类型是Task,老版本为void)
Task Execute(IJobExecutionContext context);
- JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。
- Trigger 代表一个调度参数的配置,什么时候去调。
- Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。
好了,了解了上面几个概念之后,我们准备开始入手。本文以配置文件的方式进行作业调度管理。
第一步,添加Quartz3.0 Nuget程序包
第二步,修改我们上面添加的自定义服务类,如下:
主要做如下调整:
1.声明并初始化Quartz调度程序实例,其中scheduler初始化方式和老版本的有点不同,Quartz3.0版本的返回值是Task,我们通过方式1和方式2都可以实现(实际使用中,选择一种即可)
2.程序开始和结束方法,调用scheduler的Start()和Shutdown()
1 private IScheduler scheduler;//声明Quartz调度程序实例,用与管理Job 2 /// <summary> 3 /// 构造函数初始化IScheduler实例 4 /// </summary> 5 public MyService() 6 { 7 //Quartz3.0版本初始化方式1 8 scheduler = StdSchedulerFactory.GetDefaultScheduler().GetAwaiter().GetResult(); 9 //Quartz3.0版本初始化方式2 10 //scheduler = StdSchedulerFactory.GetDefaultScheduler().Result; 11 } 12 13 /// <summary> 14 /// 程序开始入口 15 /// </summary> 16 /// <param name="hostControl"></param> 17 /// <returns></returns> 18 public bool Start(HostControl hostControl) 19 { 20 scheduler.Start(); 21 return true; 22 } 23 24 /// <summary> 25 /// 程序结束 26 /// </summary> 27 /// <param name="hostControl"></param> 28 /// <returns></returns> 29 public bool Stop(HostControl hostControl) 30 { 31 scheduler.Shutdown(); 32 return true; 33 }
第三步,我们需要添加工作任务job,这里是实际干活的任务。新建一个功能类,实现接口IJob。
1 /// <summary> 2 /// 自定义job,实际功能处理单元,需实现IJob 3 /// </summary> 4 public class TestJob : IJob 5 { 6 log4net.ILog _logger = log4net.LogManager.GetLogger(typeof(TestJob)); 7 public Task Execute(IJobExecutionContext context) 8 { 9 _logger.InfoFormat("TestJob测试"); 10 //return Task.FromResult("TestJob测试"); 11 return Task.Factory.StartNew(() => Console.WriteLine($"工作任务测试:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}")); 12 } 13 }
最后,也是比较重要的地方。我们需添加配置文件,来关联我们上面创建的schedule和job。
添加配置文件有个注意的地方:文件属性要选择始终复制,便于发布版本时可用。
添加quartz.config
默认配置就可以,里面指定的调度程序实例,线程池数量,配置文件路径名称等
1 # You can configure your scheduler in either <quartz> configuration section 2 # or in quartz properties file 3 # Configuration section has precedence 4 5 quartz.scheduler.instanceName = ServerScheduler 6 7 # configure thread pool info 8 quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz 9 quartz.threadPool.threadCount = 50 10 11 # job initialization plugin handles our xml reading, without it defaults are used 12 quartz.plugin.xml.type = Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz.Plugins 13 quartz.plugin.xml.fileNames = ~/quartz_jobs.xml 14 15 # export this server to remoting context 16 quartz.scheduler.exporter.type = Quartz.Simpl.RemotingSchedulerExporter, Quartz 17 quartz.scheduler.exporter.port = 555 18 quartz.scheduler.exporter.bindName = QuartzScheduler 19 quartz.scheduler.exporter.channelType = tcp 20 quartz.scheduler.exporter.channelName = httpQuartz
添加quartz_jobs.xml
该配置文件才是体现精华的地方。这里面真正实现了schedule,job和trigger三者的联系。也是好多同学上面有疑问的地方(为什么schedule.start()之后会自动调用我创建的job)
配置注意点:
1.job->job-type,配置job类和job类所在的命名空间
2.trigger->job-name/job-group 一定要和你上面创建的job的name/group一样
1 <?xml version="1.0" encoding="UTF-8"?> 2 3 <!-- This file contains job definitions in schema version 2.0 format --> 4 5 <job-scheduling-data xmlns="http://quartznet.sourceforge.net/JobSchedulingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0"> 6 7 <processing-directives> 8 <overwrite-existing-data>true</overwrite-existing-data> 9 </processing-directives> 10 11 <schedule> 12 13 <job> 14 <name>sampleJob</name> 15 <group>sampleGroup</group> 16 <description>Sample job for Quartz Server</description> 17 <job-type>QuartzServer.TestJob, QuartzServer</job-type> 18 <durable>true</durable> 19 <recover>false</recover> 20 <!--<job-data-map> 21 <entry> 22 <key>key1</key> 23 <value>value1</value> 24 </entry> 25 <entry> 26 <key>key2</key> 27 <value>value2</value> 28 </entry> 29 </job-data-map>--> 30 </job> 31 <!--当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择--> 32 <!--<trigger> 33 <simple> 34 <name>sampleSimpleTrigger</name> 35 <group>sampleSimpleGroup</group> 36 <description>Simple trigger to simply fire sample job</description> 37 <job-name>sampleJob</job-name> 38 <job-group>sampleGroup</job-group> 39 <misfire-instruction>SmartPolicy</misfire-instruction> 40 <repeat-count>-1</repeat-count> 41 <repeat-interval>10000</repeat-interval> 42 </simple> 43 </trigger>--> 44 <!--通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等--> 45 <trigger> 46 <cron> 47 <name>sampleCronTrigger</name> 48 <group>sampleCronGroup</group> 49 <description>Cron trigger to simply fire sample job</description> 50 <job-name>sampleJob</job-name> 51 <job-group>sampleGroup</job-group> 52 <misfire-instruction>SmartPolicy</misfire-instruction> 53 <cron-expression>0/10 * * * * ?</cron-expression> 54 </cron> 55 </trigger> 56 <!--quartz.Calendar它是一些日历特定时间点的集合,一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点。 57 假设,我们安排每周星期一早上10:00执行任务,但是如果碰到法定的节日,任务则不执行,这时就需要在Trigger触发机制的基础上使用Calendar进行定点排除--> 58 <!--<trigger> 59 <calendar-interval> 60 <name>sampleCalendarIntervalTrigger</name> 61 <group>sampleCalendarIntervalGroup</group> 62 <description>Calendar interval trigger to simply fire sample job</description> 63 <job-name>sampleJob</job-name> 64 <job-group>sampleGroup</job-group> 65 <misfire-instruction>SmartPolicy</misfire-instruction> 66 <repeat-interval>15</repeat-interval> 67 <repeat-interval-unit>Second</repeat-interval-unit> 68 </calendar-interval> 69 </trigger>--> 70 </schedule> 71 </job-scheduling-data>
常用的cron-trigger表达式配置说明
1 cron expressions 整体上还是非常容易理解的,只有一点需要注意:"?"号的用法,看下文可以知道“?”可以用在 day of month 和 day of week中,他主要是为了解决如下场景,如:每月的1号的每小时的31分钟,正确的表达式是:* 31 * 1 * ?,而不能是:* 31 * 1 * *,因为这样代表每周的任意一天。 2 3 由7段构成:秒 分 时 日 月 星期 年(可选) 4 "-" :表示范围 MON-WED表示星期一到星期三 5 "," :表示列举 MON,WEB表示星期一和星期三 6 "*" :表是“每”,每月,每天,每周,每年等 7 "/" :表示增量:0/15(处于分钟段里面) 每15分钟,在0分以后开始,3/20 每20分钟,从3分钟以后开始 8 "?" :只能出现在日,星期段里面,表示不指定具体的值 9 "L" :只能出现在日,星期段里面,是Last的缩写,一个月的最后一天,一个星期的最后一天(星期六) 10 "W" :表示工作日,距离给定值最近的工作日 11 "#" :表示一个月的第几个星期几,例如:"6#3"表示每个月的第三个星期五(1=SUN...6=FRI,7=SAT) 12 13 官方实例 14 Expression Meaning 15 0 0 12 * * ? 每天中午12点触发 16 0 15 10 ? * * 每天上午10:15触发 17 0 15 10 * * ? 每天上午10:15触发 18 0 15 10 * * ? * 每天上午10:15触发 19 0 15 10 * * ? 2005 2005年的每天上午10:15触发 20 0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发 21 0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发 22 0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 23 0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 24 0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发 25 0 15 10 ? * MON-FRI 周一至周五的上午10:15触发 26 0 15 10 15 * ? 每月15日上午10:15触发 27 0 15 10 L * ? 每月最后一日的上午10:15触发 28 0 15 10 L-2 * ? Fire at 10:15am on the 2nd-to-last last day of every month 29 0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发 30 0 15 10 ? * 6L Fire at 10:15am on the last Friday of every month 31 0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发 32 0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发 33 0 0 12 1/5 * ? Fire at 12pm (noon) every 5 days every month, starting on the first day of the month. 34 0 11 11 11 11 ? Fire every November 11th at 11:11am.
好了,大功告成,我们只需要发布部署即可。
在服务列表中可看到我们刚才安装的windows服务
好了,本文关于介绍topshelf框架快速开发windows服务以及通过Quartz框架调度管理服务的开发已介绍完毕,文中有不足之处,请各位看官多多指正!