前言

以前总结过一篇基于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服务一直卡在启动状态直至超时启动失败。

posted on 2020-11-03 16:47  gt1987  阅读(2162)  评论(7编辑  收藏  举报