Quartz.Net 是一款非常流行的任务调度组建,在很多系统中都必然要使用到,今天我就微软最新发布的.Net Core 2.2来介绍一下它的标准使用,也大致说下网上的那些不太正确的使用方式,当然那些也没错。
网上有三种方式来做 .Net core+Quartz 的任务调度,我来分析它,说说的弊端在哪里
第一种:用 Microsoft.AspNetCore.WebHost
来宿主Jobs
服务
在.Net Core 2.1 还没有发布之前,使用 WebHost
宿主Jobs
服务那也无可厚非,毕竟 .Net Core 控制台应用程序还没有官方的宿主方式,自打 .Net Core 2.1 开始,微软新增了 Microsoft.Extensions.Hosting
,其实看源码就是从 Microsoft.AspNetCore.WebHost
中分离出来,做了简化,把Web相关依赖去掉了。
第二种:把任务与触发器都写在了代码中,而没有使用配置文件
比如这样的封装:QuartzHelpers.StartAsync<HelloWorldJob>("0 0 3 * * ?", "myjob", "mytrigger", "mygroup");
第三种:自己实现配置关系的映射,类似如下实现,这个自己实现对大部分用户都是不必要的,因为配置文件就可以做到
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;
}
}
上面说了3个我认为不标准的姿势,现在我来说说标准的姿势,整个demo项目包含6个文件,包括2个配置文件在内
1. 先说说Job
类
namespace QuartzNetCore
{
[DisallowConcurrentExecution]
public class HelloWorldJob : IJob
{
private readonly ILogger<HelloWorldJob> _logger;
public HelloWorldJob(ILogger<HelloWorldJob> logger)
{
_logger = logger;
}
public Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Hello world!");
return Task.CompletedTask;
}
}
}
我得说说DisallowConcurrentExecution
属性,这个属性告诉这个HelloWorldJob
,每次执行都是一个挨一个执行,前面那次没有执行完成,后面那次执行是不允许进去的,也就是禁止HelloWorldJob
的并发执行,对其他Job
的执行不受影响。
2. 聊聊宿主类
namespace QuartzNetCore
{
public class HostServer : IHostedService
{
private readonly ILogger _logger;
private readonly ISchedulerFactory _schedulerFactory;
private readonly IJobFactory _jobFactory;
public HostServer(ILogger<HostServer> logger, ISchedulerFactory schedulerFactory, IJobFactory jobFactory)
{
_logger = logger;
_schedulerFactory = schedulerFactory;
_jobFactory = jobFactory;
}
public IScheduler Scheduler { get; set; }
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("开始Quartz调度...");
Scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
Scheduler.JobFactory = _jobFactory;
await Scheduler.Start(cancellationToken);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("停止Quartz调度...");
await Scheduler.Shutdown(cancellationToken);
}
}
}
IHostedService
接口来之Microsoft.Extensions.Hosting
,是一个轻量级的宿主接口,它提供启动与停止方法,这个对于docker部署非常友好。同时在这个启动方法里面,可以看见重新赋值了JobFactory
,这个是为了可以让Job
类也支持依赖注入。上面的Job
类public HelloWorldJob(ILogger<HelloWorldJob> logger)
就是一个接口注入。
3. 看看任务工厂
namespace QuartzNetCore
{
public class JobFactory : IJobFactory
{
private readonly IServiceProvider _serviceProvider;
public JobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob;
}
public void ReturnJob(IJob job) { }
}
}
这个类的作用就是实现Job
的依赖注入,前面已经提到。
4. 任务与触发器配置
<schedule>
<job>
<name>HelloWorldJob</name>
<group>TestGroup</group>
<description>HelloWorldJob测试任务</description>
<job-type>QuartzNetCore.HelloWorldJob, QuartzNetCore</job-type>
<durable>true</durable>
<recover>false</recover>
</job>
<trigger>
<simple>
<name>TestTrigger</name>
<group>TestGroup</group>
<description>TestTrigger测试触发器</description>
<job-name>HelloWorldJob</job-name>
<job-group>TestGroup</job-group>
<repeat-count>-1</repeat-count>
<repeat-interval>1000</repeat-interval>
</simple>
</trigger>
</schedule>
</job-scheduling-data>
其中job-type
得注意,这个必须是:类名,程序集名。repeat-count
为-1
即无限的次数重复执行,repeart-internval
是执行间隔,1000即为1秒。
5. 配置插件配置,主要指定job
的配置文件,插件的类与程序集,还有线程数等
quartz.scheduler.instanceName = QuartzTest
quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz
quartz.threadPool.threadCount = 10
quartz.threadPool.threadPriority = Normal
quartz.plugin.jobInitializer.type = Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz.Plugins
quartz.plugin.jobInitializer.fileNames = ~/quartz_jobs.xml
我的说说,这个文件是会被自动找到的,只要你将它放到跟目录,Quartz.Plugins
插件会解读它,所以整个项目中你看不见哪个地方有引用它。
6. 启动类
class Program
{
static void Main(string[] args)
{
var host = new HostBuilder()
.ConfigureHostConfiguration(configHost =>
{
configHost.SetBasePath(Directory.GetCurrentDirectory());
//configHost.AddJsonFile("hostsettings.json", true, true);
//configHost.AddEnvironmentVariables("ASPNETCORE_");
//configHost.AddCommandLine(args);
})
.ConfigureAppConfiguration((hostContext, configApp) =>
{
configApp.AddJsonFile("appsettings.json", true);
configApp.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", true);
configApp.AddEnvironmentVariables();
})
.ConfigureServices((hostContext, services) =>
{
services.AddLogging();
services.AddOptions();
services.AddSingleton<IJobFactory, JobFactory>();
services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
services.AddHostedService<HostServer>();
services.AddSingleton<HelloWorldJob>();
})
.ConfigureLogging((hostContext, configLogging) =>
{
configLogging.AddConsole();
})
.UseConsoleLifetime()
.Build();
host.Run();
}
}
设置跟目录,添加配置文件,重要的是添加宿主类HostServer
,还有注册接口与实现,确保依赖注入能正常运行。
总结:这个方式使用Quartz.Net
是最轻量级的,也是具有最小代码的,后面需要添加Job
都是在配置文件中来实现,无需修改c#代码。