.Net Core小技巧 - Hosted Services + Quartz实现定时任务调度

背景

  之前一直有朋友问,.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.0Install-Package Microsoft.Extensions.Configuration.FileExtensions -Version 2.1.0Install-Package Microsoft.Extensions.Configuration.Json -Version 2.1.0Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables -Version 2.1.0Install-Package Microsoft.Extensions.Configuration.CommandLine -Version 2.1.0Install-Package Microsoft.Extensions.Logging.Console -Version 2.1.0Install-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

原文地址:https://www.cnblogs.com/Erik_Xu/p/9219307.html

时间: 2024-08-01 20:07:10

.Net Core小技巧 - Hosted Services + Quartz实现定时任务调度的相关文章

Hosted Services+Quartz实现定时任务调度

背景 之前.net core使用quartz.net时,总感觉非常变扭,百度和谷歌了N久都没解决以下问题,造成代码丑陋,非常不优雅: 1.项目启动时,要立刻恢复执行quartz.net中的任务 2.quartz.net中的Job任务无法使用ioc注入,要额外写一套 直到最近看到这篇文章.Net Core小技巧 - Hosted Services + Quartz实现定时任务调度,终于解决了我的问题,特此记录一下 前后代码对比 Program.cs 前 查看详细内容 public class Pr

Spring整合Quartz实现定时任务调度

一. 核心类 1. Job: 表示一个工作, 具体的业务处理都在这里. 2. JobDetail: 表示一个具体的可执行的调度程序. 3. Trigger: 用于调度参数的配置(什么时候去调用Job). 4. Scheduler: 表示一个调度容器, 容器中有一个线程池, 用来并行调度执行每个作业, 一个调度容器中可以注册多个JobDetail和Trigger. 二. 整合spring 1. 代码结构图: 2. applicationContext.xml <?xml version="1

.Net Core小技巧 - Swagger适配虚拟目录及二级目录

前言 随着前后端分离模式与微服务架构的出现,Web API变得越来越重要及普遍.而后出现的网关技术,使开发者更倾向于使用二级/多级目录来暴露Web API,一是暴露的端口更少,方便管理:二是在网关中可以处理一些公共的事务,如认证.但swagger默认是适配根(root)目录的,想要适配二级/多级目录,需要额外处理,同时还要区分开发环境与其它环境.一种思路是在开发环境就让Web API是处于二级目录,此时可以在本机通过nginx和iis虚拟目录来模拟,但这种思路明显略微麻烦.另一种思路是通过配置多

.Net Core小技巧 - 使用Swagger上传文件

前言 随着前后端分离开发模式的普及,后端人员更多是编写服务端API接口.调用接口实现文件上传是一个常见的功能,同时也需要一个选择文件上传的界面,可以编写前端界面上传,可以使用Postman.curl来模拟上传请求.上述的方式多多少少有点麻烦.Swagger作为Api说明文档及调试工具,如果它能提供文件上传的界面(默认不提供),那会更加方便文件上传提示,本文将介绍如何使用Swagger来上传文件. 步骤 1. 安装Swagger Install-Package Swashbuckle.AspNet

quartz实现定时任务调度

一. 业务需求: 实际工作中我们一般会遇到这种需求: 使用Ajax技术每隔几秒从缓存或数据库中读取一些数据, 然后再显示在页面上, 眼下有一个比較好的定时调度框架: quartz能够满足我们的需求. 二. 核心类: Job: 一个接口, 它里面仅仅有一个方法void execute().我们须要运行任务就须要实现这个接口,在execute中实现我们要做的事情. JobDetail: 在Quartz每次执行Job时,都须要创建一个Job实例,所以它直接接受一个实现类以便执行时实例化,还须要一个描写

用abp vNext快速开发Quartz.NET定时任务管理界面

今天这篇文章我将通过实例代码带着大家一步一步通过abp vNext这个asp.net core的快速开发框架来进行Quartz.net定时任务调度的管理界面的开发.大伙最好跟着一起敲一下代码,当然源码我会上传到github上,有兴趣的小伙伴可以在文章底部查看源码链接. 作者:依乐祝 原文链接:https://www.cnblogs.com/yilezhu/p/10444060.html 写在前面 有几天没更新博客了,一方面因为比较忙,另一方面是因为最近在准备组织我们霸都合肥的.NET技术社区首次

10个小技巧助您写出高性能的ASP.NET Core代码

今天这篇文章我们来聊一聊如何提升并优化ASP.NET Core应用程序的性能,本文的大部分内容来自翻译,当然中间穿插着自己的理解,希望对大家有所帮助!话不多说开始今天的主题吧! 我们都知道性能是公共网站取得成功的关键因素之一.如果一个网站的响应时间超过3秒,那么用户通常不会再此光顾(此网站).谷歌,Bing,百度以及其他搜索引擎也更倾向于推荐优化后的,移动友好的以及响应速度更快的网站. 作者:依乐祝 原文地址:https://www.cnblogs.com/yilezhu/p/10507984.

Playground 你不知道的小技巧, CoreData 的使用

Playground 的出现无疑是大大的提高了开发效率,可以节省大量的编译时间. 这里介绍在 Playground 中使用 CoreData 的小技巧. 我们新建一个工程 iOS 项目工程. 点击 File -> New -> File , 在工程中新建文件 Data Model 文件  在 model 中添加一个 Entitle,如下图  编译工程后,在 Product 选择生成的 .app 文件,找到该目录,如下图  查看包中的文件,如图  可以看到一个 Mode.momd 文件, 如图 

iOS 小技巧总结,绝对有你想要的

iOS 小技巧总结,绝对有你想要的 原文链接:http://www.jianshu.com/p/4523eafb4cd4 在这里总结一些 iOS 开发中的小技巧,能大大方便我们的开发,持续更新. —— 由 xcvxvxc分享 在这里总结一些iOS开发中的小技巧,能大大方便我们的开发,持续更新. UITableView的Group样式下顶部空白处理 在viewWillAppear里面添加如下代码: //分组列表头部空白处理 CGRect frame = myTableView.tableHeade