• .NET Core开发Windows服务:使用Quartz执行定时任务


    最近工作上新项目还比较忙,回家之后就不太想碰代码了,闲暇之余修炼下厨艺,新赛季冲了一波分,也是三个多月没水过博客了。最近的项目也是主要为团队提供API接口,大多都是处理常规的业务逻辑上的事。过程中有个需求是需要每日定时定点执行一些推送消息的任务,一开始也没多想就将定时任务写到了API的项目里,部署完测试下人傻了,日志没有任何执行了任务的痕迹,调试时候没毛病。回头一想,IIS这个懒东西应该是休眠了,直接把我的任务一起回收掉了。淡定的我捋了捋思绪查了查方案,可以更改IIS设置修改定时回收的模式,可以通过访问站点来唤醒,觉得不是很合适,既然是WindowsServer,那我干脆弄一个WindowsService来定时执行任务再好不过了鸭,而且之前也没用过.net core写过WindowsService,正好吃个螃蟹。

    一开始我是直接弄了个控制台程序,按照之前.NET Framework的写法来写。后来发现.NET Core专门为这种后台服务(长时间运行的服务)设计了项目模板,称之为Worker Service。为了满足在每日的固定时间点执行,这里选择老牌的Quartz来实现。简单描述一下Demo要实现的需求:每日定点向一个API接口中发送信息。接下来详细记录一下实现过程,Demo的源码:https://github.com/Xuhy0826/WindowsServiceDemo

    使用Visual Studio(我是使用的VS2019)创建项目,选择Worker Service(如下图),姑且就命名为WindowsServiceDemo。

    项目创建完成之后里面的内容很简单,一个Program.cs和另一个Work.cs,Work类继承BackgroundService,并重写其ExecuteAsync方法。显而易见,ExecuteAsync方法就是执行后台任务的入口。

    Program.cs中,依旧是类型的通过创建一个IHost并启动运行。为了方便进行依赖注入,可以创建一个IServiceCollection的扩展方法来进行服务的注册,接下来一步步介绍。

    进行服务注册之前,先将需要引用的包通过Nuget安装一下。安装 Quartz 来实现定时执行任务。另外由于需求需要调用api接口即需要使用HttpClient发送请求,所以还需要另外引入包 Microsoft.Extentsions.Http 。由于需要部署成WindowService,需要引入包 Microsoft.Extensions.Hosting.WindowsServices 。

    首先定义Job,即执行任务的具体业务逻辑。创建一个SendMsgJob类,继承IJob接口,并实现Execute方法。Execute方法就是到了设定好的时间点时执行的方法。这里即是实现了使用注册的HttpClient来发送消息的过程。

     1 public class SendMsgJob : IJob
     2 {
     3     private readonly AppSettings _appSettings;
     4     private const string ApiClientName = "ApiClient";
     5     private readonly IHttpClientFactory _httpClientFactory;
     6     private readonly ILogger<SendMsgJob> _logger;
     7 
     8     public SendMsgJob(IHttpClientFactory httpClientFactory, IOptions<AppSettings> appSettings, ILogger<SendMsgJob> logger)
     9     {
    10         _httpClientFactory = httpClientFactory;
    11         _logger = logger;
    12         _appSettings = appSettings.Value;
    13     }
    14 
    15     /// <summary>
    16     /// 定时执行
    17     /// </summary>
    18     /// <param name="context"></param>
    19     /// <returns></returns>
    20     public async Task Execute(IJobExecutionContext context)
    21     {
    22         _logger.LogInformation($"开始执行定时任务");
    23         //从httpClientFactory获取我们注册的named-HttpClient
    24         using var client = _httpClientFactory.CreateClient(ApiClientName);
    25         var message = new
    26         {
    27             title = "今日消息",
    28             content = _appSettings.MessageNeedToSend
    29         };
    30         //发送消息
    31         var response = await client.PostAsync("/msg", new JsonContent(message));
    32         if (response.IsSuccessStatusCode)
    33         {
    34             _logger.LogInformation($"消息发送成功");
    35         }
    36     }
    37 }

    创建好Job之后,便是设置它让其定时执行即可。来到Work.cs,替换掉原来的默认演示代码,换之配置Job执行策略的代码。使用Quartz配置Job大致分为这么几部

    1. 创建调度器 Scheduler 
    2. 创建Job实例
    3. 创建触发器来控制Job的执行策略
    4. 将Job实例和触发器实例配对注册进调度器中
    5. 启动调度器
     1 public class Worker : BackgroundService
     2 {
     3     private readonly ILogger<Worker> _logger;
     4 
     5     public Worker(ILogger<Worker> logger)
     6     {
     7         _logger = logger;
     8     }
     9 
    10     protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    11     {
    12         _logger.LogInformation("服务启动");
    13 
    14         //创建一个调度器
    15         var scheduler = await StdSchedulerFactory.GetDefaultScheduler(stoppingToken);
    16         //创建Job
    17         var sendMsgJob = JobBuilder.Create<SendMsgJob>()
    18             .WithIdentity(nameof(SendMsgJob), nameof(Worker))
    19             .Build();
    20         //创建触发器
    21         var sendMsgTrigger = TriggerBuilder.Create()
    22             .WithIdentity("trigger-" + nameof(SendMsgJob), "trigger-group-" + nameof(Worker))
    23             .StartNow()
    24             .WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(08, 30)) //每日的08:30执行
    25             .Build();
    26 
    27         await scheduler.Start(stoppingToken);
    28         //把Job和触发器放入调度器中
    29         await scheduler.ScheduleJob(sendMsgJob, sendMsgTrigger, stoppingToken);  
    30     }
    31 }

    关于定时任务的配置告一段落,接下来将所需的服务注册到服务容器中。根据之前所说的,我们创建一个扩展方法来管理我们需要注册的服务。

     1 public static class DependencyInject
     2 {
     3     /// <summary>
     4     /// 定义扩展方法,注册服务
     5     /// </summary>
     6     public static IServiceCollection AddMyServices(this IServiceCollection services, IConfiguration config)
     7     {
     8         //配置文件
     9         services.Configure<AppSettings>(config);
    10 
    11         //注册“命名HttpClient”,并为其配置拦截器
    12         services.AddHttpClient("ApiClient", client =>
    13         {
    14             client.BaseAddress = new Uri(config["ApiBaseUrl"]);
    15         }).AddHttpMessageHandler(_ => new AuthenticRequestDelegatingHandler());
    16 
    17         //注册任务
    18         services.AddSingleton<SendMsgJob>();
    19 
    20         return services;
    21     }
    22 }

    修改Program.cs,调用新增的扩展方法

     1 namespace WindowsServiceDemo
     2 {
     3     public class Program
     4     {
     5         public static void Main(string[] args)
     6         {
     7             CreateHostBuilder(args).Build().Run();
     8         }
     9 
    10         public static IHostBuilder CreateHostBuilder(string[] args) =>
    11             Host.CreateDefaultBuilder(args)
    12                 .ConfigureServices((hostContext, services) =>
    13                 {
    14                     //注册服务
    15                     services.AddMyServices(hostContext.Configuration)
    16                             .AddHostedService<Worker>();
    17                 });
    18     }
    19 }

    到此,主要的代码就介绍完了。为了调试,可以修改设定好的定时执行时间(比如一分钟之后),来测试是否能够成功。修改完触发器的触发时间后,直接运行项目。但是遗憾的是,任务并没有定时触发。这是什么原因呢?其实是因为虽然我们将我们自定义的Job注入的服务容器,但是调度器创建Job实例时,并不是从我们的服务容器去取的,而是调度器自己走默认的实例化。解决方法是我们为调度器指定JobFactory来重写实例化Job类型的规则。

    首先创建一个MyJobFactory并继承IJobFactory接口,实现方法 NewJob ,这个方法便是工厂实例化Job的方法,我们可以在这里将实例化Job的方式改写成从服务容器中获取实例的方式。

     1 namespace WindowsServiceDemo
     2 {
     3     /// <summary>
     4     /// Job工厂,从服务容器中取Job
     5     /// </summary>
     6     public class MyJobFactory : IJobFactory
     7     {
     8         protected readonly IServiceProvider _serviceProvider;
     9         public MyJobFactory(IServiceProvider serviceProvider)
    10         {
    11             _serviceProvider = serviceProvider;
    12         }
    13 
    14         public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
    15         {
    16             var jobType = bundle.JobDetail.JobType;
    17             try
    18             {
    19                 var job = _serviceProvider.GetService(jobType) as IJob;
    20                 return job;
    21             }
    22             catch (Exception e)
    23             {
    24                 Console.WriteLine(e);
    25                 throw;
    26             }
    27         }
    28 
    29         public void ReturnJob(IJob job)
    30         {
    31             var disposable = job as IDisposable;
    32             disposable?.Dispose();
    33         }
    34     }
    35 }

    随后将  MyJobFactory 也注册到服务容器中,即在 AddMyServices 扩展方法中添加

    1 //添加Job工厂
    2 services.AddSingleton<MyJobFactory>();

    接下来将调度器的Factory替换成 MyJobFactory ,修改Work.cs代码如下。

     1 public class Worker : BackgroundService
     2 {
     3     private readonly ILogger<Worker> _logger;
     4     private readonly MyJobFactory _jobFactory;
     5 
     6     public Worker(ILogger<Worker> logger, MyJobFactory jobFactory)
     7     {
     8         _logger = logger;
     9         _jobFactory = jobFactory;
    10     }
    11 
    12     protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    13     {
    14         _logger.LogInformation("服务启动");
    15 
    16         //创建一个调度器
    17         var scheduler = await StdSchedulerFactory.GetDefaultScheduler(stoppingToken);
    18 
    19         //指定自定义的JobFactory
    20         scheduler.JobFactory = _jobFactory;
    21 
    22         //创建Job
    23         var sendMsgJob = JobBuilder.Create<SendMsgJob>()
    24             .WithIdentity(nameof(SendMsgJob), nameof(Worker))
    25             .Build();
    26         //创建触发器
    27         var sendMsgTrigger = TriggerBuilder.Create()
    28             .WithIdentity("trigger-" + nameof(SendMsgJob), "trigger-group-" + nameof(Worker))
    29             .StartNow()
    30             .WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(08, 30)) //每日的08:30执行
    31             .Build();
    32 
    33         await scheduler.Start(stoppingToken);
    34         //把Job和触发器放入调度器中
    35         await scheduler.ScheduleJob(sendMsgJob, sendMsgTrigger, stoppingToken);
    36     }
    37 }

    在此执行调试,现在一旦到达我们在触发器中设置的时间点, SendMsgJob 的 Execute 方法便会成功触发。

    开发完成后,现在剩下的任务就是如何将项目发布成一个WindowsService。来到 Program.cs 下,需要进行一些改动

    1 public static IHostBuilder CreateHostBuilder(string[] args) =>
    2     Host.CreateDefaultBuilder(args)
    3         .UseWindowsService()    //按照Windows Service运行
    4         .ConfigureServices((hostContext, services) =>
    5         {
    6             //注册服务
    7             services.AddMyServices(hostContext.Configuration)
    8                     .AddHostedService<Worker>();
    9         });

    重新编译项目成功后,我们便可以使用sc.exe来部署成为windows服务。以管理员身份启动命令行,执行

    > sc.exe create WindowsServiceDemo binPath="D:workspaceWindowsServiceDemoWindowsServiceDemoinDebug
    etcoreapp3.1WindowsServiceDemo.exe"
    [SC] CreateService 成功

    此时打开服务面板,便可以看到刚刚部署好的 WindowsServiceDemo 服务了。

  • 相关阅读:
    2019 年 GrapeCity Documents 产品路线图
    2019 年 Spread.NET 产品路线图(Roadmap)
    ActiveReports报表控件 V13 正式发布,提供在线报表设计和自适应报表布局
    ActiveReports 大数据分析报告:2018中国电影再次迎来黄金时代
    【Visual Studio 扩展工具】如何在ComponentOne的DataTree中实现RightToLeft布局
    JavaScript-页面打印正方形,各种三角形与菱形
    HTML入门与基础 —— 标签《一》
    springmvc异常处理
    springmvc-数据回显
    springmvc-validation校验
  • 原文地址:https://www.cnblogs.com/xhy0826/p/Net_Core_Windows_Service_Quartz.html
Copyright © 2020-2023  润新知