26.1 调度作业
📝 模块更新日志
新特性
- 定时任务看板列表支持作业分组名排序 4.8.8.43 ⏱️2023.09.14 #I7YQ9V
- 定时任务作业计划
OnChanged
事件处理 4.8.8.29 ⏱️2023.06.25 e4c4cf1 - 定时任务支持二级虚拟目录
VisualPath
配置部署 4.8.8.20 ⏱️2023.05.18 #I740IA - 定时任务作业处理程序工厂
IJobFactory
支持 4.8.8.13 ⏱️2023.05.08 ad58dd3 - 定时任务
Schedular.CompileCSharpClassCode(code)
支持动态编译作业处理程序代码 4.8.8.7 ⏱️2023.04.30 fe1e8a1 - 定时任务支持配置
IJob
执行异常FallbackAsync
回退策略 4.8.8.6 ⏱️2023.04.25 7671489 - 定时任务支持在非
IOC/DI
项目类型中使用 4.8.8.5 ⏱️2023.04.24 #I6YJNB - 定时任务看板支持自定义刷新频率
SyncRate
功能 4.8.7.43 ⏱️2023.04.12 703b465 - 定时任务看板支持完全自定义
RequestPath
入口地址功能 4.8.7.34 ⏱️2023.04.04 24736f6 - 定时任务一系列
.AlterTo
修改作业触发器触发时间便捷方法 4.8.7.31 ⏱️2023.03.31 0349017 - 定时任务看板
UI
作业列表最近执行时间
列和优化显示效果 4.8.7.12 ⏱️2023.03.15 26462a8 cb5dd17 - 定时任务作业计划/工厂立即执行
RunJob
方法 4.8.7.11 ⏱️2023.03.15 #I6LD9X - 定时任务看板
UI
提供立即执行功能 4.8.7.11 ⏱️2023.03.15 #I6LD9X - 定时任务作业执行上下文
JobExecutionContext
服务提供器ServiceProvider
属性 4.8.7.10 ⏱️2023.03.14 02586f8 - 定时任务
HTTP
作业,支持定时请求互联网URL
地址 4.8.7.7 ⏱️2023.03.11 01d4466
查看变化
services.AddSchedule(options =>
{
options.AddHttpJob(request =>
{
request.RequestUri = "https://www.chinadot.net";
request.HttpMethod = HttpMethod.Get;
// request.Body = "{}"; // 设置请求报文体
}, Triggers.PeriodSeconds(5));
});
- 定时任务作业触发器
Trigger
执行结果Result
和执行耗时ElapsedTime
属性 4.8.7.7 ⏱️2023.03.11 01d4466
- 定时任务作业触发器
- 定时任务作业看板支持查看作业触发器执行结果
Result
和执行耗时ElapsedTime
属性 4.8.7.7 ⏱️2023.03.11 01d4466
- 定时任务作业看板支持查看作业触发器执行结果
- 定时任务休眠时长和唤醒时机日志输出 4.8.7.6 ⏱️2023.03.08 #I6LANE
- 定时任务
IScheduler.[Try]UpdateDetail(builder => {})
和IScheduler.[Try]UpdateTrigger(triggerId, builder => {})
重载方法 4.8.6 ⏱️2023.02.08 6e43a54
- 定时任务
查看变化
- 更新作业信息
// 返回 ScheduleResult 类型
var scheduleResult = Scheduler.TryUpdateDetail(jobBuilder =>
{
jobBuilder.SetDescription("~~~");
}, out var jobDetail);
// 无返回值
scheduler.UpdateDetail(jobBuilder =>
{
jobBuilder.SetDescription("~~~");
});
- 更新作业触发器
// 返回 ScheduleResult 类型
var scheduleResult = scheduler.TryUpdateTrigger("triggerId", triggerBuilder =>
{
triggerBuilder.SetDescription("~~");
}, out var trigger);
// 无返回值
scheduler.UpdateTrigger("triggerId", triggerBuilder =>
{
triggerBuilder.SetDescription("~~");
});
- 定时任务
Dashboard
可自定义入口地址/schedule
4.8.5.6 ⏱️2023.02.02 c5639f5
- 定时任务
- 定时任务执行上下文
RunId
属性,用于标识单次作业触发器执行 4.8.5.1 ⏱️2023.01.30 1aac470
- 定时任务执行上下文
- 定时任务
Dashboard
查看作业触发器最近运行记录功能 4.8.4.3 ⏱️2023.01.03 e7d24d8
- 定时任务
- 定时任务作业触发器
trigger.GetTimelines()
获取最近10
条运行记录列表 4.8.4.3 ⏱️2023.01.03 e7d24d8
- 定时任务作业触发器
- 定时任务
Dashboard
看板 4.8.4 ⏱️2022.12.30 d3f9669
- 定时任务
- 定时任务
IScheduler.GetEnumerable()
方法,可将作业计划转换成可枚举字典 4.8.4 ⏱️2022.12.30 4d5235c
- 定时任务
- 定时任务配置选项
options.JobDetail.LogEnabled
配置,可自动输出执行日志 4.8.3.7 ⏱️2022.12.14 58d2c20
- 定时任务配置选项
- 定时任务
IScheduler
对象每次操作后自动刷新和提供手动刷新Reload()
方法 4.8.3.3 ⏱️2022.12.09 #I65EQ1
- 定时任务
- 定时任务间隔分钟作业触发器
Triggers.PeriodMinutes(5)
和[PeriodMinutes(5)]
特性 4.8.2.8 ⏱️2022.12.01 8e1f06f
- 定时任务间隔分钟作业触发器
- 定时任务工作日作业触发器
Triggers.Workday()
和[Workday]
特性 4.8.2.6 ⏱️2022.11.30 28b2d20
- 定时任务工作日作业触发器
- 定时任务作业校对功能,可对误差进行校正 4.8.2.6 ⏱️2022.11.30 f725a25
- 定时任务
Triggers
所有带At
的Cron
表达式触发器构建器及特性 4.8.2.5 ⏱️2022.11.29 #I63PLR
- 定时任务
- 定时任务批量添加
SchedulerBuilder
作业功能 4.8.2.4 ⏱️2022.11.29 5faa67b
- 定时任务批量添加
-
JobDetail
和Trigger
自定义ConvertToSQL
输出SQL
配置 4.8.2 ⏱️2022.11.27 0bb9d8f
-
- 作业触发器
ResetOnlyOnce
属性,支持只运行一次的作业重新启动服务重复执行 4.8.1.5 ⏱️2022.11.25 a8be728
- 作业触发器
- 动态作业处理程序委托支持 4.8.1.8 ⏱️2022.11.27 e02266c
突破性变化
查看变化
减少记忆负担,统一动态作业和普通作业的 ExecuteAsync
方法签名,故做出调整。
由:
options.AddJob((serviceProvider, context, stoppingToken) =>
{
serviceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}, Triggers.PeriodSeconds(5));
调整为:
options.AddJob((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}, Triggers.PeriodSeconds(5));
- 定时任务底层所有代码,日志,注释,文档 4.8.1.10 ⏱️2022.12.05
问题修复
- 定时任务因上一版本修改 4e2615b 导致自定义作业触发器异常问题 4.8.8.36 ⏱️2023.07.06 #I7J59D
- 定时任务因上一版本修改 4e2615b 导致
Cron
解析异常问题 4.8.8.32 ⏱️2023.06.28 #I7GQ5I - 定时任务设置额外数据不支持
long/int64
类型参数问题 4.8.8.31 ⏱️2023.06.28 4e2615b - 定时任务休眠毫秒数大于
int.MaxValue
时出现ArgumentOutOfRangeException
4.8.8.27 ⏱️2023.06.21 #I7F6ZT - 定时任务通过作业
Id
删除作业不能删除作业触发器问题 4.8.7.35 ⏱️2023.04.05 312ca35 - 定时任务作业状态为
积压:0
和归档:6
时调用立即执行后不能恢复上一次状态 4.8.7.18 ⏱️2023.03.21 6f5aae8 - 定时任务更新作业
null
值默认被跳过问题 4.8.7.17 ⏱️2023.03.20 #I6OHO4 - 定时任务生成
SQL
语句没有处理'
转义问题 4.8.7.15 ⏱️2023.03.19 #I6NXKA - 定时任务服务在停止进程时会卡住
30秒
问题 4.8.7.8 ⏱️2023.03.13 #I6MI9I #I6MHOU - 定时任务看板删除不存在的作业触发器出现空异常 4.8.7.7 ⏱️2023.03.11 01d4466
- 定时任务
StartAll
出现个别作业显示无触发时间
的状态 4.8.4.14 ⏱️2023.01.12 #I6A08X - 定时任务停止作业触发器后运行记录不能写入最新记录问题 4.8.4.8 ⏱️2023.01.05 d4c553f
- 定时任务使用
Furion.Pure
包访问Dashboard
出现404
问题 4.8.4.2 ⏱️2023.01.02 21977b7 - 定时任务通过
scheduler.RemoveTrigger(triggerId)
报异常问题 4.8.3.3 ⏱️2022.12.09 #I65EQ1 - 定时任务作业触发器配置了
EndTime
和StartTime
之后Status
没有对应上 4.8.3.1 ⏱️2022.12.09 52a5506 - 定时任务通过
scheduler.AddTrigger(triggerBuilder)
无效的问题 4.8.3.1 ⏱️2022.12.09 #I65EQ1 - 作业拥有多个触发器时暂停作业后依然存在个别未暂停的清空(并发问题) 4.8.2.12 ⏱️2022.12.07 #I655W9
- 作业触发器不符合下一次执行规律但
NextRunTime
不为null
情况 4.8.1.5 ⏱️2022.11.25 a8be728 - 运行时启动/暂停作业无效问题 4.8.1.6 ⏱️2022.11.25 #I6368M
- 定时任务生成的
SQL
语句不支持MySQL
问题 4.8.1.7 ⏱️2022.11.26 #I638ZC
其他更改
- 定时任务
GC
回收逻辑,避免高频添加作业导致尾延迟
问题 4.8.8.3 ⏱️2023.04.21 #I6XIV8 - 定时任务日志设计,减少不必要的日志输出 4.8.8.3 ⏱️2023.04.21 #I6XI2L
- 定时任务动态委托作业持久化逻辑,采用不触发持久化操作 4.8.7.36 ⏱️2023.04.06 7bb58b6
- 定时任务
Http
作业HttpMethod
属性拼写错成HttpMedhod
4.8.7.24 ⏱️2023.03.28 !756 - 定时任务配置选项
BuilSqlType
属性命为BuildSqlType
4.8.7.11 ⏱️2023.03.15 92117b8 - 定时任务查看作业触发器运行记录由保存
10条
改为5条
4.8.7.7 ⏱️2023.03.07 01d4466 - 定时任务调度器时间精度,控制持续执行一年误差在
100ms
以内 4.8.2.9 ⏱️2022.12.01 334d089 - 定时任务作业计划工厂
GetNextRunJobs()
方法逻辑 4.8.2.7 ⏱️2022.11.30 #I63VS2
- 定时任务
文档
以下内容仅限 Furion 4.8.0 +
版本使用。
26.1.1 关于调度作业
调度作业又称定时任务,顾名思义,定时任务就是在特定的时间或符合某种时间规律自动触发并执行任务。

26.1.1.1 使用场景
定时任务的应用场景非常广,几乎是每一个软件系统必备功能:
- 叫你起床的闹钟
- 日历日程提醒
- 生日纪念日提醒
- 定时备份数据库
- 定时清理垃圾数据
- 定时发送营销信息,邮件
- 定时上线产品,比如预售产品,双十一活动
- 定时发送优惠券
- 定时发布,实现 Devops 功能,如 Jenkins
- 定时爬虫抓数据
- 定时导出报表,历史统计,考勤统计
- ...
26.1.2 快速入门
- 定义作业处理程序
MyJob
:
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context}");
return Task.CompletedTask;
}
}
- 在
Startup.cs
注册Schedule
服务:
services.AddSchedule(options =>
{
// 注册作业,并配置作业触发器
options.AddJob<MyJob>(Triggers.Secondly()); // 表示每秒执行
});
- 查看作业执行结果
info: 2022-12-02 16:51:33.5032989 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-02 16:51:33.5180669 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-02 16:51:34.1452041 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 16:51:34.1541701 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-02 16:51:34.1748401 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-02 16:51:35.0712571 +08:00 星期五 L MyJob[0] #4
<job1> [C] <job1 job1_trigger1> * * * * * * 1ts 2022-12-02 16:51:35.000 -> 2022-12-02 16:51:36.000
info: 2022-12-02 16:51:36.0317375 +08:00 星期五 L MyJob[0] #14
<job1> [C] <job1 job1_trigger1> * * * * * * 2ts 2022-12-02 16:51:36.000 -> 2022-12-02 16:51:37.000
info: 2022-12-02 16:51:37.0125007 +08:00 星期五 L MyJob[0] #9
<job1> [C] <job1 job1_trigger1> * * * * * * 3ts 2022-12-02 16:51:37.000 -> 2022-12-02 16:51:38.000
info: 2022-12-02 16:51:38.0179920 +08:00 星期五 L MyJob[0] #8
<job1> [C] <job1 job1_trigger1> * * * * * * 4ts 2022-12-02 16:51:38.000 -> 2022-12-02 16:51:39.000
JobExecutionContext
重写了 ToString()
方法并提供以下几种格式:
# 持续运行格式
<作业Id> 作业描述 [并行C/串行S] <作业Id 触发器Id> 触发器字符串 触发器描述 触发次数ts 触发时间 -> 下一次触发时间
# 触发停止格式
<作业Id> 作业描述 [并行C/串行S] <作业Id 触发器Id> 触发器字符串 触发器描述 触发次数ts 触发时间 [触发器终止状态]
26.1.2.1 指定作业 Id
默认情况下,不指定作业 Id
会自动生成 job[编号]
。
services.AddSchedule(options =>
{
options.AddJob<MyJob>("myjob", Triggers.Secondly());
});
查看作业执行结果:
info: 2022-12-02 17:15:43.3024818 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-02 17:15:43.3107918 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-02 17:15:43.9498664 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <myjob_trigger1> trigger for scheduler of <myjob> successfully appended to the schedule.
info: 2022-12-02 17:15:43.9532894 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The scheduler of <myjob> successfully appended to the schedule.
warn: 2022-12-02 17:15:43.9941565 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-02 17:15:44.1230353 +08:00 星期五 L MyJob[0] #6
<myjob> [C] <myjob myjob_trigger1> * * * * * * 1ts 2022-12-02 17:15:44.000 -> 2022-12-02 17:15:45.000
info: 2022-12-02 17:15:45.0854893 +08:00 星期五 L MyJob[0] #9
<myjob> [C] <myjob myjob_trigger1> * * * * * * 2ts 2022-12-02 17:15:45.000 -> 2022-12-02 17:15:46.000
info: 2022-12-02 17:15:46.0100813 +08:00 星期五 L MyJob[0] #13
<myjob> [C] <myjob myjob_trigger1> * * * * * * 3ts 2022-12-02 17:15:46.000 -> 2022-12-02 17:15:47.000
26.1.2.2 多个作业触发器
有时候,一个作业支持多种触发时间,比如 每分钟
执行一次,每 5秒
执行一次,每分钟第 3/7/8秒
执行一次。
services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Minutely() // 每分钟开始
, Triggers.Period(5000) // 每 5 秒,还支持 Triggers.PeriodSeconds(5),Triggers.PeriodMinutes(5),Triggers.PeriodHours(5)
, Triggers.Cron("3,7,8 * * * * ?", CronStringFormat.WithSeconds)); // 每分钟第 3/7/8 秒
});
查看作业执行结果:
info: 2022-12-02 17:18:53.3593518 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-02 17:18:53.3663583 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-02 17:18:54.0381456 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 17:18:54.0708796 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger2> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 17:18:54.0770193 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger3> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 17:18:54.0800017 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-02 17:18:54.1206816 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-02 17:18:59.0040452 +08:00 星期五 L MyJob[0] #9
<job1> [C] <job1 job1_trigger2> 5000ms 1ts 2022-12-02 17:18:58.927 -> 2022-12-02 17:19:03.944
info: 2022-12-02 17:19:00.0440142 +08:00 星期五 L MyJob[0] #15
<job1> [C] <job1 job1_trigger1> * * * * * 1ts 2022-12-02 17:19:00.000 -> 2022-12-02 17:20:00.000
info: 2022-12-02 17:19:03.0149075 +08:00 星期五 L MyJob[0] #6
<job1> [C] <job1 job1_trigger3> 3,7,8 * * * * ? 1ts 2022-12-02 17:19:03.000 -> 2022-12-02 17:19:07.000
info: 2022-12-02 17:19:03.9519350 +08:00 星期五 L MyJob[0] #15
<job1> [C] <job1 job1_trigger2> 5000ms 2ts 2022-12-02 17:19:03.944 -> 2022-12-02 17:19:08.919
info: 2022-12-02 17:19:07.0116797 +08:00 星期五 L MyJob[0] #4
<job1> [C] <job1 job1_trigger3> 3,7,8 * * * * ? 2ts 2022-12-02 17:19:07.000 -> 2022-12-02 17:19:08.000
info: 2022-12-02 17:19:08.0078132 +08:00 星期五 L MyJob[0] #15
<job1> [C] <job1 job1_trigger3> 3,7,8 * * * * ? 3ts 2022-12-02 17:19:08.000 -> 2022-12-02 17:20:03.000
info: 2022-12-02 17:19:08.9298393 +08:00 星期五 L MyJob[0] #14
<job1> [C] <job1 job1_trigger2> 5000ms 3ts 2022-12-02 17:19:08.919 -> 2022-12-02 17:19:13.897
info: 2022-12-02 17:19:13.9056247 +08:00 星期五 L MyJob[0] #8
<job1> [C] <job1 job1_trigger2> 5000ms 4ts 2022-12-02 17:19:13.897 -> 2022-12-02 17:19:18.872
info: 2022-12-02 17:19:18.8791123 +08:00 星期五 L MyJob[0] #12
<job1> [C] <job1 job1_trigger2> 5000ms 5ts 2022-12-02 17:19:18.872 -> 2022-12-02 17:19:23.846
26.1.2.3 串行
执行
默认情况下,作业采用 并行
执行方式,也就是不会等待上一次作业执行完成,只要触发时间到了就自动执行,但一些情况下,我们可能希望等待上一次作业完成再执行,如:
services.AddSchedule(options =>
{
options.AddJob<MyJob>(concurrent: false, Triggers.Secondly()); // 串行,每秒执行
});
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context.JobId} {context.TriggerId} {context.OccurrenceTime} {context.Trigger}");
await Task.Delay(2000, stoppingToken); // 这里模拟耗时操作,比如耗时2秒
}
}
查看作业执行结果:
info: 2022-12-02 17:23:27.3726863 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-02 17:23:27.3830366 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-02 17:23:27.9083148 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 17:23:27.9184699 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-02 17:23:27.9740028 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-02 17:23:28.0638789 +08:00 星期五 L MyJob[0] #9
<job1> [S] <job1 job1_trigger1> * * * * * * 1ts 2022-12-02 17:23:28.000 -> 2022-12-02 17:23:29.000
warn: 2022-12-02 17:23:29.1119269 +08:00 星期五 L System.Logging.ScheduleService[0] #9
12/02/2022 17:23:29: The <job1_trigger1> trigger of job <job1> failed to execute as scheduled due to blocking.
warn: 2022-12-02 17:23:30.0090551 +08:00 星期五 L System.Logging.ScheduleService[0] #9
12/02/2022 17:23:30: The <job1_trigger1> trigger of job <job1> failed to execute as scheduled due to blocking.
info: 2022-12-02 17:23:31.0121694 +08:00 星期五 L MyJob[0] #9
<job1> [S] <job1 job1_trigger1> * * * * * * 2ts 2022-12-02 17:23:31.000 -> 2022-12-02 17:23:32.000
warn: 2022-12-02 17:23:32.0243646 +08:00 星期五 L System.Logging.ScheduleService[0] #9
12/02/2022 17:23:32: The <job1_trigger1> trigger of job <job1> failed to execute as scheduled due to blocking.
串行
执行规则说明串行
执行如果遇到上一次作业还未完成那么它会等到下一次触发时间到了再执行,以此重复。
默认情况下,使用 串行
执行但因为耗时导致触发时间到了但实际未能执行会默认输出 warn
警告日志,如需关闭只需要:
services.AddSchedule(options =>
{
options.LogEnabled = false;
options.AddJob<MyJob>(concurrent: false, Triggers.Secondly()); // 每秒执行
});
查看作业执行结果:
info: 2022-12-02 17:27:13.1136450 +08:00 星期五 L MyJob[0] #12
<job1> [S] <job1 job1_trigger1> * * * * * * 1ts 2022-12-02 17:27:13.000 -> 2022-12-02 17:27:14.000
info: 2022-12-02 17:27:16.0092433 +08:00 星期五 L MyJob[0] #8
<job1> [S] <job1 job1_trigger1> * * * * * * 2ts 2022-12-02 17:27:16.000 -> 2022-12-02 17:27:17.000
info: 2022-12-02 17:27:19.0092363 +08:00 星期五 L MyJob[0] #6
<job1> [S] <job1 job1_trigger1> * * * * * * 3ts 2022-12-02 17:27:19.000 -> 2022-12-02 17:27:20.000
info: 2022-12-02 17:27:22.0183594 +08:00 星期五 L MyJob[0] #9
<job1> [S] <job1 job1_trigger1> * * * * * * 4ts 2022-12-02 17:27:22.000 -> 2022-12-02 17:27:23.000
info: 2022-12-02 17:27:25.0152323 +08:00 星期五 L MyJob[0] #4
<job1> [S] <job1 job1_trigger1> * * * * * * 5ts 2022-12-02 17:27:25.000 -> 2022-12-02 17:27:26.000
26.1.2.4 打印作业完整信息
框架提供了四种方式打印作业完整信息。
- 第一种:输出完整的作业
JSON
信息:context.ConvertToJSON()
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation(context.ConvertToJSON());
await Task.CompletedTask;
}
}
查看作业打印结果:
info: 2022-12-02 18:00:59.4140802 +08:00 星期五 L MyJob[0] #13
{
"jobDetail": {
"jobId": "job1",
"groupName": null,
"jobType": "MyJob",
"assemblyName": "ConsoleApp32",
"description": null,
"concurrent": true,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-12-02 18:00:59.390"
},
"trigger": {
"triggerId": "job1_trigger1",
"jobId": "job1",
"triggerType": "Furion.Schedule.PeriodSecondsTrigger",
"assemblyName": "Furion",
"args": "[5]",
"description": null,
"status": 2,
"startTime": null,
"endTime": null,
"lastRunTime": "2022-12-02 18:00:59.326",
"nextRunTime": "2022-12-02 18:01:04.358",
"numberOfRuns": 1,
"maxNumberOfRuns": 0,
"numberOfErrors": 0,
"maxNumberOfErrors": 0,
"numRetries": 0,
"retryTimeout": 1000,
"startNow": true,
"runOnStart": false,
"resetOnlyOnce": true,
"result": null,
"elapsedTime": 100,
"updatedTime": "2022-12-02 18:00:59.390"
}
}
- 第二种:输出单独的作业
JSON
信息:jobDetail.ConvertToJSON()
或trigger.ConvertToJSON()
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation(context.JobDetail.ConvertToJSON());
_logger.LogInformation(context.Trigger.ConvertToJSON(NamingConventions.UnderScoreCase)); // 支持三种属性名输出规则
await Task.CompletedTask;
}
}
查看作业打印结果:
info: 2022-12-02 18:02:10.7923360 +08:00 星期五 L MyJob[0] #8
{
"jobId": "job1",
"groupName": null,
"jobType": "MyJob",
"assemblyName": "ConsoleApp32",
"description": null,
"concurrent": true,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-12-02 18:02:10.774"
}
info: 2022-12-02 18:02:10.8008708 +08:00 星期五 L MyJob[0] #8
{
"trigger_id": "job1_trigger1",
"job_id": "job1",
"trigger_type": "Furion.Schedule.PeriodSecondsTrigger",
"assembly_name": "Furion",
"args": "[5]",
"description": null,
"status": 2,
"start_time": null,
"end_time": null,
"last_run_time": "2022-12-02 18:02:10.727",
"next_run_time": "2022-12-02 18:02:15.733",
"number_of_runs": 1,
"max_number_of_runs": 0,
"number_of_errors": 0,
"max_number_of_errors": 0,
"num_retries": 0,
"retry_timeout": 1000,
"start_now": true,
"run_on_start": false,
"reset_only_once": true,
"result": null,
"elapsed_time": 100,
"updated_time": "2022-12-02 18:02:10.774"
}
- 第三种:输出单独的作业
SQL
信息:jobDetail.ConvertToSQL()
或trigger.ConvertToSQL()
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
var jobDetail = context.JobDetail;
var trigger = context.Trigger;
_logger.LogInformation(jobDetail.ConvertToSQL("作业信息表名", PersistenceBehavior.Appended)); // 输出新增语句
_logger.LogInformation(trigger.ConvertToSQL("作业触发器表名", PersistenceBehavior.Removed, NamingConventions.Pascal)); // 输出删除语句
_logger.LogInformation(trigger.ConvertToSQL("作业触发器表名", PersistenceBehavior.Updated, NamingConventions.UnderScoreCase)); // 输出更新语句
await Task.CompletedTask;
}
}
查看作业打印结果:
info: 2022-12-02 18:03:11.8543760 +08:00 星期五 L MyJob[0] #13
INSERT INTO 作业信息表名(
jobId,
groupName,
jobType,
assemblyName,
description,
concurrent,
includeAnnotations,
properties,
updatedTime
)
VALUES(
'job1',
NULL,
'MyJob',
'ConsoleApp32',
NULL,
1,
0,
'{}',
'2022-12-02 18:03:11.836'
);
info: 2022-12-02 18:03:11.8636268 +08:00 星期五 L MyJob[0] #13
DELETE FROM 作业触发器表名
WHERE TriggerId = 'job1_trigger1' AND JobId = 'job1';
info: 2022-12-02 18:03:11.8669134 +08:00 星期五 L MyJob[0] #13
UPDATE 作业触发器表名
SET
trigger_id = 'job1_trigger1',
job_id = 'job1',
trigger_type = 'Furion.Schedule.PeriodSecondsTrigger',
assembly_name = 'Furion',
args = '[5]',
description = NULL,
status = 2,
start_time = NULL,
end_time = NULL,
last_run_time = '2022-12-02 18:03:11.778',
next_run_time = '2022-12-02 18:03:16.794',
number_of_runs = 1,
max_number_of_runs = 0,
number_of_errors = 0,
max_number_of_errors = 0,
num_retries = 0,
retry_timeout = 1000,
start_now = 1,
run_on_start = 0,
reset_only_once = 1,
result = NULL,
elapsed_time = 100,
updated_time = '2022-12-02 18:03:11.836'
WHERE trigger_id = 'job1_trigger1' AND job_id = 'job1';
- 第四种:输出单独的作业
Monitor
信息:jobDetail.ConvertToMonitor()
或trigger.ConvertToMonitor()
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation(context.JobDetail.ConvertToMonitor());
_logger.LogInformation(context.Trigger.ConvertToMonitor());
await Task.CompletedTask;
}
}
查看作业打印结果:
info: 2022-12-02 18:04:06.2833095 +08:00 星期五 L MyJob[0] #8
┏━━━━━━━━━━━ JobDetail ━━━━━━━━━━━
┣ MyJob
┣
┣ jobId: job1
┣ groupName:
┣ jobType: MyJob
┣ assemblyName: ConsoleApp32
┣ description:
┣ concurrent: True
┣ includeAnnotations: False
┣ properties: {}
┣ updatedTime: 2022-12-02 18:04:06.254
┗━━━━━━━━━━━ JobDetail ━━━━━━━━━━━
info: 2022-12-02 18:04:06.2868205 +08:00 星期五 L MyJob[0] #8
┏━━━━━━━━━━━ Trigger ━━━━━━━━━━━
┣ Furion.Schedule.PeriodSecondsTrigger
┣
┣ triggerId: job1_trigger1
┣ jobId: job1
┣ triggerType: Furion.Schedule.PeriodSecondsTrigger
┣ assemblyName: Furion
┣ args: [5]
┣ description:
┣ status: Running
┣ startTime:
┣ endTime:
┣ lastRunTime: 2022-12-02 18:04:06.189
┣ nextRunTime: 2022-12-02 18:04:11.212
┣ numberOfRuns: 1
┣ maxNumberOfRuns: 0
┣ numberOfErrors: 0
┣ maxNumberOfErrors: 0
┣ numRetries: 0
┣ retryTimeout: 1000
┣ startNow: True
┣ runOnStart: False
┣ resetOnlyOnce: True
┣ result:
┣ elapsedTime: 100
┣ updatedTime: 2022-12-02 18:04:06.254
┗━━━━━━━━━━━ Trigger ━━━━━━━━━━━
26.1.2.5 运行时(动态)操作作业
有时候,我们需要在运行时对作业动态的增加,更新,删除等操作,如动态添加作业:
- 注册
services.AddSchedule()
服务
// 可以完全动态操作,只需要注册服务即可
services.AddSchedule();
// 也可以部分静态,部分动态注册
services.AddSchedule(options =>
{
options.AddJob<MyJob>(concurrent: false, Triggers.PeriodSeconds(5));
});
- 注入
ISchedulerFactory
服务
public class YourService: IYourService
{
private readonly ISchedulerFactory _schedulerFactory;
public YourService(ISchedulerFactory schedulerFactory)
{
_schedulerFactory = schedulerFactory;
}
public void AddJob()
{
_schedulerFactory.AddJob<MyJob>("动态作业 Id", Triggers.Secondly());
}
}
- 查看作业执行结果
info: 2022-12-02 18:07:33.7799062 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-02 18:07:33.7971487 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-02 18:07:33.8751390 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 18:07:33.8805159 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-02 18:07:33.9013656 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-02 18:07:38.9241031 +08:00 星期五 L MyJob[0] #9
<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-02 18:07:38.813 -> 2022-12-02 18:07:43.863
info: 2022-12-02 18:07:43.0865787 +08:00 星期五 L System.Logging.ScheduleService[0] #16
The <动态作业 Id_trigger1> trigger for scheduler of <动态作业 Id> successfully appended to the schedule.
warn: 2022-12-02 18:07:43.0894163 +08:00 星期五 L System.Logging.ScheduleService[0] #16
Schedule hosted service cancels hibernation and GC.Collect().
info: 2022-12-02 18:07:43.1129824 +08:00 星期五 L System.Logging.ScheduleService[0] #16
The scheduler of <动态作业 Id> successfully appended to the schedule.
info: 2022-12-02 18:07:43.8810686 +08:00 星期五 L MyJob[0] #17
<job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-02 18:07:43.863 -> 2022-12-02 18:07:48.848
info: 2022-12-02 18:07:44.0104025 +08:00 星期五 L MyJob[0] #16
<动态作业 Id> [C] <动态作业 Id 动态作业 Id_trigger1> * * * * * * 1ts 2022-12-02 18:07:44.000 -> 2022-12-02 18:07:45.000
info: 2022-12-02 18:07:45.0092441 +08:00 星期五 L MyJob[0] #8
<动态作业 Id> [C] <动态作业 Id 动态作业 Id_trigger1> * * * * * * 2ts 2022-12-02 18:07:45.000 -> 2022-12-02 18:07:46.000
26.1.2.6 作业触发器特性
默认情况下,框架不会扫描 IJob
实现类的作业触发器特性,但可以设置作业的 IncludeAnnotations
进行启用。
- 启用
IncludeAnnotations
扫描
services.AddSchedule(options =>
{
options.AddJob(JobBuilder.Create<MyJob>().SetIncludeAnnotations(true)
, Triggers.PeriodSeconds(5)); // 这里可传可不传,传了则会自动载入特性和这里配置的作业触发器
// 还可以更简单~~
options.AddJob(typeof(MyJob).ScanToBuilder());
// 还可以批量新增 Furion 4.8.2.4+
options.AddJob(App.EffectiveTypes.ScanToBuilders());
});
- 在
MyJob
中添加多个作业触发器特性
[Minutely]
[Cron("3,7,8 * * * * ?", CronStringFormat.WithSeconds)]
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context}");
await Task.CompletedTask;
}
}
- 查看作业执行结果
info: 2022-12-02 18:12:56.4199663 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-02 18:12:56.4287962 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-02 18:12:56.6149505 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 18:12:56.6205117 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger2> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 18:12:56.6266132 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The <job1_trigger3> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-02 18:12:56.6291006 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-02 18:12:56.6454334 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-02 18:13:00.0842828 +08:00 星期五 L MyJob[0] #15
<job1> [C] <job1 job1_trigger2> * * * * * 1ts 2022-12-02 18:13:00.000 -> 2022-12-02 18:14:00.000
info: 2022-12-02 18:13:01.5260220 +08:00 星期五 L MyJob[0] #16
<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-02 18:13:01.494 -> 2022-12-02 18:13:06.492
info: 2022-12-02 18:13:03.0076111 +08:00 星期五 L MyJob[0] #6
<job1> [C] <job1 job1_trigger3> 3,7,8 * * * * ? 1ts 2022-12-02 18:13:03.000 -> 2022-12-02 18:13:07.000
info: 2022-12-02 18:13:06.4954400 +08:00 星期五 L MyJob[0] #13
<job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-02 18:13:06.492 -> 2022-12-02 18:13:11.463
info: 2022-12-02 18:13:07.0180453 +08:00 星期五 L MyJob[0] #6
<job1> [C] <job1 job1_trigger3> 3,7,8 * * * * ? 2ts 2022-12-02 18:13:07.000 -> 2022-12-02 18:13:08.000
info: 2022-12-02 18:13:08.0114292 +08:00 星期五 L MyJob[0] #13
<job1> [C] <job1 job1_trigger3> 3,7,8 * * * * ? 3ts 2022-12-02 18:13:08.000 -> 2022-12-02 18:14:03.000
info: 2022-12-02 18:13:11.4774564 +08:00 星期五 L MyJob[0] #16
<job1> [C] <job1 job1_trigger1> 5s 3ts 2022-12-02 18:13:11.463 -> 2022-12-02 18:13:16.445
26.1.2.7 HTTP
请求作业
以下内容仅限 Furion 4.8.7.7 +
版本使用。
HTTP
请求作业通常用于定时请求/访问互联网地址。
services.AddSchedule(options =>
{
options.AddHttpJob(request =>
{
request.RequestUri = "https://www.chinadot.net";
request.HttpMethod = HttpMethod.Get;
// request.Body = "{}"; // 设置请求报文体
}, Triggers.PeriodSeconds(5));
});
System.Net.Http.IHttpClientFactory
错误如遇 Unable to resolve service for type 'System.Net.Http.IHttpClientFactory' while attempting to activate 'Furion.Schedule.HttpJob'.
错误,请先注册 servces.AddHttpClient()
服务。
作业执行日志如下:
info: 2023-03-11 11:05:36.3616747 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2023-03-11 11:05:36.3652411 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2023-03-11 11:05:36.5172940 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2023-03-11 11:05:36.5189296 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2023-03-11 11:05:36.5347816 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
warn: 2023-03-11 11:05:41.5228138 +08:00 星期六 L System.Logging.ScheduleService[0] #15
Schedule hosted service will sleep <4970> milliseconds and be waked up at <2023-03-11 11:05:46.486>.
info: 2023-03-11 11:05:41.5542865 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.LogicalHandler[100] #9
Start processing HTTP request GET https://www.chinadot.net/
info: 2023-03-11 11:05:41.5589056 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.ClientHandler[100] #9
Sending HTTP request GET https://www.chinadot.net/
info: 2023-03-11 11:05:44.1305461 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.ClientHandler[101] #8
Received HTTP response headers after 2566.7836ms - 200
info: 2023-03-11 11:05:44.1343977 +08:00 星期六 L System.Net.Http.HttpClient.HttpJob.LogicalHandler[101] #8
End processing HTTP request after 2584.2327ms - 200
info: 2023-03-11 11:05:48.6475959 +08:00 星期六 L System.Logging.ScheduleService[0] #4
Received HTTP response body with a length of <63639> output as follows - 200
<!DOCTYPE html><html><head>
<title>dotNET China | 让 .NET 开发更简单,更通用,更流行</title>
......
</body></html>
26.1.2.8 委托方式作业
有时我们需要快速开启新的定时作业但不考虑后续持久化存储(如数据库存储),这时可以使用委托作业方式,如:
services.AddSchedule(options =>
{
// 和 IJob 的 ExecuteAsync 方法签名一致
options.AddJob((context, stoppingToken) =>
{
// 可通过 context.ServiceProvider 解析服务;框架提供了 .GetLogger() 拓展方法输出日志
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}, Triggers.PeriodSeconds(5));
});
作业执行日志如下:
info: 2023-03-21 14:22:34.1910781 +08:00 星期二 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2023-03-21 14:22:34.1967420 +08:00 星期二 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2023-03-21 14:22:34.6163320 +08:00 星期二 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2023-03-21 14:22:34.6195112 +08:00 星期二 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2023-03-21 14:22:34.6398162 +08:00 星期二 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2023-03-21 14:22:39.7171392 +08:00 星期二 L System.Logging.DynamicJob[0] #9
<job1> [C] <job1 job1_trigger1> 5s 1ts 2023-03-21 14:22:39.575 -> 2023-03-21 14:22:44.623
info: 2023-03-21 14:22:44.6986483 +08:00 星期二 L System.Logging.DynamicJob[0] #9
<job1> [C] <job1 job1_trigger1> 5s 2ts 2023-03-21 14:22:44.623 -> 2023-03-21 14:22:49.657
26.1.2.9 非 IOC/DI
项目中使用
以下内容仅限 Furion 4.8.8.5 +
版本使用。
在一些不支持依赖注入的项目类型如 Console、WinForm、WPF
中,可以通过以下方式使用:
- 方式一:无需获取其他服务对象
_ = new ServiceCollection()
.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Period(5000));
})
.GetScheduleHostedService()
.StartAsync(new CancellationTokenSource().Token);
- 方式二:需要后续解析服务
// 注册服务并构建
IServiceProvider services = new ServiceCollection()
.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Period(5000));
})
.BuildServiceProvider();
// 启动作业调度主机服务
services.GetScheduleHostedService()
.StartAsync(new CancellationTokenSource().Token);
// 解析作业计划工厂
var schedulerFactory = services.GetService<ISchedulerFactory>();
只需要将 services
对象用类的静态属性存储起来即可,如:
public class DI
{
public static IServiceProvider Services {get; set;}
}
之后通过 DI.Services = services;
即可,后续便可以通过 DI.Services.GetService<T>()
解析服务。
26.1.3 作业信息 JobDetail
及构建器
26.1.3.1 关于作业信息
框架提供了 JobDetail
类型来描述作业信息,JobDetail
类型提供以下只读属性:
属性名 | 属性类型 | 默认值 | 说明 |
---|---|---|---|
JobId | string | 作业 Id | |
GroupName | string | 作业组名称 | |
JobType | string | 作业处理程序类型,存储的是类型的 FullName | |
AssemblyName | string | 作业处理程序类型所在程序集,存储的是程序集 Name | |
Description | string | 描述信息 | |
Concurrent | bool | true | 作业执行方式,如果设置为 false ,那么使用 串行 执行,否则 并行 执行 |
IncludeAnnotations | bool | false | 是否扫描 IJob 实现类 [Trigger] 特性触发器 |
Properties | string | "{}" | 作业信息额外数据,由 Dictionary<string, object> 序列化成字符串存储 |
UpdatedTime | DateTime? | 作业更新时间 |
26.1.3.2 关于作业信息构建器
作业信息 JobDetail
是作业调度模块提供运行时的只读类型,那么我们该如何创建或变更 JobDetail
对象呢?
JobBuilder
是作业调度模块提供可用来生成运行时 JobDetail
的类型,这样做的好处可避免外部直接修改运行时 JobDetail
数据,还能实现任何修改动作监听,也能避免多线程抢占情况。
作业调度模块提供了多种方式用来创建 JobBuilder
对象。
- 通过
Create
静态方法创建
// 根据作业 Id 创建
var jobBuilder = JobBuilder.Create("job1");
// 根据 IJob 实现类类型创建
var jobBuilder = JobBuilder.Create<MyJob>();
// 根据程序集名称和类型完全限定名(FullName)创建
var jobBuilder = JobBuilder.Create("YourProject", "YourProject.MyJob");
// 根据 Type 类型创建
var jobBuilder = JobBuilder.Create(typeof(MyJob));
// 通过委托创建动态作业
var jobBuilder = JobBuilder.Create((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
});
- 通过
JobDetail
类型创建
这种方式常用于在运行时更新作业信息。
var jobBuilder = JobBuilder.From(jobDetail);
//也可以通过以下方式
var jobBuilder = jobDetail.GetBuilder();
- 通过
JSON
字符串创建
该方式非常灵活,可从配置文件,JSON
字符串,或其他能够返回 JSON
字符串的地方创建。
var jobBuilder = JobBuilder.From(@"{
""jobId"": ""job1"",
""groupName"": null,
""jobType"": ""MyJob"",
""assemblyName"": ""ConsoleApp13"",
""description"": null,
""concurrent"": true,
""includeAnnotations"": false,
""properties"": ""{}"",
""updatedTime"": null
}");
如果使用的是 .NET7
,可使用 """
避免转义,如:
var jobBuilder = JobBuilder.From("""
{
"jobId": "job1",
"groupName": null,
"jobType": "MyJob",
"assemblyName": "ConsoleApp13",
"description": null,
"concurrent": true,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-12-02 18:00:59.390"
}
""");
支持 CamelCase(驼峰命名法)
,Pascal(帕斯卡命名法)
命名方式。
不支持 UnderScoreCase(下划线命名法)
,如 "include_annotations": true
- 还可以通过
Clone
静态方法从一个JobBuilder
创建
var jobBuilder = JobBuilder.Clone(fromJobBuilder);
克隆操作只会克隆 AssemblyName
,JobType
,GroupName
,Description
,Concurrent
,IncludeAnnotations
,Properties
,DynamicExecuteAsync
(动态作业)。
- 不会克隆
JobId
,UpdatedTime
。
- 还可以通过
LoadFrom
实例方法填充当前的JobBuilder
比如可以传递匿名类型,类类型,字典 Dictionary<string, object>
类型:
// 会覆盖所有相同的值
jobBuilder.LoadFrom(new
{
Description = "我是描述",
Concurrent = false
});
// 支持多个填充,还可以配置跳过 null 值覆盖
jobBuilder.LoadFrom(new
{
Description = "我是另外一个描述",
Concurrent = false,
IncludeAnnotations = default(object) // 会跳过赋值
}, ignoreNullValue: true);
// 支持忽略特定属性名映射
jobBuilder.LoadFrom(new
{
Description = "我是另外一个描述",
Concurrent = false,
IncludeAnnotations = default(object) // 会跳过赋值
}, ignorePropertyNames: new[]{ "description" });
// 支持字典类型
jobBuilder.LoadFrom(new Dictionary<string, object>
{
{"Description", "这是新的描述" },
{"include_annotations", false },
{"updatedTime", DateTime.Now }
});
支持 CamelCase(驼峰命名法)
,Pascal(帕斯卡命名法)
和 UnderScoreCase(下划线命名法)
命名方式。
26.1.3.3 设置作业信息构建器
JobBuilder
提供了和 JobDetail
完全匹配的 Set[属性名]
方法来配置作业信息各个属性,如:
services.AddSchedule(options =>
{
var jobBuilder = JobBuilder.Create<MyJob>()
.SetJobId("job1") // 作业 Id
.SetGroupName("group1") // 作业组名称
.SetJobType("Furion.Application", "Furion.Application.MyJob") // 作业类型,支持多个重载
.SetJobType<MyJob>() // 作业类型,支持多个重载
.SetJobType(typeof(MyJob)) // 作业类型,支持多个重载
.SetDescription("这是一段描述") // 作业描述
.SetConcurrent(false) // 并行还是串行方式,false 为 串行
.SetIncludeAnnotations(true) // 是否扫描 IJob 类型的触发器特性,true 为 扫描
.SetProperties("{}") // 作业额外数据 Dictionary<string, object> 类型序列化,支持多个重载
.SetProperties(new Dictionary<string, object> { { "name", "Furion" } }) // 作业类型额外数据,支持多个重载,推荐!!!
.SetDynamicExecuteAsync((context, stoppingToken) => {
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}) // 动态委托处理程序,一旦设置了此委托,那么优先级将大于 MyJob 的 ExecuteAsync
;
options.AddJob(jobBuilder, Triggers.PeriodSeconds(5));
});
26.1.3.4 作业信息/构建器额外数据
有时候我们需要在作业运行的时候添加一些额外数据,或者实现多个触发器共享数据,经常用于 串行
执行中(并行
也同样工作),后面一个触发器需等待前一个触发器完成。
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
var jobDetail = context.JobDetail;
var count = jobDetail.GetProperty<int>("count");
jobDetail.AddOrUpdateProperty("count", count + 1); // 递增 count
_logger.LogInformation($"count: {count} {context}");
await Task.CompletedTask;
}
}
查看作业运行日志:
info: 2022-12-03 23:16:46.5150228 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-03 23:16:46.5197497 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-03 23:16:46.6987703 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-03 23:16:46.7003295 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-03 23:16:46.7248216 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-03 23:16:51.7013640 +08:00 星期六 L MyJob[0] #8
count: 0 <job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-03 23:16:51.663 -> 2022-12-03 23:16:56.656
info: 2022-12-03 23:16:56.6768044 +08:00 星期六 L MyJob[0] #9
count: 1 <job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-03 23:16:56.656 -> 2022-12-03 23:17:01.635
info: 2022-12-03 23:17:01.6454604 +08:00 星期六 L MyJob[0] #8
count: 2 <job1> [C] <job1 job1_trigger1> 5s 3ts 2022-12-03 23:17:01.635 -> 2022-12-03 23:17:06.608
info: 2022-12-03 23:17:06.6247917 +08:00 星期六 L MyJob[0] #6
count: 3 <job1> [C] <job1 job1_trigger1> 5s 4ts 2022-12-03 23:17:06.608 -> 2022-12-03 23:17:11.586
作业调度模块为 JobDetail
和 JobBuilder
提供了多个方法操作额外数据:
// 查看所有额外数据
var properties = jobDetail.GetProperties();
// 查看单个额外数据,返回 object
var value = jobBuilder.GetProperty("key");
// 查看单个额外数据泛型
var value = jobDetail.GetProperty<int>("key");
// 添加新的额外数据,支持链式操作,如果键已存在,则跳过
jobDetail.AddProperty("key", "Furion").AddProperty("key1", 2);
// 添加或更新额外数据,支持链式操作,不存在则新增,存在则替换,推荐
jobDetail.AddOrUpdateProperty("key", "Furion").AddOrUpdateProperty("key1", 2);
// 还可以通过委托的方式:如果键不存在则插入 count = newValue,否则更新为 value(旧值)+1
jobDetail.AddOrUpdateProperty("count", newValue, value => value + 1);
// 删除某个额外数据,支持链式操作,如果 key 不存在则跳过
jobDetail.RemoveProperty("key").RemoveProperty("key1");
// 清空所有额外数据
jobDetail.ClearProperties();
作业额外数据每一项的值只支持 int32
,int64
,string
,bool
,null
或它们组成的数组类型。
26.1.3.5 作业信息特性
作业信息特性 [JobDetail]
是为了方便运行时或启动时快速创建作业计划构建器而提供的,可在启动时或运行时通过以下方式创建,如:
[JobDetail("job1", "这是一段描述")]
[PeriodSeconds(5, TriggerId = "trigger1")]
public class MyJob : IJob
{
}
- 启动
IncludeAnnotations
属性自动填充
services.AddSchedule(options =>
{
options.AddJob(JobBuilder.Create<MyJob>()
.SetIncludeAnnotations(true)); // 此时 [JobDetail] 配置的非空属性将自动复制给 JobBuilder,[PeriodSeconds] 也会自动创建 TriggerBuilder
});
- 手动扫描并创建作业计划构建器
var schedulerBuilder = typeof(MyJob).ScanToBuilder();
- 通过程序集类型扫描批量创建作业计划构建器
也可以用于作业持久化 Preload
初始化时使用:
public IEnumerable<SchedulerBuilder> Preload()
{
// 扫描所有类型并创建
return App.EffectiveTypes.Where(t => t.IsJobType())
.Select(t => t.ScanToBuilder());
// 还可以更简单~~
return App.EffectiveTypes.ScanToBuilders();
}
作业信息特性还提供了多个属性配置,如:
JobId
:作业信息 Id,string
类型GroupName
:作业组名称,string
类型Description
:描述信息,string
类型Concurrent
:是否采用并行执行,bool
类型,如果设置为false
,那么使用串行
执行
使用如下:
[JobDetail("jobId")] // 仅作业 Id
[JobDetail("jobId", "这是一段描述")] // 描述
[JobDetail("jobId", false)] // 串行
[JobDetail("jobId", false, "这是一段描述")] // 串行 + 描述
[JobDetail("jobId", Concurrent = false, Description = "这是一段描述")]
[JobDetail("jobId", Concurrent = false, Description = "这是一段描述", GroupName = "分组名")]
public class MyJob : IJob
{
// ....
}
26.1.3.6 多种格式字符串输出
JobDetail
和 JobBuilder
都提供了多种将自身转换成特定格式的字符串。
- 转换成
JSON
字符串
var json = jobDetail.ConvertToJSON();
字符串打印如下:
{
"jobId": "job1",
"groupName": null,
"jobType": "MyJob",
"assemblyName": "ConsoleApp13",
"description": null,
"concurrent": true,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-12-04 11:51:00.483"
}
- 转换成
SQL
字符串
// 输出新增 SQL,使用 CamelCase 属性命名
var insertSql = jobDetail.ConvertToSQL("tbName"
, PersistenceBehavior.Appended
, NamingConventions.CamelCase);
// 更便捷拓展
var insertSql = jobDetail.ConvertToInsertSQL("tbName", NamingConventions.CamelCase);
// 输出删除 SQL,使用 Pascal 属性命名
var deleteSql = jobDetail.ConvertToSQL("tbName"
, PersistenceBehavior.Removed
, NamingConventions.Pascal);
// 更便捷拓展
var deleteSql = jobDetail.ConvertToDeleteSQL("tbName", NamingConventions.Pascal);
// 输出更新 SQL,使用 UnderScoreCase 属性命名
var updateSql = jobDetail.ConvertToSQL("tbName"
, PersistenceBehavior.Updated
, NamingConventions.UnderScoreCase);
// 更便捷拓展
var updateSql = jobDetail.ConvertToUpdateSQL("tbName", NamingConventions.UnderScoreCase);
字符串打印如下:
-- 新增语句
INSERT INTO tbName(
jobId,
groupName,
jobType,
assemblyName,
description,
concurrent,
includeAnnotations,
properties,
updatedTime
)
VALUES(
'job1',
NULL,
'MyJob',
'ConsoleApp13',
NULL,
1,
0,
'{}',
'2022-12-04 11:53:05.489'
);
-- 删除语句
DELETE FROM tbName
WHERE JobId = 'job1';
-- 更新语句
UPDATE tbName
SET
job_id = 'job1',
group_name = NULL,
job_type = 'MyJob',
assembly_name = 'ConsoleApp13',
description = NULL,
concurrent = 1,
include_annotations = 0,
properties = '{}',
updated_time = '2022-12-04 11:53:05.489'
WHERE job_id = 'job1';
- 转换成
Monitor
字符串
var monitor = jobDetail.ConvertToMonitor();
字符串打印如下:
┏━━━━━━━━━━━ JobDetail ━━━━━━━━━━━
┣ MyJob
┣
┣ jobId: job1
┣ groupName:
┣ jobType: MyJob
┣ assemblyName: ConsoleApp13
┣ description:
┣ concurrent: True
┣ includeAnnotations: False
┣ properties: {}
┣ updatedTime: 2022-12-04 11:55:11.186
┗━━━━━━━━━━━ JobDetail ━━━━━━━━━━━
- 简要字符串输出
var str = jobDetail.ToString();
字符串打印如下:
<job1> 这是一段描述 [C]
26.1.3.7 自定义 SQL
输出配置
以下内容仅限 Furion 4.8.2 +
版本使用。
services.AddSchedule(options =>
{
options.JobDetail.ConvertToSQL = (tableName, columnNames, jobDetail, behavior, naming) =>
{
// 生成新增 SQL
if (behavior == PersistenceBehavior.Appended)
{
return jobDetail.ConvertToInsertSQL(tableName, naming);
}
// 生成更新 SQL
else if (behavior == PersistenceBehavior.Updated)
{
return jobDetail.ConvertToUpdateSQL(tableName, naming);
}
// 生成删除 SQL
else if (behavior == PersistenceBehavior.Removed)
{
return jobDetail.ConvertToDeleteSQL(tableName, naming);
}
return string.Empty;
};
});
ConvertToSQL
委托参数说明tableName
:数据库表名称,string
类型columnNames
:数据库列名:string[]
类型,只能通过索引
获取jobDetail
:作业信息JobDetail
对象behavior
:持久化PersistenceBehavior
类型,用于标记新增
,更新
还是删除
操作naming
:命名法NamingConventions
类型,包含CamelCase(驼峰命名法)
,Pascal(帕斯卡命名法)
和UnderScoreCase(下划线命名法)
如果在该自定义 SQL
输出方法中调用 jobDetail.ConvertToSQL(..)
会导致死循环。
26.1.3.8 启用作业执行日志输出
以下内容仅限 Furion 4.8.3.7 +
版本使用。
通常我们需要在 IJob
实现类中输出作业触发日志,如 _logger.LogInformation($"{context}");
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context}");
return Task.CompletedTask;
}
}
但这样的 范式代码
几乎每一个 IJob
实现类都可能输出,所以在 Furion 4.8.3.7+
版本提供了更便捷的配置,无需每一个 IJob
编写 _logger.LogInformation($"{context}");
。
配置启用如下:
services.AddSchedule(options =>
{
options.JobDetail.LogEnabled = true; // 默认 false
});
之后 MyJob
可以更加精简了,日志类别自动设置为 MyJob
类型完整限定名。
public class MyJob : IJob
{
public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
// 这里写业务逻辑即可,无需调用 _logger.LogInformation($"{context}");
return Task.CompletedTask;
}
}
作业执行日志如下:
info: 2022-12-14 11:56:12.3963326 +08:00 星期三 L Furion.Application.MyJob[0] #4
<job1> [C] <job1 job1_trigger2> 5s 1ts 2022-12-14 11:56:08.361 -> 2022-12-14 11:56:13.366
info: 2022-12-14 11:56:13.4100745 +08:00 星期三 L Furion.Application.MyJob[0] #6
<job1> [C] <job1 job1_trigger2> 5s 2ts 2022-12-14 11:56:13.366 -> 2022-12-14 11:56:18.376
info: 2022-12-14 11:56:18.3931380 +08:00 星期三 L Furion.Application.MyJob[0] #9
<job1> [C] <job1 job1_trigger2> 5s 3ts 2022-12-14 11:56:18.376 -> 2022-12-14 11:56:23.360
26.1.4 作业处理程序 IJob
作业处理程序是作业符合触发时间执行的业务逻辑代码,通常由程序员编写,作业处理程序需实现 IJob
接口。
26.1.4.1 如何定义
public class MyJob : IJob
{
public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
// your code...
}
}
26.1.4.2 JobExecutingContext
上下文
JobExecutingContext
上下文作为 ExecuteAsync
方法的第一个参数,包含以下运行时信息:
JobExecutingContext
属性列表JobId
:作业Id
TriggerId
:当前触发器Id
JobDetail
:作业信息Trigger
:作业触发器OccurrenceTime
:作业计划触发时间,最准确的记录时间ExecutingTime
:实际执行时间(可能存在误差)RunId
:本次作业执行唯一Id
,Furion 4.8.5.1+
提供Result
:设置/读取本次作业执行结果,Furion 4.8.7.7+
提供ServiceProvider
:服务提供器,Furion 4.8.7.10+
提供
JobExecutingContext
方法列表.ConvertToJSON(naming)
:将上下文转换成JSON
字符串.ToString()
:输出为字符串
26.1.4.3 作业处理程序实例
以下内容仅限 Furion 4.8.8.13 +
版本使用。
默认情况下,作业处理程序会在作业触发器符合触发条件下通过 ActivatorUtilities.CreateInstance
动态创建,也就是每次触发都会创建新的 IJob
实例,如:
var jobHandler = ActivatorUtilities.CreateInstance(_serviceProvider, jobType);
其中 _serviceProvider
是单例服务提供器,所以 IJob
实现类只能通过构造函数注入 单例服务
。如果没有范围作用域服务的需求,那么可以将 IJob
注册为单例服务,这样就可以避免每次重复创建 IJob
实例,对性能和减少内存占用有不小优化。 如:
services.AddSingleton<YourJob>();
如果希望能够在构造函数注入范围作用域或瞬时作用域,可实现 IJobFactory
接口,如:
using Furion.Schedule;
using Microsoft.Extensions.DependencyInjection;
namespace Furion.Application;
public class JobFactory : IJobFactory
{
public IJob CreateJob(IServiceProvider serviceProvider, JobFactoryContext context)
{
return ActivatorUtilities.CreateInstance(serviceProvider, context.JobType) as IJob;
// 如果通过 services.AddSingleton<YourJob>(); 或 serivces.AddScoped<YourJob>(); 或 services.AddTransient<YourJob> 可通过下列方式
// return serviceProvider.GetRequiredService(context.JobType) as IJob;
}
}
之后注册 JobFactory
即可,如:
services.AddSchedule(options =>
{
// 添加作业处理程序工厂
options.AddJobFactory<JobFactory>();
});
这样作业就可以注入范围和瞬时服务了。
26.1.4.4 依赖注入
实现 IJob
的作业处理程序类型默认注册为 单例
,那么只要是单例的服务,皆可以通过构造函数注入,如:ILogger<>
,IConfiguration
。
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
private readonly IConfiguration _configuration;
public MyJob(ILogger<MyJob> logger
, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context} {_configuration["key"]}");
await Task.CompletedTask;
}
}
- 如果是非
单例
的接口,如瞬时
或范围
服务,可通过IServiceScopeFactory
创建
IJobFactory
方式Furion 4.8.8.13+
版本可以通过上一小节 IJobFactory
统一实现。推荐。
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
private readonly IConfiguration _configuration;
private readonly IServiceScopeFactory _scopeFactory;
public MyJob(ILogger<MyJob> logger
, IConfiguration configuration
, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_configuration = configuration;
_schedulerFactory = scopeFactory;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
using var serviceScope = _scopeFactory.CreateScope();
var repository = serviceScope.ServiceProvider.GetService<IRepository<User>>();
_logger.LogInformation($"{context} {_configuration["key"]}");
await Task.CompletedTask;
}
}
- 针对高频定时任务,比如每秒执行一次,或者更频繁的任务
IJobFactory
方式Furion 4.8.8.13+
版本可以通过上一小节 IJobFactory
统一实现。推荐。
为了避免频繁创建作用域和销毁作用域,可创建长范围的作用域。
public class MyJob : IJob, IDisposable
{
private readonly ILogger<MyJob> _logger;
private readonly IConfiguration _configuration;
private readonly IServiceScope _serviceScope;
public MyJob(ILogger<MyJob> logger
, IConfiguration configuration
, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_configuration = configuration;
_serviceScope = scopeFactory.CreateScope();
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
var repository = _serviceScope.ServiceProvider.GetService<IRepository<User>>();
var user = await repository.GetAsync(1);
_logger.LogInformation($"{context} {_configuration["key"]}");
await Task.CompletedTask;
}
public void Dispose()
{
_serviceScope?.Dispose();
}
}
26.1.4.5 动态作业 DynamicJob
框架提供了便捷的动态作业 DynamicJob
类型,可通过 Func<JobExecutingContext, CancellationToken, Task>
委托传入,无需创建 IJob
实现类型。
框架还为 JobExecutionContext
属性 ServiceProvder
提供了 .GetLogger()
拓展方法,方便快速获取 ILogger<System.Logging.DynamicJob>
日志对象实例。
// 通过 JobBuilder 创建
var jobBuilder = JobBuilder.Create((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
});
// 通过 jobBuilder 方法 SetDynamicExecuteAsync 创建
jobBuilder.SetDynamicExecuteAsync((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
});
// 通过 AddJob 创建
service.AddSchedule(options =>
{
options.AddJob((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}, Triggers.PeriodSeconds(5));
});
// 通过 ISchedulerFactory 创建
_schedulerFactory.AddJob((context, stoppingToken) =>
{
context.ServiceProvider.GetLogger().LogInformation($"{context}");
return Task.CompletedTask;
}, Triggers.PeriodSeconds(5));
动态作业执行结果:
info: 2022-12-04 12:26:18.6562296 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 12:26:18.6618404 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 12:26:18.8727764 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-04 12:26:18.8745765 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 12:26:18.9013540 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-04 12:26:23.8753926 +08:00 星期日 L System.Logging.DynamicJob[0] #6
<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-04 12:26:23.837 -> 2022-12-04 12:26:28.835
info: 2022-12-04 12:26:28.8686474 +08:00 星期日 L System.Logging.DynamicJob[0] #6
<job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-04 12:26:28.835 -> 2022-12-04 12:26:33.823
info: 2022-12-04 12:26:33.8531796 +08:00 星期日 L System.Logging.DynamicJob[0] #13
<job1> [C] <job1 job1_trigger1> 5s 3ts 2022-12-04 12:26:33.823 -> 2022-12-04 12:26:38.820
- 动态作业处理程序类型是:
DynamicJob
类型 - 动态作业提供的
.GetLogger()
拓展输出日志类别是:System.Logging.DynamicJob
- 如果普通作业同时设置了
SetJobType
和SetDynamicExecuteAsync
,那么优先作为动态作业执行。 - 动态作业无法将
Func<..>
进行序列化持久化存储
26.1.4.6 使用 Roslyn
动态创建
以下内容仅限 Furion 4.8.8.7 +
版本使用。
按照程序开发的正常思维,理应先在代码中创建作业处理程序类型,但我们可以借助 Roslyn
动态编译 C#
代码。
- 根据字符串创建
IJob
类型
// 调用 Schedular 静态类提供的 CompileCSharpClassCode 方法
var jobAssembly = Schedular.CompileCSharpClassCode(@"
using Furion.Schedule;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace YourProject;
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($""我是 Roslyn 方式创建的:{context}"");
await Task.CompletedTask;
}
}
");
// 生成运行时 MyJob 类型
var jobType = jobAssembly.GetType("YourProject.MyJob");
- 注册作业
// 可以在启动的时候添加
services.AddSchedule(options =>
{
options.AddJob(jobType
, Triggers.PeriodSeconds(5));
});
// 也可以完全在运行时添加(常用)
_schedulerFactory.AddJob(jobType
, Triggers.PeriodSeconds(5));
查看作业执行日志:
info: 2022-12-04 12:38:00.6249410 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2022-12-04 12:38:00.6294089 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2022-12-04 12:38:00.7496005 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2022-12-04 12:38:00.7514579 +08:00 星期日 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2022-12-04 12:38:00.7836777 +08:00 星期日 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2022-12-04 12:38:05.7389682 +08:00 星期日 L YourProject.MyJob[0] #6
我是 Roslyn 方式创建的:<job1> [C] <job1 job1_trigger1> 5s 1ts 2022-12-04 12:38:05.713 -> 2022-12-04 12:38:10.692
info: 2022-12-04 12:38:10.7108416 +08:00 星期日 L YourProject.MyJob[0] #11
我是 Roslyn 方式创建的:<job1> [C] <job1 job1_trigger1> 5s 2ts 2022-12-04 12:38:10.692 -> 2022-12-04 12:38:15.673
info: 2022-12-04 12:38:15.6925578 +08:00 星期日 L YourProject.MyJob[0] #11
我是 Roslyn 方式创建的:<job1> [C] <job1 job1_trigger1> 5s 3ts 2022-12-04 12:38:15.673 -> 2022-12-04 12:38:20.656
惊不惊喜,意外意外~。
通过 Roslyn
的方式支持创建 IJob
,JobDetail
,Trigger
,Scheduler
哦,自行测试。😊
26.1.4.7 作业执行异常处理
正常情况下,程序员应该保证作业执行程序总是稳定运行,但有时候会出现一些不可避免的意外导致出现异常,如网络异常等。
下面给出模拟出现异常和常见的处理方式例子:
services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.PeriodSeconds(3));
});
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
public MyJob(ILogger<MyJob> logger)
{
_logger = logger;
}
public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
_logger.LogInformation($"{context}");
// 模拟异常
var num = 10;
var n = 0;
var c = num / n;
return Task.CompletedTask;
}
}
输出日志如下:
info: 2023-04-22 22:18:04.2149071 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is running.
info: 2023-04-22 22:18:04.2189082 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service is preloading...
info: 2023-04-22 22:18:04.3216571 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The <job1_trigger1> trigger for scheduler of <job1> successfully appended to the schedule.
info: 2023-04-22 22:18:04.3230110 +08:00 星期六 L System.Logging.ScheduleService[0] #1
The scheduler of <job1> successfully appended to the schedule.
warn: 2023-04-22 22:18:04.3521056 +08:00 星期六 L System.Logging.ScheduleService[0] #1
Schedule hosted service preload completed, and a total of <1> schedulers are appended.
info: 2023-04-22 22:18:07.3782666 +08:00 星期六 L MyJob[0] #17
<job1> [C] <job1 job1_trigger1> 3s 1ts 2023-04-22 22:18:07.288 -> 2023-04-22 22:18:10.308
fail: 2023-04-22 22:18:07.6652239 +08:00 星期六 L System.Logging.ScheduleService[0] #17
Error occurred executing <job1> [C] <job1 job1_trigger1> 3s 1ts 2023-04-22 22:18:07.288 -> 2023-04-22 22:18:10.308.
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
System.DivideByZeroException: Attempted to divide by zero.
at MyJob.ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) in C:\Users\MonkSoul\source\repos\ConsoleApp3\Program.cs:line 29
at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_3.<<BackgroundProcessing>b__3>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 233
--- End of stack trace from previous location ---
at Furion.FriendlyException.Retry.InvokeAsync(Func`1 action, Int32 numRetries, Int32 retryTimeout, Boolean finalThrow, Type[] exceptionTypes, Func`2 fallbackPolicy, Action`2 retryAction) in C:\Workplaces\Furion\framework\Furion\FriendlyException\Retry.cs:line 79
at Furion.Schedule.ScheduleHostedService.<>c__DisplayClass24_2.<<BackgroundProcessing>b__2>d.MoveNext() in C:\Workplaces\Furion\framework\Furion\Schedule\HostedServices\ScheduleHostedService.cs:line 231
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
info: 2023-04-22 22:18:10.3507729 +08:00 星期六 L MyJob[0