前言

看过我之前博客的人应该都知道,我负责了相当久的部门数据同步相关的工作。其中的艰辛不赘述了。

随着需求的越来越复杂,最近windows的计划任务已经越发的不能满足我了,而且计划任务毕竟太弱智,总是会失败之类,强制结束之类的。

最近增加了一些复杂的参数,每天的任务对同步程序调用需要多次调用不同参数,我也终于打算不再忍受弱智的计划任务。最初测试了一下基于 IIS 的 Quart ,发现还是存在会被回收无法定时的情况,

在此之前我并未做过 Quart 相关的开发。我查了查相关资料,可以更改 IIS 设置修改定时回收的模式,可以通过访问站点来唤醒等,觉得不是很合适。而且综合业务的考虑,实在是没必要在内网客户机搭一个 Web 站点。

这样一来,干脆搞一个 WindowsService 得了,而且定时的场景还是比较常见的,写一份肯定不亏,以后还是用的上。而且也没尝试过基于 Core 写 WindowsService,正好借此机会学习一下。

Worker Service

使用 VS2019 ,安装了 .NET CORE 3.0 以上的 SDK ,安装SDK的时候最好也安装运行时,免得最后忘记。

项目模板自带的代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace WorkerServiceTest
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
}
}
}

Program.cs中,依旧是创建一个 IHost 并启动。为了方便进行依赖注入,可以创建一个 IServiceCollection 的扩展方法来进行相关服务的注册。

而 Worker 类已经提供了一个默认例子,其中有一个 ExecuteAsync 方法,可以直接执行后台任务。这个时候,直接F5就可以正常运行了实现了一个显示当前时间的程序。

Work 类继承 BackgroundService,并重写其 ExecuteAsync 方法。显而易见,ExecuteAsync 方法就是执行后台任务的入口。

Quartz.Net

Quartz.Net 是一个功能齐全的开源作业调度系统,可以在最小规模的应用程序到大型企业系统使用。

Quartz.Net有三个主要概念:

  • job         这是你想要运行的后台任务。
  • trigger     trigger 控制 job 何时运行,通常按某种调度规则触发。
  • scheduler     它负责协调 job 和 trigger,根据 trigger 的要求执行 job。

ASP.NET Core 很好地支持通过 hosted services(托管服务)运行“后台任务”。当你的 ASP.NET Core 应用程序启动,托管服务也启动,并在应用程序的生命周期中在后台运行。

现在有了一个官方包 Quartz.Extensions.Hosting 实现使用 Quartz.Net 运行后台任务,所以把 Quartz.Net 添加到 ASP.NET Core 或 Worker Service 要简单得多。

Quartz.Net 3.2.0 通过 Quartz.Extensions.Hosting 引入了对该模式的直接支持。

Quartz.Extensions.Hosting 即可以用在ASP.NET Core应用程序,也可以用在基于“通用主机”的Worker Service。

虽然可以创建一个“定时”后台服务(例如,每10分钟运行一个任务),但Quartz.NET提供了一个更加健壮的解决方案。

通过使用Cron trigger,你可以确保任务只在一天的特定时间(例如凌晨2:30)运行,或者只在特定的日子运行,或者这些时间的任意组合运行。

Quartz.Net还允许你以集群的方式运行应用程序的多个实例,以便在任何时候只有一个实例可以运行给定的任务。

Quartz.Net托管服务负责Quartz的调度。它将在应用程序的后台运行,检查正在执行的触发器,并在必要时运行相关的作业。

你需要配置调度程序,但不需要担心启动或停止它,IHostedService 会为你管理。

引用 Quartz.Net

你可以通过使用 dotnet add package Quartz.Extensions.Hosting 命令安装 Quartz.Net 包。

如果你查看项目的.csproj,它应该是这样的:

<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<UserSecretsId>dotnet-QuartzWorkerService-9D4BFFBE-BE06-4490-AE8B-8AF1466778FD</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.3.2" />
</ItemGroup>
</Project>

添加 Quartz.Net 托管服务

注册Quartz.Net需要做两件事:

  1. 注册Quartz.Net需要的DI容器服务。
  2. 注册托管服务。

在 ASP.NET Core 中,通常会在 Startup.ConfigureServices() 方法中完成这两项操作。

但 Worker Services 不使用 Startup 类,所以我们在 Program.cs 中的 IHostBuilder 的 ConfigureServices 方法中注册它们:

    public class Program
{
public static void Main(string[] args)
{
//...
} public static IHostBuilder CreateHostBuilder(string[] args)
{
IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args);
hostBuilder.ConfigureServices((hostContext, services) =>
{
services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionScopedJobFactory();
//q.InitJobAndTriggerFromJobsettings(hostContext.Configuration);
}); services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
//services.AddHostedService<Worker>();
}); //Windows
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ hostBuilder.UseWindowsService(); } //Linux
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{ hostBuilder.UseSystemd(); } return hostBuilder;
}
}
}

UseMicrosoftDependencyInjectionScopedJobFactory 告诉 Quartz.NET 注册一个 IJobFactory,该 IJobFactory 通过从DI容器中创建 job。

方法中的 Scoped 部分意味着你的作业可以使用 scoped 服务,而不仅仅是 single 或 transient 服务。

WaitForJobsToComplete 用来设置确保当请求关闭时,Quartz.NET在退出之前优雅地等待作业结束。

如果你现在运行应用程序,将看到Quartz服务启动,并将大量日志转储到控制台,因为篇幅原因,此处不再贴出。

另外 可能有读者注意到了其中的 hostBuilder.UseWindowsService();/hostBuilder.UseSystemd();

这是关于跨平台的一段设置,稍后会有简单讲解。

此时,你已经让 Quartz 作为托管服务在你的应用程序中运行,但是没有任何job让它运行。在下一节中,我们将创建并注册一个简单的job。

创建 Job

因为我的场景是定时运行一个EXE,最常见的通用定时任务场景应该是调用一个接口。

这里举例一个打印日志的Job,我的相关源代码会在结尾处放出。

using Quartz;
using Serilog;
using System;
using System.Threading.Tasks; namespace AX.QuartzServer.Core.Jobs
{
[DisallowConcurrentExecution]
public class TestJob : AXQuartzJob
{
public string Name { get { return "测试Job"; } }
public string Note { get { return "会打印日志"; } } public Task Execute(IJobExecutionContext context)
{
Log.Logger.Information($"{Newtonsoft.Json.JsonConvert.SerializeObject(context.JobDetail.JobDataMap)}");
Log.Logger.Information($"Hello world! {DateTime.Now.ToLongTimeString()}");
return Task.CompletedTask;
}
}
}

这里我使用了全局的 Serilog 来记录日志。所以和一般的日志不太一样。

我还用 [DisallowConcurrentExecution] 属性装饰了 job 。此属性防止Quartz.NET试图同时运行相同的作业。

它将定时的在日志或控制台中打印 Hello world! 和当前时间。

现在我们已经有了作业,我们需要将它与 trigger 一起注册到 DI 容器中。

启动时自动配置Job

Quartz.NET 为运行 job 提供了一些简单的 schedule,但最常见的方法之一是使用 Quartz.NET Cron 表达式,这里不再赘述。

因为我的场景是Windows服务,暂不考虑一些高级的,可以实时停止,注册Job,运行Job之类的封装。

所以决定是在启动时直接通过读取配置文件注册 Job。

下面是注册的关键代码:

    public static class AXQuartzConfigExtensions
{
public static void InitJobAndTriggerFromJobsettings(this IServiceCollectionQuartzConfigurator quartz, IConfiguration configuration)
{
var allJobs = configuration.GetSection("Jobs").Get<List<BaseJobConfig>>(); Log.Logger.Information($"开始注册 Job");
Log.Logger.Information($"共获取到 {allJobs.Count} 个 Job"); foreach (var item in allJobs)
{
Log.Logger.Information($"{JsonConvert.SerializeObject(item)}"); var jobName = $"{item.JobType}_{item.Name}";
var jobKey = new JobKey(jobName);
Log.Logger.Information($"{nameof(jobKey)}_{jobKey}"); var jobData = new JobDataMap();
jobData.PutAll(ToIDictionary(item)); if (item.JobType.ToLower().Contains("testjob"))
{ quartz.AddJob<Jobs.TestJob>(opts => { opts.WithIdentity(jobKey); opts.SetJobData(jobData); }); }
if (item.JobType.ToLower().Contains("windowscmdjob"))
{ quartz.AddJob<Jobs.WindowsCMDJob>(opts => { opts.WithIdentity(jobKey); opts.SetJobData(jobData); }); } quartz.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity($"{jobName}_Trigger")
.WithCronSchedule(item.Cron));
} Log.Logger.Information($"结束注册 Job");
} //...
}

从配置文件中读取了配置之后,为每个 job 创建唯一的 JobKey 。这用于将job与其trggier连接在一起。

用 AddJob 注册我们的 TestJob。它将 TestJob 添加到了 DI 容器中,这样就可以创建它。它还在内部向 Quartz 注册了job。

然后用 AddTrigger 添加触发器,

使用 JobKey 将 trigger 与一个 job 关联起来,并为 trigger 提供唯一的名称(在本例中不是必需的,但如果你在集群模式下运行quartz,这很重要)。

最后,为trigger 设置了 Cron 表达式, Cron 表达式来自配置文件,测试时我用的是每五秒一次。

其中有一些快速实现时未优化的弱智代码之类的,各位读者不用在意。

配置文件配置节:

{
"Logging": {
//...
}
}, //任务配置 DEMO
"JobDemo": {
"Name": "唯一任务名称",
"JobType": "任务类型 windowscmdjob/testjob",
"Cron": "运行时间表达式"
}, "Jobs": [
{
"Name": "LogHelloWorldTest",
"JobType": "testjob",
"Cron": "0 0 */1 * * ?" //这是每小时一次
//"Cron": "0/5 * * * * ?" 这是每五秒一次
}
]
}

如果你现在运行你的应用程序,你会看到和以前一样的启动消息,然后每5秒你会看到HelloWorldJob写入控制台:

这就是搭建一个定时服务的全部关键内容了。

跨平台

在 Host.CreateDefaultBuilder(args) 增加相关环境的调用。

可以使用判断平台的一个函数: IsOSPlatform ,可以判断是否在Windows平台运行,并进行分别调用。

虽然程序可以正常执行,但是还不能正常部署为服务,需要依据平台添加对应的nuget包:
windows服务,需要添加:
Install-Package Microsoft.Extensions.Hosting.WindowsServices
Linux服务,需要添加:
Install-Package Microsoft.Extensions.Hosting.Systemd .UseWindowsService();
.UseSystemd();

Windows下部署

管理员下运行cmd/powershell,执行

sc.exe create WorkerServiceTest binPath=【你编译后的exe路径,不需要带双引号】

提示 CreateService 成功 即安装成功了,可以输入下面的命令运行服务。

sc.exe start WorkerServiceTest

sc.exe负责管理服务,具体配置启动方式和删除,可以查看命令的帮助。另外,友情提醒,如果是在powershell中,不要省略这个.exe,sc有别的用处...

开源代码

https://github.com/aaxuan/AX.QuartzServer(请选择性的忽略其他仓库的垃圾代码 :)

本文将同步发布到个人的语雀博客,欢迎使用语雀的小伙伴相互关注。

https://www.yuque.com/cuxuan

参考

https://devblogs.microsoft.com/aspnet/net-core-workers-as-windows-services/

https://devblogs.microsoft.com/dotnet/net-core-and-systemd/

https://docs.microsoft.com/en-us/dotnet/core/extensions/generic-host

https://dejanstojanovic.net/aspnet/2018/june/setting-up-net-core-servicedaemon-on-linux-os/

https://dotnetcoretutorials.com/2019/12/07/creating-windows-services-in-net-core-part-3-the-net-core-worker-way/

http://www.cnblogs.com/podolski/p/13890572.html

https://www.cnblogs.com/xhy0826/p/Net_Core_Windows_Service_Quartz.html

https://segmentfault.com/a/1190000038753018

基于.Net Core 5.0 Worker Service 的 Quart 服务的更多相关文章

  1. 基于.net core 2.0+mysql+AceAdmin搭建一套快速开发框架

    前言 .net core已经出来一段时间了,相信大家对.net core的概念已经很清楚了,这里就不再赘述.笔者目前也用.net core做过一些项目,并且将以前framework下的一些经验移植到了 ...

  2. 一个基于 .NET Core 2.0 开发的简单易用的快速开发框架 - LinFx

    LinFx 一个基于 .NET Core 2.0 开发的简单易用的快速开发框架,遵循领域驱动设计(DDD)规范约束,提供实现事件驱动.事件回溯.响应式等特性的基础设施.让开发者享受到正真意义的面向对象 ...

  3. .NET Worker Service 作为 Windows 服务运行及优雅退出改进

    上一篇文章我们了解了如何为 Worker Service 添加 Serilog 日志记录,今天我接着介绍一下如何将 Worker Service 作为 Windows 服务运行. 我曾经在前面一篇文章 ...

  4. .NET Core中的Worker Service

    当你想到ASP.NET Core时,可能会想到Web应用程序后端代码,包括MVC和WebAPI.MVC视图和Razor页面还允许使用后端代码生成带有HTML元素的前端UI.全新的Blazor更进一步, ...

  5. HDD成都站:HMS Core 6.0带来新可能 多元服务驱动产品商业成功

    9月10日,由华为开发者联盟主办的HDD(Huawei Developer Day)于成都举行.活动中,华为HMS Core各领域专家重点解读了HMS Core 6.0为开发者带来的多项全新能力,及生 ...

  6. .net core 3.0 Signalr - 07 业务实现-服务端 自定义管理组、用户、连接

    Hub的管理 重写OnConnectedAsync 从连接信息中获取UserId.Groups,ConnectId,并实现这三者的关系,存放于redis中 代码请查看 using CTS.Signal ...

  7. 基于ef core 2.0的数据库增删改审计系统

    1.首先是建审计存储表 CREATE TABLE [dbo].[Audit] ( [Id] [uniqueidentifier] NOT NULL, [EntityName] [nvarchar](1 ...

  8. 在WPF中使用.NET Core 3.0依赖项注入和服务提供程序

    前言 我们都知道.NET Core提供了对依赖项注入的内置支持.我们通常在ASP.NET Core中使用它(从Startup.cs文件中的ConfigureServices方法开始),但是该功能不限于 ...

  9. .NET 中的 Worker Service 入门介绍

    翻译自 Steve Gordon 2020年3月30日的文章 <WHAT ARE .NET WORKER SERVICES?> [1] 随着 .NET Core 3.0 的发布,ASP.N ...

随机推荐

  1. 微信小程序:条件渲染wx:if和hidden

    一.条件渲染:wx:if,wx:elif,wx:else 花括号中的true或false可以改成变量从而来动态显示. 二.hidden 只显示hidden2 当标签不是频繁切换显示(控制是否渲染到页面 ...

  2. Guava - LoadingCache实现Java本地缓存

    前言 Guava是Google开源出来的一套工具库.其中提供的cache模块非常方便,是一种与ConcurrentMap相似的缓存Map. 官方地址:https://github.com/google ...

  3. Kubernetes-6.Service

    docker version:20.10.2 kubernetes version:1.20.1 本文概述Kubernetes Service的基本原理和使用. 服务 Service是将运行在一组Po ...

  4. java帝国的诞生

    Java : 一个帝国的诞生 C语言帝国的统治 现在是公元1995年, C语言帝国已经统治了我们20多年, 实在是太久了. 1972年, 随着C语言的诞生和Unix的问世, 帝国迅速建立统治, 从北美 ...

  5. Java 集合框架体系总览

    尽人事,听天命.博主东南大学硕士在读,热爱健身和篮球,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上我们一起进步 本文已收录于 「CS-Wiki」Gitee ...

  6. 攻防世界 reverse xx

    xx 程序开始验证输入长度为19位. 取前4位(作为后面加密的key),验证这4位都在'qwertyuiopasdfghjklzxcvbnm1234567890'中. 将key用0填充为16位 调用x ...

  7. PTA 求二叉树的深度

    6-7 求二叉树的深度 (6 分)   本题要求实现一个函数,可返回二叉树的深度. 函数接口定义: int Depth(BiTree T); T是二叉树树根指针,函数Depth返回二叉树的深度,若树为 ...

  8. [换根DP][倍增]luogu P5666 树的重心

    题面 https://www.luogu.com.cn/problem/P5666 分析 对于一棵以i为根的树来说,它的重心必然在其size大于等于sumsize/2的子树中. 那么断掉一条边e(u, ...

  9. JetBrains Projector 体验

    先来一张最终效果图: JetBrains Projector 是 JetBrains 的"远程开发"解决方案,基于 Client + Server 架构,对标的是微软 VSCode ...

  10. Spring与Mybatis整合配置

    Mybatis核心配置文件: 配置文件内可以不写内容 <?xml version="1.0" encoding="UTF-8" ?> <!DO ...