查看原文
其他

.NET Core+Topshelf+Quartz创建Windows定时任务服务

DotNet 2021-09-23

(给DotNet加星标,提升.Net技能


转自:丨秋风扫落叶 
cnblogs.com/fast-song/p/11764304.html

准备工作


创建.NET Core 控制台应用程序,这里不做过多介绍


  • 添加TopShelf包:TopShelf;


  • 添加Quartz包:Quartz、Quartz.Plugins;


  • 添加依赖注入包:Microsoft.Extensions.DependencyInjection;


  • 添加读取配置文件包:Microsoft.Extensions.Configuration.Json;


  • 添加访问数据库包:Microsoft.EntityFrameworkCore;


  • 添加日志包:Serilog、Serilog.Sinks.Console、Serilog.Sinks.File


配置Quartz


创建appsettings.json文件,右键文件属性,并更改属性为始终复制 内容


{
"quartz": {
"scheduler": {
"instanceName": "Job"
},
"threadPool": {
"type": "Quartz.Simpl.SimpleThreadPool, Quartz",
"threadPriority": "Normal",
"threadCount": 10
},
"plugin": {
"jobInitializer": {
"type": "Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz.Plugins",
"fileNames": "quartz_jobs.xml"
}
}
}
}


创建QuartzOption 类


namespace Job
{
public class QuartzOption
{
public QuartzOption(IConfiguration config)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}
var section = config.GetSection("quartz");
section.Bind(this);
}
public Scheduler Scheduler { get; set; }
public ThreadPool ThreadPool { get; set; }
public Plugin Plugin { get; set; }
public NameValueCollection ToProperties()
{
var properties = new NameValueCollection
{
["quartz.scheduler.instanceName"] = Scheduler?.InstanceName,
["quartz.threadPool.type"] = ThreadPool?.Type,
["quartz.threadPool.threadPriority"] = ThreadPool?.ThreadPriority,
["quartz.threadPool.threadCount"] = ThreadPool?.ThreadCount.ToString(),
["quartz.plugin.jobInitializer.type"] = Plugin?.JobInitializer?.Type,
["quartz.plugin.jobInitializer.fileNames"] = Plugin?.JobInitializer?.FileNames
};
return properties;
}
}
public class Scheduler
{
public string InstanceName { get; set; }
}
public class ThreadPool
{
public string Type { get; set; }
public string ThreadPriority { get; set; }
public int ThreadCount { get; set; }
}
public class Plugin
{
public JobInitializer JobInitializer { get; set; }
}
public class JobInitializer
{
public string Type { get; set; }
public string FileNames { get; set; }
}
}


添加一个Job


namespace Job
{
public class SyncJob : IJob
{
private readonly IService _service;
public SyncJob(IService service)
{
_service = service;
}
public async Task Execute(IJobExecutionContext context)
{
Log.Information("同步开始...");
_service.DoSomeThing();
}
}
}


实现IJobFactory


namespace Job
{
public class JobFactory : IJobFactory
{
protected readonly IServiceProvider Container;
public JobFactory(IServiceProvider container)
{
Container = container;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
return Container.GetService(bundle.JobDetail.JobType) as IJob;
}
public void ReturnJob(IJob job)
{
(job as IDisposable)?.Dispose();
}
}
}


创建Quartz调度的配置文件 quartz_jobs.xml


文件名与appsetting中的quartz.plugin.jobInitializer.fileNames 保持一致


<?xml version="1.0" encoding="UTF-8"?>
<job-scheduling-data xmlns="http://quartznet.sourceforge.net/JobSchedulingData"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="2.0">

<processing-directives>
<overwrite-existing-data>true</overwrite-existing-data>
</processing-directives>
<schedule>
<job>
<name>SyncJob</name>
<group>SyncGroup</group>
<description>数据同步任务</description>
<job-type>Mille.Job.SyncJob, Mille.Job</job-type>
<durable>true</durable>
<recover>false</recover>
</job>
<trigger>
<cron>
<name>SyncTrigger</name>
<group>SyncGroup</group>
<description>同步触发器</description>
<job-name>SyncDJob</job-name>
<job-group>SyncGroup</job-group>
<!--每晚23:50跑一次,具体参见cron表达式-->
<cron-expression>0 50 23 ? * *</cron-expression>
</cron>
</trigger>
<!--<trigger>
<simple>
<name>SyncTrigger</name>
<group>SyncGroup</group>
<description>数据同步触发器</description>
<job-name>SyncJob</job-name>
<job-group>SyncGroup</job-group>
<repeat-count>-1</repeat-count>
2s跑一次
<repeat-interval>2000</repeat-interval>
</simple>
</trigger>-->

</schedule>
</job-scheduling-data>


添加一个类,此类用户服务启动调用


namespace Job
{
public class SyncService
{
public async Task StartAsync()
{
var provider = RegisterServices();
Scheduler = provider.GetService(typeof(IScheduler)) as IScheduler;
await Scheduler.Start();
Log.Information("Quartz调度已启动...");
}
public async Task StopAsync()
{
await Scheduler.Shutdown();
Log.Information("Quartz调度结束...");
Log.CloseAndFlush();
}
#region Utils
private IScheduler Scheduler { get; set; }
private static ServiceProvider RegisterServices()
{
Log.Information("配置依赖注入...");
var configuration = ReadFromAppSettings();
var services = new ServiceCollection();
#region
services.AddScoped<SyncService>();
services.AddDbContext<DataContext>(opt => opt.UseMySql(configuration.GetConnectionString("ConnStr")));
services.AddScoped<IService,Service>();
#endregion

#region Quartz
Log.Information("配置Quartz...");
services.AddScoped<IJobFactory, JobFactory>();
services.AddSingleton(service =>
{
var option = new QuartzOption(configuration);
var sf = new StdSchedulerFactory(option.ToProperties());
var scheduler = sf.GetScheduler().Result;
scheduler.JobFactory = service.GetService<IJobFactory>();
return scheduler;
});
services.AddScoped<SyncJob>();
//此处不能写成services.AddScoped<IJob,SyncJob>(); 会造成在找不到SyncJob
#endregion
var provider = services.BuildServiceProvider();
return provider;
}
private static IConfigurationRoot ReadFromAppSettings()
{
//读取appsettings.json
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false)
.Build();
}
#endregion
}
}


配置TopShelf


详情参见 https://topshelf.readthedocs.io/en/latest/configuration/quickstart.html


namespace Job
{
public class Program
{
public static void Main(string[] args)
{
InstanceLog();
var rc = HostFactory.Run(x =>
{
x.Service<SyncService>(s =>
{
s.ConstructUsing(name => new SyncService());
s.WhenStarted(async tc => await tc.StartAsync()); //调用此方法前勿有太多操作,会造成服务启动失败
s.WhenStopped(async tc => await tc.StopAsync());
});
x.RunAsLocalSystem();
x.SetDescription("SyncJob Description");
x.SetDisplayName("SyncJob DisplayName");
x.SetServiceName("SyncJob ServiceName");
});
var exitCode = (int)Convert.ChangeType(rc, rc.GetTypeCode());
Environment.ExitCode = exitCode;
}
private static void InstanceLog()
{
//配置Serilog
var template = "{Timestamp:HH:mm:ss} [{Level:u3}] {Message}{NewLine}{Exception}";
Log.Logger = new LoggerConfiguration()
.WriteTo.File(path: "logs/log.txt", outputTemplate: template, rollingInterval: RollingInterval.Day)
.WriteTo.Console(LogEventLevel.Information)
.CreateLogger();
}
}
}


然后在项目文件中加上项目的运行环境相关配置


<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<RuntimeIdentifier>win7-x64</RuntimeIdentifier>//不同的环境RID不同
</PropertyGroup>


运行


编译项目之后,进入到/bin/Debug/netcoreapp3.0/win7-x64目录,在此处以管理员运行cmd,然后执行 依次Job.exe install 安装服务, Job.exe start 启动服务


如果遇到启动服务时报 1053错误:服务没有及时响应启动或控制请求。检查Start函数调用之前是否还有其他操作,如有,请将这些操作移动到Start调用后执行;本人就是由于在Start之前执行了依赖注入等操作,导致服务启动失败,故写下这篇文章


推荐阅读

(点击标题可跳转阅读)

.NET Core实现健康检查

.NET Core 3.0 进行跨平台 IoT 编程

.NET Core 3.0中使用SignalR实现实时通信


看完本文有收获?请转发分享给更多人

关注「DotNet」加星标,提升.Net技能 

好文章,我在看❤️

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存