1. 程式人生 > >.Net Core小技巧 - Hosted Services + Quartz實現定時任務調度

.Net Core小技巧 - Hosted Services + Quartz實現定時任務調度

TBase 觸發 控制 soft ghost .so 不錯 什麽 ora

背景

  之前一直有朋友問,.Net Core + Linux環境有沒有類似Windows服務的東西。其實是有的,我了解的方法有兩種:

  #1 創建一個ASP.Net Core的Web項目(如Web API),然後通過添加中間件(Middleware)的方式來啟動任務;

  #2 創建一個.Net Core的項目,添加Host,Dependency Injection,Configuration等組件,然後通過Main方法或中間件的方式啟動服務。

  但是,上述兩種方法都有點不足,如:

  #1 會把Web的生命周期引進來,但實際上,我們並不需要Web的功能,如Controller;

  #2 本身是沒有問題的,但是對開發者的要求相對高一點點,需要對.Net Core的各個組成部分都有一定的認識,簡而言之,門檻有一丟丟高。

  .Net Core 2.1推出了一個Generic Host的概念,可以很好的解決上面兩種方法的不足:

  技術分享圖片

  至於為什麽選擇Quartz來做調度,我想可能是因為情懷吧,因為之前是用的TopShelf+Quartz,其實Hangfire也不錯。

使用Hosted Service

1. 創建一個控制臺程序。

技術分享圖片

2. 添加Host Nuget包。

Install-Package Microsoft.Extensions.Hosting -Version 2.1.0
Install-Package Microsoft.Extensions.Configuration.FileExtensions -Version 2.1.0
Install-Package Microsoft.Extensions.Configuration.Json -Version 2.1.0
Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables -Version 2.1.0
Install-Package Microsoft.Extensions.Configuration.CommandLine -Version 2.1.0
Install-Package Microsoft.Extensions.Logging.Console -Version 2.1.0
Install-Package Microsoft.Extensions.Logging.Debug -Version 2.1.0

3. 添加一個基於Timer的簡單Hosted Service(用於簡單演示),繼承IHostedService。

internal class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is starting.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        _logger.LogInformation(string.Format("[{0:yyyy-MM-dd hh:mm:ss}]Timed Background Service is working.", DateTime.Now));
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

4. Main函數中添加Host的相關代碼。

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(); //configApp.AddCommandLine(args); }) .ConfigureServices((hostContext, services) => { services.AddLogging(); services.AddHostedService<TimedHostedService>(); }) .ConfigureLogging((hostContext, configLogging) => { configLogging.AddConsole(); if (hostContext.HostingEnvironment.EnvironmentName == EnvironmentName.Development) { configLogging.AddDebug(); } }) .UseConsoleLifetime() .Build(); host.Run();

5. 查看結果

技術分享圖片

6. 代碼解析

a. Host配置

.ConfigureHostConfiguration(configHost =>

{

  //配置根目錄

  configHost.SetBasePath(Directory.GetCurrentDirectory());

  //讀取host的配置json,和appsetting類似,暫不需要先註釋掉,可根據需要開啟

  //configHost.AddJsonFile("hostsettings.json", true, true);

  //讀取環境變量,Asp.Net core默認的環境變量是以ASPNETCORE_作為前綴的,這裏也采用此前綴以保持一致

  configHost.AddEnvironmentVariables("ASPNETCORE_");

  //可以在啟動host的時候之前可傳入參數,暫不需要先註釋掉,可根據需要開啟

  //configHost.AddCommandLine(args);

})

b. App配置

.ConfigureAppConfiguration((hostContext, configApp) =>

{

  //讀取應用的配置json

  configApp.AddJsonFile("appsettings.json", true);

  //讀取應用特定環境下的配置json

  configApp.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", true);

  //讀取環境變量

  configApp.AddEnvironmentVariables();

  //可以在啟動host的時候之前可傳入參數,暫不需要先註釋掉,可根據需要開啟

  //configApp.AddCommandLine(args);

})

c. 配置服務及依賴註入註冊,註:沒有Middleware的配置了。

.ConfigureServices((hostContext, services) =>
{

  //添加日誌Service
  services.AddLogging();

  //添加Timer Hosted Service
  services.AddHostedService<TimedHostedService>();
})

d. 日誌配置

.ConfigureLogging((hostContext, configLogging) =>
{

  //輸出控制臺日誌
  configLogging.AddConsole();

  //開發環境輸出Debug日誌
  if (hostContext.HostingEnvironment.EnvironmentName == EnvironmentName.Development)
  {
    configLogging.AddDebug();
  }
})

e. 使用控制臺生命周期

.UseConsoleLifetime() //使用Ctrl + C退出

其它詳細的可參考:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.1

使用Quartz

1. 添加Host Nuget包。

Install-Package Quartz -Version 3.0.5
Install-Package Quartz.Plugins -Version 3.0.5

2. Quartz配置。

之前Quartz的配置是放在quartz.config裏面的,但我更喜歡使用appsettings.json,因此,把配置改成了從appsettings.json。

先建一個QuartzOption的類:

/// <summary>
/// 更多設置請參考:https://github.com/quartznet/quartznet/blob/master/src/Quartz/Impl/StdSchedulerFactory.cs
/// </summary>
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; }
}

3. 重寫JobFactory。

public class JobFactory : IJobFactory
{
    private readonly IServiceProvider _serviceProvider;

    public JobFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
    {
        var job = _serviceProvider.GetService(bundle.JobDetail.JobType) as IJob;
        return job;
    }

    public void ReturnJob(IJob job)
    {
    }
}

4. 編寫Quartz Hosted Service

public class QuartzService : IHostedService
{
    private readonly ILogger _logger;
    private readonly IScheduler _scheduler;

    public QuartzService(ILogger<QuartzService> logger, IScheduler scheduler)
    {
        _logger = logger;
        _scheduler = scheduler;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("開始Quartz調度...");
        await _scheduler.Start(cancellationToken);
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("停止Quartz調度...");
        await _scheduler.Shutdown(cancellationToken);
    }
}

5. 準備appsettings.json

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

6. 編寫一個TestJob

public class TestJob : IJob
{
    private readonly ILogger _logger;

    public TestJob(ILogger<TestJob> logger)
    {
        _logger = logger;
    }

    public Task Execute(IJobExecutionContext context)
    {
        _logger.LogInformation(string.Format("[{0:yyyy-MM-dd hh:mm:ss:ffffff}]任務執行!", DateTime.Now));
        return Task.CompletedTask;
    }
}

7. 準備Quartz的調度文件quartz_jobs.xml

<?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>TestJob</name>
      <group>TestGroup</group>
      <description>測試任務</description>
      <job-type>HostedService.Quartz.Jobs.TestJob, HostedService.Quartz</job-type>
      <durable>true</durable>
      <recover>false</recover>
    </job>
    <trigger>
      <simple>
        <name>TestTrigger</name>
        <group>TestGroup</group>
        <description>測試觸發器</description>
        <job-name>TestJob</job-name>
        <job-group>TestGroup</job-group>
        <repeat-count>-1</repeat-count>
        <repeat-interval>2000</repeat-interval>
      </simple>
    </trigger>

    <!--<trigger>
      <cron>
        <name>TestTrigger</name>
        <group>TestGroup</group>
        <description>測試觸發器</description>
        <job-name>TestJob</job-name>
        <job-group>TestGroup</job-group>
        <cron-expression>0/2 * * * * ?</cron-expression>
      </cron>
    </trigger>-->
  </schedule>
</job-scheduling-data>

8. 註冊Quartz Hosted Service和TestJob

.ConfigureServices((hostContext, services) =>
{
    services.AddLogging();
    //services.AddHostedService<TimedHostedService>();

    services.AddSingleton<IJobFactory, JobFactory>();
    services.AddSingleton(provider =>
    {
        var option = new QuartzOption(hostContext.Configuration);
        var sf = new StdSchedulerFactory(option.ToProperties());
        var scheduler = sf.GetScheduler().Result;
        scheduler.JobFactory = provider.GetService<IJobFactory>();
        return scheduler;
    });
    services.AddHostedService<QuartzService>();

    services.AddSingleton<TestJob, TestJob>();
})

9. 查看結果

技術分享圖片

10. 補充說明。

Generic Service默認的環境是Production,如果想使用Development環境,可以在項目屬性的Debug頁簽中添加環境變量來實現。

技術分享圖片

技術分享圖片

源碼地址

https://github.com/ErikXu/.NetCoreTips/tree/master/HostedService.Quartz

便捷使用

https://www.nuget.org/packages/Quartz.HostedService/

https://github.com/ErikXu/Quartz.HostedService

.Net Core小技巧 - Hosted Services + Quartz實現定時任務調度