.NET开源作业调度框架Quartz
一 Quartz简介
-
Quartz是一个强大、开源、轻量的作业调度框架,你能够用它来为执行一个简单或复杂的作业调度任务。
-
是对非常流行的JAVA开源调度框架 Quartz 的移植。
-
它有很多特征,如:数据库支持,集群,插件,支持Cron-like表达式等等。非常适合在平时的工作中,做定时轮询数据库同步,定时邮件通知,定时数据处理等操作。
-
允许开发人员根据时间间隔(或天)来调度作业。它实现了作业和触发器的多对多关系,还能把多个作业与不同的触发器关联。
官网: http://www.quartz-scheduler.net/
源码: https://github.com/quartznet/quartznet
示例: https://www.quartz-scheduler.net/documentation/quartz-3.x/quick-start.html
1 运行时环境
运行时环境
Quartz.NET 从3.0版本开始支持Standard 2.0
Quartz.NET 3.0 既可以在.NetFarmerwork上运行,也可以在.NetCore项目上运行
2 三个基础概念
-
任务:需要执行的任务。
-
触发器:触发器用于配置调度参数,设置触发条件。
-
调度器:调度器将对应的触发器和任务绑定在一起,当触发器被触发时执行对应的任务。
•Job 为作业的接口,JobDetail 用来描述 Job 的实现类及其它相关的静态信息;
•Trigger 作为作业的定时管理工具,一个 Trigger 只能对应一个作业实例,而一个作业实例可对应多个 Trigger ;
•Scheduler 做为定时任务容器,它包含了所有触发器和作业,每个 Scheduler 都存有 JobDetail 和 Trigger的注册,一个 Scheduler 中可以注册多个 JobDetail 和多个 Trigger 。
3 Trigger任务触发器
lSimpleTrigger 设置一些简单的属性,如开始时间、结束时间、重复次数、重复间隔等。
•name(必填) 触发器名称,同一个分组中的名称必须不同
•group(选填) 触发器组
•description(选填) 触发器描述
•job-name(必填) 要调度的任务名称,该job-name必须和对应job节点中的name完全相同
•job-group(选填) 调度任务(job)所属分组,该值必须和job中的group完全相同
•start-time(选填) 任务开始执行时间utc时间,北京时间需要+08:00,如:2012-04-01T08:00:00+08:00表示北京时间2012年4月1日上午8:00开始执行,注意服务启动或重启时都会检测此属性,若没有设置此属性或者start-time设置的时间比当前时间较早,则服务启动后会立即执行一次调度,若设置的时间比当前时间晚,服务会等到设置时间相同后才会第一次执行任务,一般若无特殊需要请不要设置此属性
•repeat-count(必填) 任务执行次数,如:-1表示无限次执行,10表示执行10次
•repeat-interval(必填) 任务触发间隔(毫秒),如:10000 每10秒执行一次
lCronTrigger 复杂任务触发器–使用cron表达式定制任务调度(强烈推荐)
•name(必填) 触发器名称,同一个分组中的名称必须不同
•group(选填) 触发器组
•description(选填) 触发器描述
•job-name(必填) 要调度的任务名称,该job-name必须和对应job节点中的name完全相同
•job-group(选填) 调度任务(job)所属分组,该值必须和job中的group完全相同
•start-time(选填) 任务开始执行时间utc时间,北京时间需要+08:00,如:2012-04-01T08:00:00+08:00表示北京时间2012年4月1日上午8:00开始执行,注意服务启动或重启时都会检测此属性,若没有设置此属性,服务会根据cron-expression的设置执行任务调度;若start-time设置的时间比当前时间较早,则服务启动后会忽略掉cron-expression设置,立即执行一次调度,之后再根据cron-expression执行任务调度;若设置的时间比当前时间晚,则服务会在到达设置时间相同后才会应用cron-expression,根据规则执行任务调度,一般若无特殊需要请不要设置此属性
•cron-expression(必填) cron表达式,如:0/10 * * * * ?每10秒执行一次
4 监听器
任务调度框架并不是完美的,它也会出现任务执行失败的情况。如果你需要处理任务失败后的逻辑,这个时候监听器就发挥作用了。Quartz的监听器用于当任务调度中你所关注事件发生时,能够及时获取这一事件的通知。包括像任务执行前、执行中、执行后、执行异常等事件的通知。
种类
Quartz监听器主要有以下三种,分别表示任务、触发器、调度器对应的监听器。
lJobListener:任务调度过程中,与任务Job相关的事件包括job开始要执行的提示,job执行完成的提示。
lTriggerListener:任务调度过程中,与触发器Trigger相关的事件包括触发器触发、触发器未正常触发、触发器完成等。
lSchedulerListener:会在Scheduler的生命周期中关键事件发生时被调用。与Scheduler有关的事件包括:增加一个job/trigger,删除一个job/trigger,scheduler发生严重错误,关闭scheduler等。
-
全局监听器:能够接收到所有的Job/Trigger的事件通知
-
非全局监听器:只能接收到在其上注册的Job或Trigger的事件,不在其上注册的Job或Trigger则不会进行监听。
对于非全局的 JobListener
,它应于任何引用到它的 JobDetail
使用 schedulerJob()
或 addJob()
方法注册之前被注册。
5 Quartz API 的主要接口和类如下
IScheduler - 和调度器交互的主要API
IJob - 调度器会执行实现这个接口的实例
IJobDetail - 用来定义任务的实例
ITrigger - 定义任务执行安排的组件
JobBuilder - 用来定义/构造JobDetail的实例,其中JobDetail的实例定义Jobs的实例
TriggerBuilder - 用来定义/构造Trigger的实例
6 依赖框架
-
引入框架的方法非常简单你可以直接用NuGet管理包也可以在项目中添加引用。
-
点击“工具”->“NuGet包管理器”->“程序包管理器控制台”
输入安装包的命令:
Install-Package Quartz
二 Quartz应用
五步创建Quartz应用
- 创建一个调度器
- 定义要执行的任务
- 创建一个任务对象
- 创建一个触发器
- 将任务与触发器添加到调度器中并执行
1 创建一个Scheduler任务调度容器
StdSchedulerFactory factory = new StdSchedulerFactory();
//创建一个Scheduler任务调度容器
IScheduler scheduler = await factory.GetScheduler();
2 定义要执行的任务
定义一个类,实现IJob接口,实现方法Execute,代码如下:
using Quartz;
namespace QuartZDemo.Web.Jobs
{
//增加特性保证任务不会重叠执行
[DisallowConcurrentExecution]
public class SendMailJob : IJob
{
//Job类
public Task Execute(IJobExecutionContext context)
{
return Task.Run(() =>
{
//doSomthing
Console.WriteLine($"开始发送邮件{DateTime.Now}");
});
}
}
}
3 创建一个任务对象
这个任务对象就是我们将要执行的工作,job1是名称,group1是组名。
//指定具体执行的任务Job
IJobDetail sendEmailJob = JobBuilder.Create<SendMailJob>()
.WithIdentity("sendEmailJob", "sendEmailJobGrop")
.WithDescription("定时发送邮件").Build();
4 创建一个触发器
触发器定义了什么时间任务开始或每隔多久执行一次。
//设置触发条件为五秒执行一次
ITrigger sendEmailTrigger = TriggerBuilder.Create()
.WithIdentity("sendEmailTrigger", "sendEmailJobGrop")
.WithDescription("QuartZ")
.WithCronSchedule("*/5 * * * * ?")
.Build();
5 将任务与触发器添加到调度器中并执行
//把策略和任务放入到Scheduler
await scheduler.ScheduleJob(sendEmailJob, sendEmailTrigger);
//执行任务
await scheduler.Start();
三 Cron表达式
1 Cron表达式结构
Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:
Seconds Minutes Hours DayofMonth Month DayofWeek
Seconds Minutes Hours DayofMonth Month DayofWeek Year
Cron表达式从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份
2 各字段的含义
3 特殊符号解释
*
:表示匹配该域的任意值。假如在Minutes域使用*, 即表示每分钟都会触发事件。
?
:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。
-
:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次
/
:表示起始时间开始触发,然后每隔固定时间触发一次。例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.
,
:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
L
:表示最后,只能出现在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。
W
:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。
LW
:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。
#
:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。
4 注意要点
- 单个子表达式可以包含范围或者列表。例如:前面例子中的周中的天这个域(这里是“WED”)可以被替换为MON-FRI,“MON,WED,FRI”或者甚至MON-WED,SAT。
- Cron表达式全程Crontab表达式,是描述Crontab定时任务执行周期的一种语法格式。而Cron表达式严格上来说有许多特别的版本。如:Linux的、Spring的、Quartz框架等表达式,虽说它们总体上来说一类似的,但总会有一些语法的差异在里面。而本文要介绍的是基于Quartz 的Cron表达式。
5 Cron表达式示例
// 秒 分 时 月份中的日期 月 周的天数 年(默认可以不填写)
//注意:一般月份中的日期会和周的天数冲突
“0 0 10,14,16 * * ?” 每天上午10点,下午2点,4点
“0 0/30 9-17 * * ?” 朝九晚五工作时间内每半小时,从0分开始每隔30分钟发送一次
“0 0 12 ? * WED” 表示每个星期三中午12点
“0 0 12 * * ?” 每天中午12点触发
“0 15 10 ? * *” 每天上午10:15触发
“0 15 10 * * ?” 每天上午10:15触发
“0 15 10 * * ? *” 每天上午10:15触发
“0 15 10 * * ? 2005” 2005年的每天上午10:15触发
“0 * 14 * * ?” 在每天下午2点到下午2:59期间的每1分钟触发
“0 0/55 14 * * ?” 在每天下午2点到下午2:59期间,从0开始到55分钟触发
“0 0/55 14,18 * * ?” 在每天下午2点到下午2:59期间和下午6点到6:55期间,从0开始到55分钟触发
“0 0-5 14 * * ?” 在每天下午2点到下午2:05期间的每1分钟触发
“0 10,44 14 ? 3 WED” 每年3月的星期三的下午2:10和2:44触发
“0 15 10 ? * MON-FRI” 周一到周五的上午10:15触发
“0 15 10 15 * ?” 每月15日上午10:15触发
“0 15 10 L * ?” 每月最后一日的上午10:15触发
“0 15 10 ? * 6L” 每月的最后一个星期五上午10:15触发
“0 15 10 ? * 6L 2002-2005” 2002年至2005年的每月的最后一个星期五上午10:15触发
“0 15 10 ? * 6#3” 每月的第三个星期五上午10:15触发
6 表达式生成器
有很多的Cron表达式在线生成器,这里给大家推荐几款:
https://www.pppet.net/
https://www.bejson.com/othertools/cron/
四 Quartz持久化与集群
1 QuartZ持久化
能保证实例重启后job不丢失、 负载均衡缓解服务器压力和解决单点故障问题。
- Quartz集群中每个节点是一个独立的Quartz任务应用,该集群需要对每个节点分别启动或停止。
- 独立的Quartz节点并不与另一个节点或是管理节点通信。Quartz应用是通过共有相同数据库表来感知到另一应用。
- 也就是说只有使用持久化JobStore存储Job和Trigger才能完成Quartz集群。
2 JobStore介绍
用于追踪任务调度相关的所有数据,Quartz提供了两种:
-
RAMJobStore
RAMJobStore是最简单的JobStore,顾名思义这种JobStore将所有的数据都存放在内存中,这也是它运行速度快的原因,但是弊端也很明显:一旦应用结束或者遇到断电所有的数据都会丢失。RAMJobStore是默认的JobStore,我们也已通过下边的代码来显式设置使用的JobStore为RAMJobStore。
quartz.jobStore.type = Quartz.Simpl.RAMJobStore, Quartz -
AdoJobStore
AdoJobStore通过Ado.net将数据存储在数据库中,因此可以解决断电数据丢失的问题,但是因为要读写数据库所以效率相对较低。AdoJobStore官方支持的数据库有:MySql,SqlServer,Sqllite,Oracle等,当前AdoJobStore只有一种类型JobStoreTX,这一点不同于Jave版本,java版本还有JobStoreCMT类型。 -
官方提供的各种数据库脚本
https://github.com/quartznet/quartznet/tree/master/database/tables
3 数据库表说明
Table name | Description |
---|---|
qrtz_blob_triggers | 用来存储Trigger作为Blob类型(用于 Quartz 用户用 JDBC 创建他们自己定制的 Trigger 类型,JobStore 并不知道如何存储实例的时候)。 |
qrtz_calendars | 用来存储日历信息, quartz可配置一个日历来指定一个时间范围。 |
qrtz_cron_triggers | 用来存储触发器 Cron表达式和时区信息。如:调度名称、触发器名称、分组、cron表达式、时区等 |
qrtz_fired_triggers | 用来存储已触发的Trigger相关的状态信息,以及相关联Job的执行信息。 |
qrtz_job_details | 用来存储已配置的Job的详细信息。如:调度名称、集群中job的名称、分组、是否持久化、是否并发等 |
qrtz_locks | 用来存储程序的悲观锁的信息(假如使用了悲观锁)。包括:调度名称、悲观锁名称 |
qrtz_paused_trigger_grps | 用来存储已暂停的Trigger组的信息。包括:调度名称、触发器所属组的名称,qrtz_triggers表的TRIGGER_GROUP的外键 |
qrtz_scheduler_state | 用来存储集群中调度实例信息,quartz会定时读取该表的信息判断集群中每个实例的当前状态。包括:调度名称、调度实例id、上次检查时间、检查间隔时间 |
qrtz_simple_triggers | 用来存储简单的 Trigger,包括重复次数,间隔,以及已触发的次数。 |
qrtz_simprop_triggers | 用来存储存储CalendarIntervalTrigger和DailyTimeIntervalTrigger。 |
qrtz_triggers | 用来存储触发器的基本信息。如:调度名称、触发器名称、分组、上一次&下一次触发时间,优先级,开始&结束时间等 |
4 上代码
- 创建 JobManager.cs
using Quartz;
using Quartz.Impl;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QuartZDemo.Console.Jobs
{
public class JobManager
{
public Task<IScheduler> GetScheduler()
{
try
{
//1.首先创建一个作业调度池
var properties = new NameValueCollection();
//schedule名称
properties["quartz.scheduler.instanceName"] = "MyScheduler";
//自动生成scheduler实例ID,主要为了保证集群中的实例具有唯一标识
properties["quartz.scheduler.instanceId"] = "AUTO";
properties["quartz.threadPool.type"] = "Quartz.Simpl.DefaultThreadPool, Quartz";
//线程池个数
properties["quartz.threadPool.threadCount"] = "20";
properties["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX,Quartz";
//表明前缀
properties["quartz.jobStore.tablePrefix"] = "QRTZ_";
// 序列化类型,必须添加,不添加无法注册数据库
properties["quartz.serializer.type"] = "binary";
//驱动类型
properties["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.MySQLDelegate,Quartz";
//数据源名称
properties["quartz.jobStore.dataSource"] = "quartznet";
//是否配置集群
properties["quartz.jobStore.clustered"] = "true";
properties["quartz.jobStore.clusterCheckinInterval"] = "5000";
//连接字符串
properties["quartz.dataSource.quartznet.connectionString"] = "Database=quartznet;Data Source=127.0.0.1;Port=3306;User Id=root;Password=forever@123456;Charset=utf8;TreatTinyAsBoolean=false;";
//版本
properties["quartz.dataSource.quartznet.provider"] = "MySql";
//最大链接数
properties["quartz.dataSource.quartznet.maxConnections"] = "100";
ISchedulerFactory factory = new StdSchedulerFactory(properties);
return factory.GetScheduler();
}
catch (Exception ex)
{
System.Console.WriteLine(ex);
throw;
}
}
}
}
- 创建任务SendMailJob.cs
using Quartz;
namespace QuartZDemo.Console.Jobs
{
//增加特性保证任务不会重叠执行
[DisallowConcurrentExecution]
public class SendMailJob : IJob
{
//Job类
public Task Execute(IJobExecutionContext context)
{
return Task.Run(() =>
{
//doSomthing
System.Console.WriteLine($"开始发送邮件{DateTime.Now}");
});
}
}
}
-
执行任务Program.cs
// See https://aka.ms/new-console-template for more information using Quartz; using Quartz.Impl; using QuartZDemo.Console.Jobs; Console.WriteLine("begin************************"); IScheduler scheduler = new JobManager().GetScheduler().Result; scheduler.Start(); //指定具体执行的任务Job IJobDetail sendEmailJob = JobBuilder.Create<SendMailJob>() .WithIdentity("sendEmailJob", "sendEmailJobGrop") .WithDescription("定时发送邮件").Build(); //设置触发条件为五秒执行一次 ITrigger sendEmailTrigger = TriggerBuilder.Create() .WithIdentity("sendEmailTrigger", "sendEmailJobGrop") .WithDescription("QuartZ") .WithCronSchedule("*/3 * * * * ?") .Build(); if (scheduler.CheckExists(sendEmailJob.Key).Result) { scheduler.DeleteJob(sendEmailJob.Key).Wait(); } scheduler.ScheduleJob(sendEmailJob, sendEmailTrigger).Wait(); Console.WriteLine("end**************************"); Console.ReadKey();
5 创建Quartz表数据
运行程序Quartz会自动在数据库中记录调度任务相关的数据
- Quartz自动向数据库写入的trigger信息:
- Quartz自动向数据库写入的Job信息:
- 程序执行结果:
- 到这里我们看到了Db持久化已经实现了,但是上边的例子,我们在代码中通过 [“quartz.jobStore.clustered”] = “true” 配置了集群,这个有什么用呢?首先添加一个debug文件夹的副本
- 然后运行这两个文件夹下的xxx.exe文件(如果使用的是.net core,生成的是xxx.dll文件,进入dll文件所在目录,命
令行运行 dotnet xxx.dll 即可启动),运行结果如下:
- 如上所示,运行两个xxx.exe(core中是dll)后,原文件和副本在同一时间只有一个在运行,所以我们调度的任务没有重复执行。如果我们关掉正在执行的那个程序,那么另一个程序会开始执行。我们可以得出结论:Quartz的集群并不会造成任务重复执行,而且当一个服务器挂了后,另一个服务器会自动开始执行,这种机制大大增加了任务调度的容灾性能。
6 注意问题
- 1.Quartz3.x支持async和await,为提高性能,我们最好将Job中的Execute方法都写成异步方法;
- 2.不管使用的是RAMJobStore还是AdoJobStore,千万不要通过代码来直接操作JobStore(比如我们直接通过代码修改数据库中的数据),JobStore让Quartz自动操作即可。无论使用场景是web应用还是桌面程序,我们只使用Scheduler提供的接口方法来实现Job和Trigger等的增/删/改/查/暂停/恢复即可。
- 3.时间同步问题
Quartz实际并不关心你是在相同还是不同的机器上运行节点。当集群放置在不同的机器上时,称之为水平集群。节点跑在同一台机器上时,称之为垂直集群。对于垂直集群,存在着单点故障的问题。这对高可用性的应用来说是无法接受的,因为一旦机器崩溃了,所有的节点也就被终止了。对于水平集群,存在着时间同步问题。节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。最简单的同步计算机时钟的方式是使用某一个Internet时间服务器(Internet Time Server ITS)。 - 4.节点争抢Job问题
因为Quartz使用了一个随机的负载均衡算法,Job以随机的方式由不同的实例执行。Quartz官网上提到当前,还不存在一个方法来指派(钉住) 一个 Job 到集群中特定的节点。
五 Quartz部署模式
1 IIS部署
在MVC框架中集成了Quartz定时调度,此时该调度系统会随着MVC框架被挂在IIS下,IIS会进程回收,所以大部分开发都会遇到Quartz挂在IIS下一段时间不好用。
**补充:**IIS可以设置定时自动回收,默认回收是1740分钟,也就是29小时。IIS自动回收相当于服务器IIS重启,应用程序池内存清空,所有数据被清除,相当于IIS重启,在度量快速开发平台服务器端,为了减小数据库负担,内存中暂存了很多信息,不适合频繁的回收,因为回收会造成服务器端所有存在内存中的数据丢失,如果没有及时保存到数据库中,可能导致程序出现问题。而如果系统使用高峰时期,并不适合回收,回收可能导致几十秒IIS无响应,对于正在工作的人员来说,是一种很不好的体验,会以为是网络或者掉线等问题。
解决方案:关闭该项目在IIS上对应的进程池的回收机制。
如何关闭进程池的回收机制:选中IIS中部署的项目对应的进程池,点击【高级设置】,里面有5个核心参数:
① 发生配置更改时禁止回收:如果为True,应用程序池在发生配置更改时将不会回收。
② 固定时间间隔(分钟):超过设置的时间后,应用程序池回收,设置为:0 意味着应用程序池不回收。系统默认设置的时间是1740(29小时)。
③ 禁用重叠回收:如果为true,将发生应用程序池回收,以便在创建另一个工作进程之前退出现有工作进程
④ 请求限制:应用程序池在回收之前可以处理的最大请求数。如果值为0,则表示应用程序池可以处理的请求数没有限制。
⑤ 生成回收事件日志条目:每发生一次指定的回收事件时便产生一个事件日志条目。
总结:即使可以将IIS进程池回收关掉,仍然不建议把Quartz挂到IIS下,长时间不回收,会存在内存溢出的问题。
2 C/S程序直接运行
用控制台的形式或者Winform的形式单独做一套定时调度系统,与主框架分类,也便于维护,可以直接将exe程序或者Winform窗体程序在服务器上运行。
**总结:**该方法不存在回收的问题,但直接在服务器上运行,容易不小心被运维人员关掉了。
3 借助topshelf来进行的windows服务部署
- 通过NuGet下载 Topshelf 的程序集
- 配置QuartzService类,充当定时任务的服务端程序
- 在主程序中通过topshelf代码调用:HostFactory.Run 详见主程序。(在里面可以设置服务的名称、描述等)
- 通过指令进行服务发布和卸载(查看windows服务:services.msc)
4 Worker Service 部署(推荐)
- 什么是Worker Service?
Worker Service 是使用模板构建的 .NET 项目,该模板提供了一些有用的功能,可以将常规控制台应用程序变得更加强大。Worker Service 运行于宿主(Host)的概念之上,宿主维护应用程序的生命周期。宿主还提供了一些常见的特性,如依赖注入、日志记录和配置。
Worker Service 通常是长时间运行的服务,执行一些规律发生的工作负载。 - 部署依赖
Microsoft.Extensions.Hosting.WindowsServices(windows平台下的服务)
Microsoft.Extensions.Hosting.Systemd(linux平台下的服务)
根据自己需要选择添加,也可以都添加,判断平台注入服务。
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
//是否是windows平台
bool isWinPlantform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
Console.WriteLine($"Window Plantform:{isWinPlantform}");
if (isWinPlantform)
return Host.CreateDefaultBuilder(args)
.UseSystemd()//使用linux服务
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
return Host.CreateDefaultBuilder(args)
.UseWindowsService()//使用windows服务
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
}
}