quartz3.0.7和topshelf4.2.1实现任务调度
分类:
IT文章
•
2025-02-06 17:04:07
我们都知道quartz是实现任务调度的,也就是定时计划,topshelf是创建windows服务。网上看过很多的例子,都不够完整,于是自己去翻了一遍quart.Net的源码,现在我就来根据quartz.net的源码例子,简单改造一下,基本上也就符合我们的工作使用了,同时quartz.Net还支持集群,可看文章末尾转至quartz.net进行参考。
一、创建一个控制台程序

二、引入相关插件
引入以下插件,可以通过管理NuGet程序包添加,我是通过程序包控制台添加的,如下:
PM> install-package quartz (安装这个时候同时会安装: quartz.job)
PM> install-package quartz.plugins
PM> install-package topshelf
我们还会使用到日志相关的,原官方的例子是用log4net, 我这里改了一下就用serilog
PM> install-package serilog
PM> install-package serilog.sinks.file (写入日志到文件)
PM> install-package serilog.sinks.console (输出日志到控制台)

三、复制相关文件到新建的项目中
1. Configuration.cs
2. IQuartzServer.cs
3. QuartzServer.cs
4. QuartzServerFactory.cs
5. quartz.config
6. quartz_jobs.xml
7. SampleJob.cs (实现了IJob接口)
8. LoggerProvider.cs (serilog日志相关)
9. Test.Production.config (单独的配置文件)

四、代码部分
/// <summary>
/// Configuration for the Quartz server.
/// </summary>
public class Configuration
{
//private static readonly ILog log = LogManager.GetLogger(typeof(Configuration));
private const string PrefixServerConfiguration = "QuartzNetWindowsService";
private const string KeyServiceName = PrefixServerConfiguration + ".serviceName";
private const string KeyServiceDisplayName = PrefixServerConfiguration + ".serviceDisplayName";
private const string KeyServiceDescription = PrefixServerConfiguration + ".serviceDescription";
private const string KeyServerImplementationType = PrefixServerConfiguration + ".type";
private const string DefaultServiceName = "TestService";
private const string DefaultServiceDisplayName = "TestService";
private const string DefaultServiceDescription = "Quartz Job Scheduling Server";
private static readonly string DefaultServerImplementationType = typeof(QuartzServer).AssemblyQualifiedName;
private static readonly NameValueCollection configuration;
/// <summary>
/// Initializes the <see cref="Configuration"/> class.
/// </summary>
static Configuration()
{
try
{
configuration = (NameValueCollection) ConfigurationManager.GetSection("quartz");
}
catch (Exception e)
{
//log.Warn("could not read configuration using ConfigurationManager.GetSection: " + e.Message);
}
}
/// <summary>
/// Gets the name of the service.
/// </summary>
/// <value>The name of the service.</value>
public static string ServiceName => GetConfigurationOrDefault(KeyServiceName, DefaultServiceName);
/// <summary>
/// Gets the display name of the service.
/// </summary>
/// <value>The display name of the service.</value>
public static string ServiceDisplayName => GetConfigurationOrDefault(KeyServiceDisplayName, DefaultServiceDisplayName);
/// <summary>
/// Gets the service description.
/// </summary>
/// <value>The service description.</value>
public static string ServiceDescription => GetConfigurationOrDefault(KeyServiceDescription, DefaultServiceDescription);
/// <summary>
/// Gets the type name of the server implementation.
/// </summary>
/// <value>The type of the server implementation.</value>
public static string ServerImplementationType => GetConfigurationOrDefault(KeyServerImplementationType, DefaultServerImplementationType);
/// <summary>
/// Returns configuration value with given key. If configuration
/// for the does not exists, return the default value.
/// </summary>
/// <param name="configurationKey">Key to read configuration with.</param>
/// <param name="defaultValue">Default value to return if configuration is not found</param>
/// <returns>The configuration value.</returns>
private static string GetConfigurationOrDefault(string configurationKey, string defaultValue)
{
string retValue = null;
if (configuration != null)
{
retValue = configuration[configurationKey];
}
if (retValue == null || retValue.Trim().Length == 0)
{
retValue = defaultValue;
}
return retValue;
}
}
}
代码:Configuration.cs
/// <summary>
/// Service interface for core Quartz.NET server.
/// </summary>
public interface IQuartzServer
{
/// <summary>
/// Initializes the instance of <see cref="IQuartzServer"/>.
/// Initialization will only be called once in server's lifetime.
/// </summary>
Task Initialize();
/// <summary>
/// Starts this instance.
/// </summary>
void Start();
/// <summary>
/// Stops this instance.
/// </summary>
void Stop();
/// <summary>
/// Pauses all activity in scheduler.
/// </summary>
void Pause();
/// <summary>
/// Resumes all activity in server.
/// </summary>
void Resume();
}
}
代码:IQuartzServer.cs
/// <summary>
/// The main server logic.
/// </summary>
public class QuartzServer : ServiceControl, IQuartzServer
{
// private readonly ILog logger;
private ISchedulerFactory schedulerFactory;
private IScheduler scheduler;
/// <summary>
/// Initializes a new instance of the <see cref="QuartzServer"/> class.
/// </summary>
public QuartzServer()
{
// logger = LogManager.GetLogger(GetType());
}
/// <summary>
/// Initializes the instance of the <see cref="QuartzServer"/> class.
/// </summary>
public virtual async Task Initialize()
{
try
{
schedulerFactory = CreateSchedulerFactory();
scheduler = await GetScheduler().ConfigureAwait(false);
}
catch (Exception e)
{
// logger.Error("Server initialization failed:" + e.Message, e);
throw;
}
}
/// <summary>
/// Gets the scheduler with which this server should operate with.
/// </summary>
/// <returns></returns>
protected virtual Task<IScheduler> GetScheduler()
{
return schedulerFactory.GetScheduler();
}
/// <summary>
/// Returns the current scheduler instance (usually created in <see cref="Initialize" />
/// using the <see cref="GetScheduler" /> method).
/// </summary>
protected virtual IScheduler Scheduler => scheduler;
/// <summary>
/// Creates the scheduler factory that will be the factory
/// for all schedulers on this instance.
/// </summary>
/// <returns></returns>
protected virtual ISchedulerFactory CreateSchedulerFactory()
{
return new StdSchedulerFactory();
}
/// <summary>
/// Starts this instance, delegates to scheduler.
/// </summary>
public virtual void Start()
{
try
{
scheduler.Start();
//// define the job and tie it to our HelloJob class
//IJobDetail job = JobBuilder.Create<HelloJob>()
// .WithIdentity("job1", "group1")
// .Build();
}
catch (Exception ex)
{
// logger.Fatal($"Scheduler start failed: {ex.Message}", ex);
throw;
}
//logger.Info("Scheduler started successfully");
}
/// <summary>
/// Stops this instance, delegates to scheduler.
/// </summary>
public virtual void Stop()
{
try
{
scheduler.Shutdown(true);
}
catch (Exception ex)
{
// logger.Error($"Scheduler stop failed: {ex.Message}", ex);
throw;
}
//logger.Info("Scheduler shutdown complete");
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public virtual void Dispose()
{
// no-op for now
}
/// <summary>
/// Pauses all activity in scheduler.
/// </summary>
public virtual void Pause()
{
scheduler.PauseAll();
}
/// <summary>
/// Resumes all activity in server.
/// </summary>
public void Resume()
{
scheduler.ResumeAll();
}
/// <summary>
/// TopShelf's method delegated to <see cref="Start()"/>.
/// </summary>
public bool Start(HostControl hostControl)
{
Start();
return true;
}
/// <summary>
/// TopShelf's method delegated to <see cref="Stop()"/>.
/// </summary>
public bool Stop(HostControl hostControl)
{
Stop();
return true;
}
/// <summary>
/// TopShelf's method delegated to <see cref="Pause()"/>.
/// </summary>
public bool Pause(HostControl hostControl)
{
Pause();
return true;
}
/// <summary>
/// TopShelf's method delegated to <see cref="Resume()"/>.
/// </summary>
public bool Continue(HostControl hostControl)
{
Resume();
return true;
}
}
}
代码:QuartzServer.cs
/// <summary>
/// Factory class to create Quartz server implementations from.
/// </summary>
public class QuartzServerFactory
{
// private static readonly ILog logger = LogManager.GetLogger(typeof (QuartzServerFactory));
/// <summary>
/// Creates a new instance of an Quartz.NET server core.
/// </summary>
/// <returns></returns>
public static QuartzServer CreateServer()
{
string typeName = Configuration.ServerImplementationType;
Type t = Type.GetType(typeName, true);
// logger.Debug("Creating new instance of server type '" + typeName + "'");
QuartzServer retValue = (QuartzServer) Activator.CreateInstance(t);
// logger.Debug("Instance successfully created");
return retValue;
}
}
}
代码:QuartzServerFactory.cs
# You can configure your scheduler in either <quartz> configuration section
# or in quartz properties file
# Configuration section has precedence
quartz.scheduler.instanceName = ServerScheduler
# configure thread pool info
quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz
quartz.threadPool.threadCount = 60
# 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 = 21004
quartz.scheduler.exporter.bindName = QuartzScheduler
quartz.scheduler.exporter.channelType = tcp
quartz.scheduler.exporter.channelName = httpQuartz
配置文件:quartz.config
<?xml version="1.0" encoding="UTF-8"?>
<!-- This file contains job definitions in schema version 2.0 format -->
<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>
<!--ProductTaskJob-->
<job>
<name>ProductTaskJob</name>
<group>ProductTaskJobGroup</group>
<description>ProductTaskJob for Quartz Server</description>
<job-type>QuartzNetWindowsService.Order.ProductTaskJob, QuartzNetWindowsService</job-type>
<durable>true</durable>
<recover>false</recover>
</job>
<trigger>
<cron>
<name>ProductTaskJobTrigger</name>
<group>ProductTaskJobSimpleGroup</group>
<description>ProductTaskJob trigger List</description>
<job-name>ProductTaskJob</job-name>
<job-group>ProductTaskJobGroup</job-group>
<cron-expression>0/5 * * * * ?</cron-expression>
</cron>
</trigger>
<!--ListJob-->
<job>
<name>ListJob</name>
<group>ListJobGroup</group>
<description>ListJob for Quartz Server</description>
<job-type>QuartzNetWindowsService.Order.ListJob, QuartzNetWindowsService</job-type>
<durable>true</durable>
<recover>false</recover>
</job>
<trigger>
<cron>
<name>ListJobTrigger</name>
<group>ListJobSimpleGroup</group>
<description>ListJob trigger List</description>
<job-name>ListJob</job-name>
<job-group>ListJobGroup</job-group>
<cron-expression>0/5 * * * * ?</cron-expression>
</cron>
</trigger>
<job>
<name>sampleJob</name>
<group>sampleGroup</group>
<description>Sample job for Quartz Server</description>
<job-type>QuartzNetWindowsService.SampleJob, QuartzNetWindowsService</job-type>
<durable>true</durable>
<recover>false</recover>
<job-data-map>
<entry>
<key>key1</key>
<value>value1</value>
</entry>
<entry>
<key>key2</key>
<value>value2</value>
</entry>
</job-data-map>
</job>
<trigger>
<simple>
<name>sampleSimpleTrigger</name>
<group>sampleSimpleGroup</group>
<description>Simple trigger to simply fire sample job</description>
<job-name>sampleJob</job-name>
<job-group>sampleGroup</job-group>
<misfire-instruction>SmartPolicy</misfire-instruction>
<repeat-count>-1</repeat-count>
<repeat-interval>10000</repeat-interval>
</simple>
</trigger>
<trigger>
<cron>
<name>sampleCronTrigger</name>
<group>sampleCronGroup</group>
<description>Cron trigger to simply fire sample job</description>
<job-name>sampleJob</job-name>
<job-group>sampleGroup</job-group>
<misfire-instruction>SmartPolicy</misfire-instruction>
<cron-expression>0/10 * * * * ?</cron-expression>
</cron>
</trigger>
<trigger>
<calendar-interval>
<name>sampleCalendarIntervalTrigger</name>
<group>sampleCalendarIntervalGroup</group>
<description>Calendar interval trigger to simply fire sample job</description>
<job-name>sampleJob</job-name>
<job-group>sampleGroup</job-group>
<misfire-instruction>SmartPolicy</misfire-instruction>
<repeat-interval>15</repeat-interval>
<repeat-interval-unit>Second</repeat-interval-unit>
</calendar-interval>
</trigger>
</schedule>
</job-scheduling-data>
job配置文件:quartz_jobs.xml
/// <summary>
/// A sample job that just prints info on console for demostration purposes.
/// </summary>
public class SampleJob : IJob
{
/// <summary>
/// Called by the <see cref="IScheduler" /> when a <see cref="ITrigger" />
/// fires that is associated with the <see cref="IJob" />.
/// </summary>
/// <remarks>
/// The implementation may wish to set a result object on the
/// JobExecutionContext before this method exits. The result itself
/// is meaningless to Quartz, but may be informative to
/// <see cref="IJobListener" />s or
/// <see cref="ITriggerListener" />s that are watching the job's
/// execution.
/// </remarks>
/// <param name="context">The execution context.</param>
public async Task Execute(IJobExecutionContext context)
{
LoggerProvider.LogInfomation(nameof(SampleJob) + "正在执行中。。。。。。。。。。。。");
await Task.Delay(1);
}
}
}
代码:SampleJob.cs
class Constants
{
public const string logPath = "serilog:write-to:File.path";
public const string logLimitFileSize = "serilog:write-to:File.fileSizeLimitBytes";
}
/// <summary>
/// Serilog日志
/// </summary>
public class LoggerProvider
{
private readonly static ILogger log;
static LoggerProvider()
{
log = new LoggerConfiguration()
.WriteTo.File(
path: ConfigurationManager.AppSettings[Constants.logPath],
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: Convert.ToInt32(ConfigurationManager.AppSettings[Constants.logLimitFileSize]),
rollOnFileSizeLimit: true
).CreateLogger();
Console.WriteLine(1122);
}
public static void LogInfomation(string message, Exception exception = null)
{
log.Information(exception, message);
}
public static void LogDebug(string message, Exception exception = null)
{
log.Debug(exception, message);
}
public static void LogWarning(string message, Exception exception = null)
{
log.Warning(exception, message);
}
public static void LogError(string message, Exception exception = null)
{
log.Error(exception, message);
}
}
}
日志相关文件:LoggerProvider.cs
这里设置日志输出的路径和设置日志的文件的大小
<?xml version="1.0" encoding="utf-8" ?>
<appSettings>
<add key="serilog:write-to:File.path" value="Logs est-.txt" />
<add key="serilog:write-to:File.fileSizeLimitBytes" value="1234567" />
</appSettings>
配置文件:Test.Production.config
五、修改控制台程序入口

static class Program
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
static void Main()
{
Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
HostFactory.Run(x =>
{
x.RunAsLocalSystem();
x.SetDescription(Configuration.ServiceDescription);
x.SetDisplayName(Configuration.ServiceDisplayName);
x.SetServiceName(Configuration.ServiceName);
x.Service(factory =>
{
QuartzServer server = QuartzServerFactory.CreateServer();
server.Initialize().GetAwaiter().GetResult();
return server;
});
});
}
}
}
六、修改配置文件为始终复制


七、app.config文件修改,注意标记红色代码
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="quartz" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup>
<appSettings configSource="Test.Production.config"></appSettings>
</configuration>
八、新建一个类库,创建一个产品业务相关类:ProductAppService
然后 QuartzNetWindowsService 引用 类库 ProductAppService,并且新建一个ProductTaskJob:IJob ,实现IJob接口
public class ProductAppService
{
public string GetProductList()
{
return nameof(ProductAppService) + ":我获取到产品列表啦!";
}
}
public class ProductTaskJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
//具体业务实现
var result = new BMYAppService.ProductAppService().GetProductList();
//输出日志
LoggerProvider.LogInfomation(result);
await Task.Delay(1);
}
}
}
九、把控制台安装到电脑中变成服务
新建一个文本文件,命名为:安装服务.bat ,注意:另存为时候,编码格式选择为:ANSI ,保存后再修改后缀名为*.bat,卸载服务也是一样的,这里路径可以自己设置,我为了方便直接复制程序运行路径


看下我的安装结果,并查看相关日志记录,证明已经在运行了!




至此,整个例子实现完成。如有疑问或错误请留言指正。谢谢!
quart.net 参考文献:https://www.quartz-scheduler.net/
quartz.net github:https://github.com/quartznet/quartznet/releases
job文件相关表达式执行配置参考:http://qqe2.com/cron
Demo下载:https://download.****.net/download/u011195704/11982812