前言
以前總結過一篇基於Quartz+Topshelf+.netcore實現定時任務Windows服務 https://www.cnblogs.com/gt1987/p/11806053.html。回顧起來發現有點野路子的感覺,沒有使用.netcore推薦的基於 HostedService
的方式,也沒有體現.net core跨平台的風格。於是重新寫了一個Sample。
Work Service
首先搭建項目框架。
- 版本 .netcore3.1
- 建立一個Console程序項目模板,修改 project 屬性為
<Project Sdk="Microsoft.NET.Sdk.Worker">
。 - Nuget引入
Microsoft.Extensions.Hosting
,支持配置+Logging+注入等基本框架內容。 - Nuget引入
Quartz.Jobs
組件。
后來發現vs2019實際有一個 Worker Service 項目模板,直接選擇建立即可,不用上面這么麻煩~~
QuartzJob、HostedService集成
集成的主要思路為以 HostedService
作為服務承載,啟動的時候 加載 QuartzJob
定時任務配置並啟動。而 HostedService
則自動接入.netcore服務程序體系。
由於 QuartzJob
暫沒有專門的.netcore版本,這里我們首先要作下特別處理,實現一個JobFactory用於集成.net core依賴注入框架。
public class MyJobFactory : IJobFactory
{
private readonly IServiceProvider _serviceProvider;
public MyJobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob;
}
public void ReturnJob(IJob job)
{
//IJob已經在.net core容器體系下,應該考慮通過DI的方式 dispose
//IJob對象的銷毀 if implement IDisposable
//var dispose = job as IDisposable;
//dispose?.Dispose();
}
}
定義一個SampleJob:
[DisallowConcurrentExecution]
public class SampleJob : IJob
{
private readonly ILogger<SampleJob> _logger;
public SampleJob(ILogger<SampleJob> logger)
{
_logger = logger;
}
public void Dispose()
{
_logger.LogInformation($"{nameof(SampleJob)} disposed.");
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation($"{nameof(SampleJob)} executed at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}");
await Task.CompletedTask;
}
}
定義自己的HostedService,在Start方法里面配置了定時任務並啟動
public class QuartzJobHostedService : IHostedService
{
private readonly IScheduler _scheduler;
private readonly ILogger<QuartzJobHostedService> _logger;
public QuartzJobHostedService(IScheduler scheduler,
ILogger<QuartzJobHostedService> logger)
{
_scheduler = scheduler;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
//just for sample test,should configuration in config file when dev
//sample job
var job = CreateJob(typeof(SampleJob));
var trigger = CreateTrigger("SampleJob", "0/5 * * * * ?");
await _scheduler.ScheduleJob(job, trigger, cancellationToken);
//disposed job
var job2 = CreateJob(typeof(DisposedSampleJob));
var trigger2 = CreateTrigger("DisposeSampleJob", "0/10 * * * * ?");
await _scheduler.ScheduleJob(job2, trigger2, cancellationToken);
await _scheduler.Start(cancellationToken);
_logger.LogInformation("jobScheduler started.");
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await _scheduler?.Shutdown(cancellationToken);
_logger.LogInformation("jobScheduler stoped.");
}
private ITrigger CreateTrigger(string name, string cronExpression)
{
return TriggerBuilder
.Create()
.WithIdentity($"{name}.trigger")
.WithCronSchedule(cronExpression)
.Build();
}
private IJobDetail CreateJob(Type jobType)
{
return JobBuilder
.Create(jobType)
.WithIdentity(jobType.FullName)
.WithDescription(jobType.Name)
.Build();
}
}
最后我們看一下服務的啟動配置,注意IScheduler和IJobFactory的生命周期,這里由於 StdSchedulerFactory.GetDefaultScheduler()
的原因,必須是Singleton來兼容。
class Program
{
static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args)
.ConfigureLogging(loggingBuilder =>
{
loggingBuilder.AddConsole();
loggingBuilder.SetMinimumLevel(LogLevel.Information);
})
.ConfigureServices((context, services) =>
{
services.AddTransient<SampleJob>()
.AddTransient<DisposedSampleJob>()
.AddTransient<IDisposableService, DisposableService>()
.AddSingleton<IJobFactory, MyJobFactory>()
.AddSingleton<IScheduler>(sp =>
{
var scheduler = StdSchedulerFactory.GetDefaultScheduler().ConfigureAwait(false).GetAwaiter().GetResult();
scheduler.JobFactory = sp.GetRequiredService<IJobFactory>();
return scheduler;
})
.AddHostedService<QuartzJobHostedService>();
})
//if install by topshelf,don't need this
.UseWindowsService();
}
這樣一個簡單的定時任務服務就搭建完成了,可以本地啟動運行。但是如果要部署到服務器上作為windows服務或者linux服務,還需要作一點額外的工作。
IDisposable 問題
這里插入另外一個話題,我們看到 IJobFactory
有一個 ReturnJob 方法用於處理Job對象的資源釋放問題。但是我們知道在.netcore依賴注入體系下,任何通過注入獲取的對象一定不能通過自己手動方式來處理資源釋放問題。參考官方文檔 https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1#design-services-for-dependency-injection。那么如何處理需要手動釋放例如 IDisposable
的問題呢?
這里我們建立一個繼承 IDisposable
接口服務
//通常情況下 不應該將IDisposebale接口 注冊為 Transient or Scope。改用工廠模式創建
public interface IDisposableService : IDisposable
{
}
public class DisposableService : IDisposableService
{
private ILogger<DisposableService> _logger;
public DisposableService(ILogger<DisposableService> logger)
{
_logger = logger;
}
public void Dispose()
{
_logger.LogInformation($"{nameof(DisposableService)} has disposed at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}.");
}
}
依賴IDisposableService的SampleJob:
/// <summary>
/// 需要dispose
/// 1.IDisposableService可以注冊為Singleton,會自動dispose
/// 2.如果不能注冊單例,則如本例方式通過IServiceProvider.CreateScope方式處理
/// </summary>
[DisallowConcurrentExecution]
public class DisposedSampleJob : IJob
{
private readonly ILogger<DisposedSampleJob> _logger;
private readonly IServiceProvider _serviceProvider;
public DisposedSampleJob(ILogger<DisposedSampleJob> logger,
IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
public async Task Execute(IJobExecutionContext context)
{
//if IDisposableService register Transient,use CreateScope to dispose IDisposableService
//if IDisposableService register singleton,it can be inject directly and dispose automatically
using (var scope = _serviceProvider.CreateScope())
{
var service = scope.ServiceProvider.GetRequiredService<IDisposableService>();
_logger.LogInformation($"{nameof(DisposedSampleJob)} executed at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}.");
await Task.CompletedTask;
}
}
}
這里如注釋,要么將 IDisposableService 注冊為單例,直接注入引用,.net core DI框架會自動處理資源釋放。如果某些原因不能注冊單例,則需要采取不太推薦的 定位器模式
,如上面代碼中實現方式來處理。
部署windows服務
如果要部署到windows服務,則還需要引入 Microsoft.Extensions.Hosting.WindowsServices
組件,在構建 IHostBuilder
的時候加入 UseWindowsService()
即可。它主要的功能是將整個系統的生命周期接入windows服務的生命周期。(默認的是console控制台程序生命周期)。
然后就是煩人的部署到windows服務。這里提供了install和uninstall2個腳本,使用的是window sc工具。
install.bat
set serviceName=QuartzJob.Sample.JobService
set serviceFilePath=F:\gt_work\MyProject\git_project\gt.SomeSamples\QuartzJob.Sample\bin\Release\netcoreapp3.1\QuartzJob.Sample.exe
set serviceDescription=sample job
sc create %serviceName% BinPath=%serviceFilePath%
sc config %serviceName% start=auto
sc description %serviceName% %serviceDescription%
sc start %serviceName%
pause
uninstall.bat
set serviceName=QuartzJob.Sample.JobService
sc stop %serviceName%
sc delete %serviceName%
pause
通過管理員權限啟動即可正常部署和卸載windows服務
部署Linux服務
如果要部署到Linux服務的話,我查到的有兩種方式,一種是Systemd方式,需要引入 Microsoft.Extensions.Hosting.Systemd
組件,加入 UseSystemd
。另一種方式使用SuperVisor來創建服務。這里我嘗試使用了SuperVisor來實現。
我的Linux版本是Centos7,這里剛開始我找了一台Centos6的機器,在安裝.netcore sdk這步就走不下去了,這里注意下。
-
注冊microsoft密鑰
sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm
-
安裝.netcore
sudo yum install dotnet-sdk-3.1
-
安裝SuperVisor
yum install -y supervisor
,這里也許要安裝依賴yum install epel-release
-
配置啟動,在/etc/supervisord.d/ 新建 QuartzJob.Sample.ini 配置文件。directory指向到發布包目錄。
[program:QuartzJob.Sample] command=dotnet QuartzJob.Sample.dll directory=/root/gt/QuartzJob.Sample environment=ASPNETCORE__ENVIRONMENT=Production user=root stopsignal=INT autostart=false autorestart=false startsecs=1 stderr_logfile=/var/log/quartzJob.err.log stdout_logfile=/var/log/quartzJob.out.log
這么做的原因可以查看 /etc/supervisord.conf 配置中這一段 files=supervisord.d/*ini
,表示默認加載啟動supervisord.d目錄下的 .ini文件配置
- 啟動SuperVisor,
sudo service supervisord start
。服務正常啟動
集成Topshelf
Topshelf組件在framework時代是一款非常方便生成服務windows服務的工具,可以通過代碼的方式配置並生成windows服務。可惜目前沒有看到.netcore的配合版本。且由於依賴windows系統,似乎不太切合.netcore跨平台的特性。這里給出集成的方式,只適用windows平台。
static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
HostFactory.Run(x =>
{
x.Service<IHost>(s =>
{
s.ConstructUsing(n => host);
s.WhenStarted(tc => tc.StartAsync());
s.WhenStopped(tc => tc.StopAsync().Wait());
});
x.RunAsLocalSystem();
x.SetDisplayName("Quartz.Sample.JobService");
x.SetServiceName("Quartz.Sample.JobService");
});
}
install 命令:
- .\QuartzJob.Sample.exe install
- .\QuartzJob.Sample.exe start
uninstall 命令:
- .\QuartzJob.Sample.exe stop
- .\QuartzJob.Sample.exe uninstall
這里的原理就是用Topshelf的Host替代.netcore的Host,在Topshelf Host啟動時再啟動.netcore Host。反正看着很變扭。
另外特別注意 s.WhenStarted(tc => tc.StartAsync());
這里使用的是StartAsync方法而不是Start方法,因為Start方法是同步堵塞的,在部署到windows服務時,由於這一步堵塞,會導致windows服務一直卡在啟動狀態直至超時啟動失敗。