Skip to main content

26.1 调度作业

📝 模块更新日志
  • 新特性

    •   定时任务间隔分钟作业触发器 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 所有带 AtCron 表达式触发器构建器及特性 4.8.2.5 ⏱️2022.11.29 #I63PLR
    •   定时任务批量添加 SchedulerBuilder 作业功能 4.8.2.4 ⏱️2022.11.29 5faa67b
    •   定时任务 BuildSqlType 配置,可设置生成不同数据库类型的 SQL 语句 4.8.2.3 ⏱️2022.11.29 293f9bc !675
    •   JobDetailTrigger 自定义 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
  • 问题修复

    •   作业触发器不符合下一次执行规律但 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
  • 其他更改

    •   定时任务调度器时间精度,控制持续执行一年误差在 100ms 以内 4.8.2.9 ⏱️2022.12.01 334d089
    •   定时任务作业计划工厂 GetNextRunJobs() 方法逻辑 4.8.2.7 ⏱️2022.11.30 #I63VS2
  • 文档

    •   作业触发器 ResetOnlyOnce 文档 4.8.1.5 ⏱️2022.11.25 a8be728
    •   通过 Roslyn 动态编译代码创建 IJob 类型文档 4.8.1.5 ⏱️2022.11.25 2c5e5be
    •   自定义 JobDetailTrigger 输出 SQL 文档 4.8.2 ⏱️2022.11.27 0bb9d8f
4.8.0 以下版本说明

Furion 4.8.0+ 版本采用 Sundial 定时任务替换原有的 TaskScheduler查看旧文档

版本说明

以下内容仅限 Furion 4.8.0 + 版本使用。

26.1.1 关于调度作业

调度作业又称定时任务,顾名思义,定时任务就是在特定的时间或符合某种时间规律自动触发并执行任务。

26.1.2 快速入门

  1. 定义作业处理程序 MyJob
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.CompletedTask;
}
}
  1. Startup.cs 注册 Schedule 服务:
services.AddSchedule(options =>
{
// 注册作业,并配置作业触发器
options.AddJob<MyJob>(Triggers.Secondly()); // 表示每秒执行
});
  1. 查看作业执行结果
info: 2022-11-17 16:23:56.0166669 +08:00 星期四 L MyJob[0] #16
job1 job1_trigger1 2022/11/17 16:23:56 * * * * * *
info: 2022-11-17 16:23:57.0125960 +08:00 星期四 L MyJob[0] #17
job1 job1_trigger1 2022/11/17 16:23:57 * * * * * *
info: 2022-11-17 16:23:58.0120379 +08:00 星期四 L MyJob[0] #16
job1 job1_trigger1 2022/11/17 16:23:58 * * * * * *
info: 2022-11-17 16:23:59.0071986 +08:00 星期四 L MyJob[0] #5
job1 job1_trigger1 2022/11/17 16:23:59 * * * * * *
info: 2022-11-17 16:24:00.0196813 +08:00 星期四 L MyJob[0] #16
job1 job1_trigger1 2022/11/17 16:24:00 * * * * * *
info: 2022-11-17 16:24:01.0305799 +08:00 星期四 L MyJob[0] #17
job1 job1_trigger1 2022/11/17 16:24:01 * * * * * *

26.1.2.1 指定作业 Id

默认情况下,不指定作业 Id 会自动生成 job[编号]

services.AddSchedule(options =>
{
options.AddJob<MyJob>("myjob", Triggers.Secondly());
});

查看作业执行结果:

info: 2022-11-17 16:25:44.0339177 +08:00 星期四 L MyJob[0] #3
myjob myjob_trigger1 2022/11/17 16:25:44 * * * * * *
info: 2022-11-17 16:25:45.0064838 +08:00 星期四 L MyJob[0] #14
myjob myjob_trigger1 2022/11/17 16:25:45 * * * * * *
info: 2022-11-17 16:25:46.0186243 +08:00 星期四 L MyJob[0] #15
myjob myjob_trigger1 2022/11/17 16:25:46 * * * * * *
info: 2022-11-17 16:25:47.0175115 +08:00 星期四 L MyJob[0] #16
myjob myjob_trigger1 2022/11/17 16:25:47 * * * * * *
info: 2022-11-17 16:25:48.0304982 +08:00 星期四 L MyJob[0] #15
myjob myjob_trigger1 2022/11/17 16:25:48 * * * * * *
info: 2022-11-17 16:25:49.0070855 +08:00 星期四 L MyJob[0] #16
myjob myjob_trigger1 2022/11/17 16:25:49 * * * * * *

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.Cron("3,7,8 * * * * ?", CronStringFormat.WithSeconds)); // 每分钟第 3/7/8 秒
});

查看作业执行结果:

info: 2022-11-17 16:45:40.5258191 +08:00 星期四 L MyJob[0] #14
job1 job1_trigger2 2022/11/17 16:45:40 5000ms
info: 2022-11-17 16:45:45.5281473 +08:00 星期四 L MyJob[0] #3
job1 job1_trigger2 2022/11/17 16:45:45 5000ms
info: 2022-11-17 16:45:50.5378417 +08:00 星期四 L MyJob[0] #8
job1 job1_trigger2 2022/11/17 16:45:50 5000ms
info: 2022-11-17 16:45:55.5436499 +08:00 星期四 L MyJob[0] #3
job1 job1_trigger2 2022/11/17 16:45:55 5000ms
info: 2022-11-17 16:46:00.0253985 +08:00 星期四 L MyJob[0] #14
job1 job1_trigger1 2022/11/17 16:46:00 * * * * *
info: 2022-11-17 16:46:00.5494676 +08:00 星期四 L MyJob[0] #16
job1 job1_trigger2 2022/11/17 16:46:00 5000ms
info: 2022-11-17 16:46:03.0238143 +08:00 星期四 L MyJob[0] #15
job1 job1_trigger3 2022/11/17 16:46:03 3,7,8 * * * * ?
info: 2022-11-17 16:46:05.5629293 +08:00 星期四 L MyJob[0] #14
job1 job1_trigger2 2022/11/17 16:46:05 5000ms
info: 2022-11-17 16:46:07.0169836 +08:00 星期四 L MyJob[0] #15
job1 job1_trigger3 2022/11/17 16:46:07 3,7,8 * * * * ?
info: 2022-11-17 16:46:08.0128756 +08:00 星期四 L MyJob[0] #14
job1 job1_trigger3 2022/11/17 16:46:08 3,7,8 * * * * ?
info: 2022-11-17 16:46:10.5731138 +08:00 星期四 L MyJob[0] #8
job1 job1_trigger2 2022/11/17 16:46:10 5000ms
info: 2022-11-17 16:46:15.5841547 +08:00 星期四 L MyJob[0] #15
job1 job1_trigger2 2022/11/17 16:46:15 5000ms
info: 2022-11-17 16:46:20.5866898 +08:00 星期四 L MyJob[0] #8
job1 job1_trigger2 2022/11/17 16:46:20 5000ms

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-11-17 16:57:49.0898900 +08:00 星期四 L MyJob[0] #8
job1 job1_trigger1 2022/11/17 16:57:49 * * * * * *
warn: 2022-11-17 16:57:50.0322409 +08:00 星期四 L System.Logging.ScheduleService[0] #8
11/17/2022 16:57:50: The <job1_trigger1> trigger of job <job1> failed to execute as scheduled due to blocking.
warn: 2022-11-17 16:57:51.0099629 +08:00 星期四 L System.Logging.ScheduleService[0] #8
11/17/2022 16:57:51: The <job1_trigger1> trigger of job <job1> failed to execute as scheduled due to blocking.
info: 2022-11-17 16:57:52.0192847 +08:00 星期四 L MyJob[0] #8
job1 job1_trigger1 2022/11/17 16:57:52 * * * * * *
warn: 2022-11-17 16:57:53.0159256 +08:00 星期四 L System.Logging.ScheduleService[0] #8
11/17/2022 16:57:53: The <job1_trigger1> trigger of job <job1> failed to execute as scheduled due to blocking.
warn: 2022-11-17 16:57:54.0101172 +08:00 星期四 L System.Logging.ScheduleService[0] #8
11/17/2022 16:57:54: The <job1_trigger1> trigger of job <job1> failed to execute as scheduled due to blocking.
info: 2022-11-17 16:57:55.0038536 +08:00 星期四 L MyJob[0] #13
job1 job1_trigger1 2022/11/17 16:57:55 * * * * * *
warn: 2022-11-17 16:57:56.0158085 +08:00 星期四 L System.Logging.ScheduleService[0] #16
11/17/2022 16:57:56: The <job1_trigger1> trigger of job <job1> failed to execute as scheduled due to blocking.
warn: 2022-11-17 16:57:57.0276842 +08:00 星期四 L System.Logging.ScheduleService[0] #16
11/17/2022 16:57:57: The <job1_trigger1> trigger of job <job1> failed to execute as scheduled due to blocking.
info: 2022-11-17 16:57:58.0100972 +08:00 星期四 L MyJob[0] #13
job1 job1_trigger1 2022/11/17 16:57:58 * * * * * *
warn: 2022-11-17 16:57:59.0149137 +08:00 星期四 L System.Logging.ScheduleService[0] #13
11/17/2022 16:57:59: 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-11-17 17:02:28.0559602 +08:00 星期四 L MyJob[0] #5
job1 job1_trigger1 2022/11/17 17:02:28 * * * * * *
info: 2022-11-17 17:02:31.0183238 +08:00 星期四 L MyJob[0] #8
job1 job1_trigger1 2022/11/17 17:02:31 * * * * * *
info: 2022-11-17 17:02:34.0130555 +08:00 星期四 L MyJob[0] #13
job1 job1_trigger1 2022/11/17 17:02:34 * * * * * *
info: 2022-11-17 17:02:37.0040306 +08:00 星期四 L MyJob[0] #15
job1 job1_trigger1 2022/11/17 17:02:37 * * * * * *
info: 2022-11-17 17:02:39.0142346 +08:00 星期四 L MyJob[0] #15
job1 job1_trigger1 2022/11/17 17:02:39 * * * * * *

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-11-17 17:13:41.0480946 +08:00 星期四 L MyJob[0] #5
{
"jobDetail": {
"jobId": "job1",
"groupName": null,
"jobType": "MyJob",
"assemblyName": "ConsoleApp32",
"description": null,
"concurrent": false,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-11-17T17:13:41.0247430+08:00"
},
"trigger": {
"triggerId": "job1_trigger1",
"jobId": "job1",
"triggerType": "Furion.Schedule.CronTrigger",
"assemblyName": "Furion",
"args": "[\"@secondly\",0]",
"description": null,
"status": 2,
"startTime": null,
"endTime": null,
"lastRunTime": "2022-11-17T17:13:41.0000000",
"nextRunTime": "2022-11-17T17:13:42.0000000",
"numberOfRuns": 1,
"maxNumberOfRuns": 0,
"numberOfErrors": 0,
"maxNumberOfErrors": 0,
"numRetries": 0,
"retryTimeout": 1000,
"startNow": true,
"runOnStart": false,
"resetOnlyOnce": true,
"updatedTime": "2022-11-17T17:13:41.0250214+08:00"
}
}
  • 第二种:输出单独的作业 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)
{
var jobDetail = context.JobDetail;
var trigger = context.Trigger;

_logger.LogInformation(jobDetail.ConvertToJSON());
_logger.LogInformation(trigger.ConvertToJSON(NamingConventions.UnderScoreCase)); // 支持三种属性名输出规则

await Task.CompletedTask;
}
}

查看作业打印结果:

info: 2022-11-17 17:17:15.0441407 +08:00 星期四 L MyJob[0] #3
{
"jobId": "job1",
"groupName": null,
"jobType": "MyJob",
"assemblyName": "ConsoleApp32",
"description": null,
"concurrent": false,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-11-17T17:17:15.0103913+08:00"
}
info: 2022-11-17 17:17:15.0503546 +08:00 星期四 L MyJob[0] #3
{
"trigger_id": "job1_trigger1",
"job_id": "job1",
"trigger_type": "Furion.Schedule.CronTrigger",
"assembly_name": "Furion",
"args": "[\"@secondly\",0]",
"description": null,
"status": 2,
"start_time": null,
"end_time": null,
"last_run_time": "2022-11-17T17:17:15.0000000",
"next_run_time": "2022-11-17T17:17:16.0000000",
"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,
"updated_time": "2022-11-17T17:17:15.0109612+08:00"
}
  • 第三种:输出单独的作业 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-11-17 17:35:11.1085426 +08:00 星期四 L MyJob[0] #9
INSERT INTO 作业信息表名(
[jobId],
[groupName],
[jobType],
[assemblyName],
[description],
[concurrent],
[includeAnnotations],
[properties],
[updatedTime]
)
VALUES(
'job1',
NULL,
'MyJob',
'ConsoleApp32',
NULL,
0,
0,
'{}',
'2022/11/17 17:35:11'
);
info: 2022-11-17 17:35:11.1150444 +08:00 星期四 L MyJob[0] #9
DELETE FROM 作业触发器表名
WHERE [TriggerId] = 'job1_trigger1' AND [JobId] = 'job1';
info: 2022-11-17 17:35:11.1190961 +08:00 星期四 L MyJob[0] #9
UPDATE 作业触发器表名
SET
[trigger_id] = 'job1_trigger1',
[job_id] = 'job1',
[trigger_type] = 'Furion.Schedule.CronTrigger',
[assembly_name] = 'Furion',
[args] = '["@secondly",0]',
[description] = NULL,
[status] = 2,
[start_time] = NULL,
[end_time] = NULL,
[last_run_time] = '2022/11/17 17:35:11',
[next_run_time] = '2022/11/17 17:35:12',
[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,
[updated_time] = '2022/11/17 17:35:11'
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)
{
var jobDetail = context.JobDetail;
var trigger = context.Trigger;

_logger.LogInformation(jobDetail.ConvertToMonitor());
_logger.LogInformation(trigger.ConvertToMonitor());

await Task.CompletedTask;
}
}

查看作业打印结果:

info: 2022-11-17 17:39:09.1086517 +08:00 星期四 L MyJob[0] #3
┏━━━━━━━━━━━ JobDetail ━━━━━━━━━━━
┣ MyJob

┣ JobId: job1
┣ GroupName:
┣ JobType: MyJob
┣ AssemblyName: ConsoleApp32
┣ Description:
┣ Concurrent: False
┣ IncludeAnnotations: False
┣ Properties: {}
┣ UpdatedTime: 2022/11/17 17:39:09
┗━━━━━━━━━━━ JobDetail ━━━━━━━━━━━
info: 2022-11-17 17:39:09.1133162 +08:00 星期四 L MyJob[0] #3
┏━━━━━━━━━━━ Trigger ━━━━━━━━━━━
┣ Furion.Schedule.CronTrigger

┣ TriggerId: job1_trigger1
┣ JobId: job1
┣ TriggerType: Furion.Schedule.CronTrigger
┣ AssemblyName: Furion
┣ Args: ["@secondly",0]
┣ Description:
┣ Status: Running
┣ StartTime:
┣ EndTime:
┣ LastRunTime: 2022/11/17 17:39:09
┣ NextRunTime: 2022/11/17 17:39:10
┣ NumberOfRuns: 1
┣ MaxNumberOfRuns: 0
┣ NumberOfErrors: 0
┣ MaxNumberOfErrors: 0
┣ NumRetries: 0
┣ RetryTimeout: 1000
┣ StartNow: True
┣ RunOnStart: False
┣ ResetOnlyOnce: True
┣ UpdatedTime: 2022/11/17 17:39:09
┗━━━━━━━━━━━ Trigger ━━━━━━━━━━━

26.1.2.5 运行时(动态)操作作业

有时候,我们需要在运行时对作业动态的增加,更新,删除等操作,如动态添加作业:

  1. 注册 services.AddSchedule() 服务
// 可以完全动态操作,只需要注册服务即可
services.AddSchedule();

// 也可以部分静态,部分动态注册
services.AddSchedule(options =>
{
options.AddJob<MyJob>(concurrent: false, Triggers.PeriodSeconds(5));
});
  1. 注入 ISchedulerFactory 服务
public class YourService: IYourService
{
private readonly ISchedulerFactory _schedulerFactory;
public YourService(ISchedulerFactory schedulerFactory)
{
_schedulerFactory = schedulerFactory;
}

public void AddJob()
{
_schedulerFactory.AddJob<MyJob>("动态作业 Id", Triggers.Secondly());
}
}
  1. 查看作业执行结果
warn: 2022-11-17 17:54:35.2654513 +08:00 星期四 L System.Logging.ScheduleService[0] #5
Schedule Hosted Service cancels hibernation.
info: 2022-11-17 17:54:35.2670018 +08:00 星期四 L System.Logging.ScheduleService[0] #5
The Scheduler of <动态作业 Id> successfully added to the schedule.
info: 2022-11-17 17:54:36.0834925 +08:00 星期四 L MyJob[0] #5
job1 job1_trigger1 2022/11/17 17:54:36 5000ms
info: 2022-11-17 17:54:36.0911692 +08:00 星期四 L MyJob[0] #3
动态作业 Id 动态作业 Id_trigger1 2022/11/17 17:54:36 * * * * * *
info: 2022-11-17 17:54:37.0146251 +08:00 星期四 L MyJob[0] #18
动态作业 Id 动态作业 Id_trigger1 2022/11/17 17:54:37 * * * * * *
info: 2022-11-17 17:54:38.0071504 +08:00 星期四 L MyJob[0] #16
动态作业 Id 动态作业 Id_trigger1 2022/11/17 17:54:38 * * * * * *
info: 2022-11-17 17:54:39.0140840 +08:00 星期四 L MyJob[0] #17
动态作业 Id 动态作业 Id_trigger1 2022/11/17 17:54:39 * * * * * *
info: 2022-11-17 17:54:40.0173240 +08:00 星期四 L MyJob[0] #16
动态作业 Id 动态作业 Id_trigger1 2022/11/17 17:54:40 * * * * * *
info: 2022-11-17 17:54:41.0249043 +08:00 星期四 L MyJob[0] #16
动态作业 Id 动态作业 Id_trigger1 2022/11/17 17:54:41 * * * * * *
info: 2022-11-17 17:54:41.0550205 +08:00 星期四 L MyJob[0] #15
job1 job1_trigger1 2022/11/17 17:54:41 5000ms
info: 2022-11-17 17:54:42.0171271 +08:00 星期四 L MyJob[0] #15
动态作业 Id 动态作业 Id_trigger1 2022/11/17 17:54:42 * * * * * *
info: 2022-11-17 17:54:43.0288486 +08:00 星期四 L MyJob[0] #18
动态作业 Id 动态作业 Id_trigger1 2022/11/17 17:54:43 * * * * * *
info: 2022-11-17 17:54:44.0092455 +08:00 星期四 L MyJob[0] #15
动态作业 Id 动态作业 Id_trigger1 2022/11/17 17:54:44 * * * * * *

26.1.2.6 作业触发器特性

默认情况下,框架不会扫描 IJob 实现类的作业触发器特性,但可以设置作业的 IncludeAnnotations 进行启用。

  1. 启用 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().ToArray())
});
  1. 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.JobId} {context.TriggerId} {context.OccurrenceTime} {context.Trigger}");

await Task.CompletedTask;
}
}
  1. 查看作业执行结果
info: 2022-11-18 10:28:56.3382585 +08:00 星期五 L MyJob[0] #14
job1 job1_trigger1 2022/11/18 10:28:56 5000ms
info: 2022-11-18 10:29:00.0219493 +08:00 星期五 L MyJob[0] #5
job1 job1_trigger2 2022/11/18 10:29:00 * * * * *
info: 2022-11-18 10:29:01.3318716 +08:00 星期五 L MyJob[0] #14
job1 job1_trigger1 2022/11/18 10:29:01 5000ms
info: 2022-11-18 10:29:03.0127992 +08:00 星期五 L MyJob[0] #16
job1 job1_trigger3 2022/11/18 10:29:03 3,7,8 * * * * ?
info: 2022-11-18 10:29:06.3457728 +08:00 星期五 L MyJob[0] #16
job1 job1_trigger1 2022/11/18 10:29:06 5000ms
info: 2022-11-18 10:29:07.0318919 +08:00 星期五 L MyJob[0] #14
job1 job1_trigger3 2022/11/18 10:29:07 3,7,8 * * * * ?
info: 2022-11-18 10:29:08.0141479 +08:00 星期五 L MyJob[0] #8
job1 job1_trigger3 2022/11/18 10:29:08 3,7,8 * * * * ?
info: 2022-11-18 10:29:11.3468100 +08:00 星期五 L MyJob[0] #16
job1 job1_trigger1 2022/11/18 10:29:11 5000ms
info: 2022-11-18 10:29:16.3504029 +08:00 星期五 L MyJob[0] #14
job1 job1_trigger1 2022/11/18 10:29:16 5000ms

26.1.3 作业信息 JobDetail 及构建器

26.1.3.1 关于作业信息

框架提供了 JobDetail 类型来描述作业信息,JobDetail 类型提供以下只读属性

属性名属性类型默认值说明
JobIdstring作业 Id
GroupNamestring作业组名称
JobTypestring作业处理程序类型,存储的是类型的 FullName
AssemblyNamestring作业处理程序类型所在程序集,存储的是程序集 Name
Descriptionstring描述信息
Concurrentbooltrue作业执行方式,如果设置为 false,那么使用 串行 执行,否则 并行 执行
IncludeAnnotationsboolfalse是否扫描 IJob 实现类 [Trigger] 特性触发器
Propertiesstring"{}"作业信息额外数据,由 Dictionary<string, object> 序列化成字符串存储
UpdatedTimeDateTime?作业更新时间

26.1.3.2 关于作业信息构建器

作业信息 JobDetail 是作业调度模块提供运行时的只读类型,那么我们该如何创建或变更 JobDetail 对象呢?

JobBuilder 是作业调度模块提供可用来生成运行时 JobDetail 的类型,这样做的好处可避免外部直接修改运行时 JobDetail 数据,还能实现任何修改动作监听,也能避免多线程抢占情况。

作业调度模块提供了多种方式用来创建 JobBuilder 对象。

  1. 通过 Create 静态方法创建
// 根据 IJob 实现类类型创建
var jobBuilder = JobBuilder.Create<MyJob>();

// 根据 Type 类型创建
var jobBuilder = JobBuilder.Create(typeof(MyJob));

// 根据程序集名称和类型完全限定名(FullName)创建
var jobBuilder = JobBuilder.Create("YourProject", "YourProject.MyJob");

// 通过委托创造动态作业
var jobBuilder = JoBuilder.Create((s, c, t) =>
{
// ....
});
  1. 通过 JobDetail 类型创建

这种方式常用于在运行时更新作业信息。

var jobBuilder = JobBuilder.From(jobDetail);

//也可以通过以下方式
var jobBuilder = jobDetail.GetBuilder();
  1. 通过 JSON 字符串创建

该方式非常灵活,可从配置文件,JSON 字符串,或其他能够返回 JSON 字符串的地方创建。

var jobBuilder = JobBuilder.From(@"
{
""jobId"": ""job1"",
""groupName"": null,
""jobType"": ""Furion.Application.MyJob"",
""assemblyName"": ""Furion.Application"",
""description"": null,
""concurrent"": true,
""includeAnnotations"": false,
""properties"": ""{}"",
""updatedTime"": ""2022-11-17T09:25:47.0471107+08:00""
}");

如果使用的是 .NET7,可使用 """ 避免转义,如:

var jobBuilder = JobBuilder.From("""
{
"jobId": "job1",
"groupName": null,
"jobType": "Furion.Application.MyJob",
"assemblyName": "Furion.Application",
"description": null,
"concurrent": true,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-11-17T09:25:47.0471107+08:00"
}
""");
关于属性名匹配规则

支持 CamelCase(驼峰命名法)Pascal(帕斯卡命名法) 命名方式。

不支持 UnderScoreCase(下划线命名法) ,如 "include_annotations": true

  1. 还可以通过 Clone 静态方法从一个 JobBuilder 创建
var jobBuilder = JobBuilder.Clone(fromJobBuilder);
克隆说明

克隆操作只会克隆 AssemblyNameJobTypeGroupNameDescriptionConcurrentIncludeAnnotationsProperties

不会克隆 JobIdUpdatedTime

  1. 还可以通过 LoadFrom 实例方法填充当前的 JobBuilder

比如可以传递匿名类型,类类型:

// 会覆盖所有相同的值
jobBuilder.LoadFrom(new
{
Description = "我是描述",
Concurrent = false
});

// 支持多个填充,还可以配置跳过 null 值覆盖
jobBuilder.LoadFrom(new
{
Description = "我是另外一个描述",
Concurrent = false,
IncludeAnnotations = default(object) // 会跳过赋值
}, ignoreNullValue: true);
关于属性名匹配规则

支持 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" } }) // 作业类型额外数据,支持多个重载,推荐!!!
.SetDynamicHandler((s, c, t) => {
s.Logger().LogInformation($"{c}");
return Task.CompletedTask;
}) // 动态委托处理程序
;

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($"{context.JobId} {context.TriggerId} {context.OccurrenceTime} {context.Trigger} {count}");

await Task.CompletedTask;
}
}

查看作业运行日志:

info: 2022-11-18 16:48:35.8308170 +08:00 星期五 L ConsoleApp32.MyJob[0] #5
job1 job1_trigger1 2022/11/18 16:48:35 5000ms 0
info: 2022-11-18 16:48:40.8437231 +08:00 星期五 L ConsoleApp32.MyJob[0] #8
job1 job1_trigger1 2022/11/18 16:48:40 5000ms 1
info: 2022-11-18 16:48:45.8471287 +08:00 星期五 L ConsoleApp32.MyJob[0] #15
job1 job1_trigger1 2022/11/18 16:48:45 5000ms 2
info: 2022-11-18 16:48:50.8607141 +08:00 星期五 L ConsoleApp32.MyJob[0] #15
job1 job1_trigger1 2022/11/18 16:48:50 5000ms 3
info: 2022-11-18 16:48:55.8645520 +08:00 星期五 L ConsoleApp32.MyJob[0] #14
job1 job1_trigger1 2022/11/18 16:48:55 5000ms 4

作业调度模块为 JobDetailJobBuilder 提供了多个方法操作额外数据:

// 查看所有额外数据
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);

// 删除某个额外数据,支持链式操作,如果 key 不存在则跳过
jobDetail.RemoveProperty("key").RemoveProperty("key1");

// 清空所有额外数据
jobDetail.ClearProperties();
作业额外数据类型支持

作业额外数据每一项的值只支持 int32stringboolnull 或它们组成的数组类型。

26.1.3.5 作业信息特性

作业信息特性 [JobDetail] 是为了方便运行时或启动时快速创建作业计划构建器而提供的,可在启动时或运行时通过以下方式创建,如:

[JobDetail("job1")]
[PeriodSeconds(5, TriggerId = "trigger1")]
public class MyJob : IJob
{
}
  • 手动扫描并创建作业计划构建器
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", Concurrent = false, Description = "这是一段描述")]
public class MyJob : IJob
{

26.1.3.6 多种格式字符串输出

JobDetailJobBuilder 都提供了多种将自身转换成特定格式的字符串。

  1. 转换成 JSON 字符串
var json = jobDetail.ConvertToJSON();

字符串打印如下:

{
"jobId": "job1",
"groupName": null,
"jobType": "Furion.Application.MyJob",
"assemblyName": "Furion.Application",
"description": null,
"concurrent": true,
"includeAnnotations": false,
"properties": "{}",
"updatedTime": "2022-11-18T22:56:47.4149299+08:00"
}
  1. 转换成 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,
'ConsoleApp13.MyJob',
'ConsoleApp13',
NULL,
1,
0,
'{}',
'2022/11/18 23:16:18'
);

-- 删除语句
DELETE FROM tbName
WHERE [JobId] = 'job1';

-- 更新语句
UPDATE tbName
SET
[job_id] = 'job1',
[group_name] = NULL,
[job_type] = 'ConsoleApp13.MyJob',
[assembly_name] = 'ConsoleApp13',
[description] = NULL,
[concurrent] = 1,
[include_annotations] = 0,
[properties] = '{}',
[updated_time] = '2022/11/18 23:16:18'
WHERE [job_id] = 'job1';
  1. 转换成 Monitor 字符串
var monitor = jobDetail.ConvertToMonitor();

字符串打印如下:

┏━━━━━━━━━━━  JobDetail ━━━━━━━━━━━
┣ ConsoleApp13.MyJob

┣ jobId: job1
┣ groupName:
┣ jobType: Furion.Application.MyJob
┣ assemblyName: Furion.Application
┣ description:
┣ concurrent: True
┣ includeAnnotations: False
┣ properties: {}
┣ updatedTime: 2022/11/18 23:26:47
┗━━━━━━━━━━━ JobDetail ━━━━━━━━━━━
  1. 简要字符串输出
var str = jobDetail.ToString();
<job1> 这是一段描述

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(下划线命名法)
注意事项

如果在该委托方法中调用 jobDetail.ConvertToSQL(..) 方法会导致死循环。

26.1.4 作业处理程序 IJob

作业处理程序是作业具体执行的业务逻辑代码,通常由程序员编写,作业处理程序必须实现 IJob 接口。

26.1.4.1 如何定义

public class MyJob : IJob
{
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
// your code...
}
}

26.1.4.2 依赖注入

实现 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.JobId} {context.TriggerId} {context.OccurrenceTime} {context.Trigger} {count}");

await Task.CompletedTask;
}
}
  • 如果是非单例的接口,如瞬时或范围服务,可通过 IServiceProvder 创建
public class MyJob : IJob
{
private readonly ILogger<MyJob> _logger;
private readonly IConfiguration _configuration;
private readonly IServiceProvider _serviceProvider;

public MyJob(ILogger<MyJob> logger
, IConfiguration configuration
, IServiceProvider serviceProvider)
{
_logger = logger;
_configuration = configuration;
_serviceProvider = serviceProvider;
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
using var serviceScope = _serviceProvider.CreateScope();
var repository = serviceScope.ServiceProvider.GetService<IRepository<User>>();

_logger.LogInformation($"{context.JobId} {context.TriggerId} {context.OccurrenceTime} {context.Trigger}");

await Task.CompletedTask;
}
}
  • 针对高频定时任务,比如每秒执行一次,或者更频繁的任务

为了避免频繁创建作用域和销毁作用域,可创建长范围的作用域。

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
, IServiceProvider serviceProvider)
{
_logger = logger;
_configuration = configuration;
_serviceScope = serviceProvider.CreateScope();
}

public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
var repository = _serviceScope.ServiceProvider.GetService<IRepository<User>>();

_logger.LogInformation($"{context.JobId} {context.TriggerId} {context.OccurrenceTime} {context.Trigger}");

await Task.CompletedTask;
}

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

26.1.4.3 JobExecutingContext 上下文

JobExecutingContext 上下文作为 ExecuteAsync 方法的第一个参数,提供了以下几个运行时信息:

  • JobExecutingContext 属性列表
    • JobId:作业 Id
    • TriggerId:当前触发器 Id
    • JobDetail:作业信息
    • Trigger:作业触发器
    • OccurrenceTime:调度器检查时间,最准确的记录时间
    • ExecutingTime:实际执行时间(可能存在误差)
  • JobExecutingContext 方法列表
    • .ConvertToJSON(naming):将作业计划转换成 JSON 字符串
    • .ToString():输出为字符串

26.1.4.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)
{
// 创建任务关联取消 Token
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);

try
{
// 传递给异步服务
await todo.SomeMethodAsync(cancellationTokenSource.Token);
}
catch (Exception ex)
{
// ...
}

_logger.LogInformation($"{context.JobId} {context.TriggerId} {context.OccurrenceTime} {context.Trigger}");
}
}

这样当作业被取消时,SomeMethodAsync 也会同步取消。

26.1.4.5 使用 Roslyn/Natasha 动态创建

按照程序开发的正常思维,理应先在代码中创建作业处理程序类型,但我们可以借助 Roslyn 动态编译 C# 代码。

  1. 编辑启动项目 .csproj 文件,添加 <PreserveCompilationContext>true</PreserveCompilationContext>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
  1. 安装 DotNetCore.Natasha.CSharp 拓展包

DotNetCore.Natasha.CSharp 是国人基于 Roslyn 开发的非常强大的动态编译 C# 库。

dotnet add package DotNetCore.Natasha.CSharp
  1. 根据字符串创建 IJob 类型
// 初始化
NatashaInitializer.Preheating();
// 创建程序集(可自定义编写程序集名称)
var oop = new AssemblyCSharpBuilder("JobAssembly");
oop.Domain = DomainManagement.Random();

// 添加代码
oop.Add(@"
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($""{context.JobDetail} {context.Trigger} {context.OccurrenceTime} {context.JobDetail.AssemblyName} {context.JobDetail.JobType}"");
await Task.CompletedTask;
}
}
");

// 生成运行时 MyJob 类型
var jobType = oop.GetTypeFromShortName("MyJob");
  1. 注册作业
// 可以在启动的时候添加
services.AddSchedule(options =>
{
options.AddJob(JobBuilder.Create(jobType)
, Triggers.PeriodSeconds(5));
});

// 也可以完全在运行时添加(常用)
_schedulerFactory.AddJob(JobBuilder.Create(jobType)
, Triggers.PeriodSeconds(5));

查看作业执行日志:

info: 2022-11-25 13:45:49.0150463 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule Hosted Service is running.
info: 2022-11-25 13:45:49.0208313 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule Hosted Service is preloading.
info: 2022-11-25 13:45:49.0406977 +08:00 星期五 L System.Logging.ScheduleService[0] #1
The Scheduler of <job1> successfully added to the schedule.
warn: 2022-11-25 13:45:49.0803822 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule Hosted Service cancels hibernation and GC.Collect().
info: 2022-11-25 13:45:49.0969060 +08:00 星期五 L System.Logging.ScheduleService[0] #1
Schedule Hosted Service preload completed, and a total of <1> schedulers are added.
info: 2022-11-25 13:45:54.0695285 +08:00 星期五 L YourProject.MyJob[0] #13
<job1> <job1 job1_trigger1> 5000ms 2022/11/25 13:45:54 JobAssembly YourProject.MyJob
info: 2022-11-25 13:45:59.0700268 +08:00 星期五 L YourProject.MyJob[0] #12
<job1> <job1 job1_trigger1> 5000ms 2022/11/25 13:45:59 JobAssembly YourProject.MyJob
info: 2022-11-25 13:46:04.0854646 +08:00 星期五 L YourProject.MyJob[0] #11
<job1> <job1 job1_trigger1> 5000ms 2022/11/25 13:46:04 JobAssembly YourProject.MyJob
info: 2022-11-25 13:46:09.0924123 +08:00 星期五 L YourProject.MyJob[0] #9
<job1> <job1 job1_trigger1> 5000ms 2022/11/25 13:46:09 JobAssembly YourProject.MyJob

惊不惊喜,意外意外~

小知识

通过 Roslyn 的方式支持创建 IJobJobDetailTriggerScheduler 哦,自行测试。😊

26.1.5 作业触发器 Trigger 及构建器

26.1.5.1 关于作业触发器

框架提供了 Trigger 类型来描述作业具体的触发时间,Trigger 类型提供以下只读属性

属性名属性类型默认值说明
TriggerIdstring作业触发器 Id
JobIdstring作业 Id
TriggerTypestring作业触发器类型,存储的是类型的 FullName
AssemblyNamestring作业触发器类型所在程序集,存储的是程序集 Name
Argsstring作业触发器参数,运行时将反序列化为 object[] 类型并作为构造函数参数
Descriptionstring描述信息
StatusTriggerStatusReady作业触发器状态
StartTimeDateTime?起始时间
EndTimeDateTime?结束时间
LastRunTimeDateTime?最近运行时间
NextRunTimeDateTime?下一次运行时间
NumberOfRunslong0触发次数
MaxNumberOfRunslong0最大触发次数,0:不限制,n:N 次
NumberOfErrorslong0出错次数
MaxNumberOfErrorslong0最大出错次数,0:不限制,n:N 次
NumRetriesint0重试次数
RetryTimeoutint1000重试间隔时间,毫秒单位
StartNowbooltrue是否立即启动
RunOnStartboolfalse是否启动时执行一次
ResetOnlyOncebooltrue是否在启动时重置最大触发次数等于一次的作业
UpdatedTimeDateTime?作业触发器更新时间

26.1.5.2 作业触发器状态

作业触发器状态指示了当前作业触发器的状态,使用 TriggerStatus 枚举类型(uint),该类型包含以下枚举成员。

枚举名枚举值说明
Backlog0积压,起始时间大于当前时间
Ready1就绪
Running2正在运行
Pause3暂停
Blocked4阻塞,本该执行但是没有执行
ErrorToReady5由失败进入就绪,运行错误当并未超出最大错误数,进入下一轮就绪
Archived6归档,结束时间小于当前时间
Panic7崩溃,错误次数超出了最大错误数
Overrun8超限,运行次数超出了最大限制
Unoccupied9无触发时间,下一次执行时间为 null
NotStart10未启动
Unknown11未知作业触发器,作业触发器运行时类型为 null
Unhandled12未知作业处理程序,作业处理程序类型运行时类型为 null

26.1.5.3 关于作业触发器构建器

作业触发器 Trigger 是作业调度模块提供运行时的只读类型,那么我们该如何创建或变更 Trigger 对象呢?

TriggerBuilder 是作业调度模块提供可用来生成运行时 Trigger 的类型,这样做的好处可避免外部直接修改运行时 Trigger 数据,还能实现任何修改动作监听,也能避免多线程抢占情况。

作业调度模块提供了多种方式用来创建 TriggerBuilder 对象。

  1. 通过 Create 静态方法创建
// 根据 Trigger 派生类类型创建
var triggerBuilder = TriggerBuilder.Create<PeriodTrigger>();

// 根据 Type 类型创建
var triggerBuilder = TriggerBuilder.Create(typeof(PeriodTrigger));

// 根据程序集名称和类型完全限定名(FullName)创建
var triggerBuilder = TriggerBuilder.Create("Furion", "Furion.Schedule.PeriodTrigger");
  1. 通过 Trigger 类型创建

这种方式常用于在运行时更新作业触发器。

var triggerBuilder = TriggerBuilder.From(trigger);

//也可以通过以下方式
var triggerBuilder = trigger.GetBuilder();
  1. 通过 JSON 字符串创建

该方式非常灵活,可从配置文件,JSON 字符串,或其他能够返回 JSON 字符串的地方创建。

var triggerBuilder = TriggerBuilder.From(@"
{
""triggerId"": ""job1_trigger1"",
""triggerType"": ""Furion.Schedule.PeriodSecondsTrigger"",
""assemblyName"": ""Furion"",
""args"": ""[5]"",
""description"": null,
""status"": 2,
""startTime"": null,
""endTime"": null,
""lastRunTime"": ""2022-11-20T18:31:56.6859410+08:00"",
""nextRunTime"": ""2022-11-20T18:32:01.7233546+08:00"",
""numberOfRuns"": 1,
""maxNumberOfRuns"": 0,
""numberOfErrors"": 0,
""maxNumberOfErrors"": 0,
""numRetries"": 0,
""retryTimeout"": 1000,
""startNow"": true,
""runOnStart"": false,
""resetOnlyOnce"": true,
""updatedTime"": ""2022-11-20T18:31:56.7233630+08:00""
}");

如果使用的是 .NET7,可使用 """ 避免转义,如:

var triggerBuilder = TriggerBuilder.From("""
{
"triggerId": "job1_trigger1",
"triggerType": "Furion.Schedule.PeriodSecondsTrigger",
"assemblyName": "Furion",
"args": "[5]",
"description": null,
"status": 2,
"startTime": null,
"endTime": null,
"lastRunTime": "2022-11-20T18:31:56.6859410+08:00",
"nextRunTime": "2022-11-20T18:32:01.7233546+08:00",
"numberOfRuns": 1,
"maxNumberOfRuns": 0,
"numberOfErrors": 0,
"maxNumberOfErrors": 0,
"numRetries": 0,
"retryTimeout": 1000,
"startNow": true,
"runOnStart": false,
"resetOnlyOnce": true,
"updatedTime": "2022-11-20T18:31:56.7233630+08:00"
}
""");
关于属性名匹配规则

支持 CamelCase(驼峰命名法)Pascal(帕斯卡命名法) 命名方式。

不支持 UnderScoreCase(下划线命名法) ,如 "include_annotations": true

  1. 还可以通过 Clone 静态方法从一个 TriggerBuilder 创建
var triggerBuilder = TriggerBuilder.Clone(fromTriggerBuilder);
克隆说明

克隆操作只会克隆 AssemblyNameTriggerTypeArgsDescriptionStartTimeEndTimeMaxNumberOfRunsMaxNumberOfErrorsNumRetriesRetryTimeoutStartNowRunOnStartResetOnlyOnce

不会克隆 TriggerIdJobIdStatusLastRunTimeNextRunTimeNumberOfRunsNumberOfErrorsUpdatedTime

  1. 还可以通过 LoadFrom 实例方法填充当前的 TriggerBuilder

比如可以传递匿名类型,类类型:

// 会覆盖所有相同的值
triggerBuilder.LoadFrom(new
{
Description = "我是描述",
StartTime = DateTime.Now
});

// 支持多个填充,还可以配置跳过 null 值覆盖
triggerBuilder.LoadFrom(new
{
Description = "我是另外一个描述",
StartTime = DateTime.Now,
LastRunTime = default(DateTime?) // 会跳过赋值
}, ignoreNullValue: true);
关于属性名匹配规则

支持 CamelCase(驼峰命名法)Pascal(帕斯卡命名法)UnderScoreCase(下划线命名法) 命名方式。

26.1.5.4 内置作业触发器构建器

为了方便快速实现作业触发器,作业调度模块内置了 Period(间隔)Cron(表达式) 作业触发器,可通过 TriggerBuilder 类型或 Triggers 静态类创建。

  • TriggerBuilder 方式
// 创建毫秒周期(间隔)作业触发器构建器
var triggerBuilder = TriggerBuilder.Period(5000);

// 创建秒周期(间隔)作业触发器构建器
var triggerBuilder = TriggerBuilder.PeriodSeconds(5);

// 创建分钟周期(间隔)作业触发器构建器
var triggerBuilder = TriggerBuilder.PeriodMinutes(5);

// 创建 Cron 表达式作业触发器构建器
var triggerBuilder = TriggerBuilder.Cron("* * * * *", CronStringFormat.Default);

// 创建 Cron 表达式 Macro 作业触发器构建器
var triggerBuilder = TriggerBuilder.MacroAt("@secondly", 1, 3, 5);
  • Triggers 方式,推荐

Triggers 具备 TriggerBuilder 所有的静态方法,另外还添加了不少更加便捷的静态方法。

// 创建毫秒周期(间隔)作业触发器构建器
var triggerBuilder = Triggers.Period(5000);
// 创建秒周期(间隔)作业触发器构建器
var triggerBuilder = Triggers.PeriodSeconds(5);
// 创建分钟周期(间隔)作业触发器构建器
var triggerBuilder = Triggers.PeriodMinutes(5);
// 创建 Cron 表达式作业触发器构建器
var triggerBuilder = Triggers.Cron("* * * * *", CronStringFormat.Default);
// 创建 Cron 表达式 Macro 作业触发器构建器
var triggerBuilder = Triggers.MacroAt("@secondly", 1, 3, 5);
// 创建每秒开始作业触发器构建器
var triggerBuilder = Triggers.Secondly();
// 创建每分钟开始作业触发器构建器
var triggerBuilder = Triggers.Minutely();
// 创建每小时开始作业触发器构建器
var triggerBuilder = Triggers.Hourly();
// 创建每天(午夜)开始作业触发器构建器
var triggerBuilder = Triggers.Daily();
// 创建每月1号(午夜)开始作业触发器构建器
var triggerBuilder = Triggers.Monthly();
// 创建每周日(午夜)开始作业触发器构建器
var triggerBuilder = Triggers.Weekly();
// 创建每年1月1号(午夜)开始作业触发器构建器
var triggerBuilder = Triggers.Yearly();
// 创建每周一至周五(午夜)开始作业触发器构建器
var triggerBuilder = Triggers.Workday();

// Furion 4.8.2.5+ 更多 Macro At
// 每第 3 秒
var triggerBuilder = Triggers.SecondlyAt(3);
// 每第 3,5,6 秒
var triggerBuilder = Triggers.SecondlyAt(3, 5, 6);

// 每分钟第 3 秒
var triggerBuilder = Triggers.MinutelyAt(3);
// 每分钟第 3,5,6 秒
var triggerBuilder = Triggers.MinutelyAt(3, 5, 6);

// 每小时第 3 分钟
var triggerBuilder = Triggers.HourlyAt(3);
// 每小时第 3,5,6 分钟
var triggerBuilder = Triggers.HourlyAt(3, 5, 6);

// 每天第 3 小时正(点)
var triggerBuilder = Triggers.DailyAt(3);
// 每天第 3,5,6 小时正(点)
var triggerBuilder = Triggers.DailyAt(3, 5, 6);

// 每月第 3 天零点正
var triggerBuilder = Triggers.MonthlyAt(3);
// 每月第 3,5,6 天零点正
var triggerBuilder = Triggers.MonthlyAt(3, 5, 6);

// 每周星期 3 零点正
var triggerBuilder = Triggers.WeeklyAt(3);
var triggerBuilder = Triggers.WeeklyAt("WED"); // SUN(星期天),MON,TUE,WED,THU,FRI,SAT
// 每周星期 3,5,6 零点正
var triggerBuilder = Triggers.WeeklyAt(3, 5, 6);
var triggerBuilder = Triggers.WeeklyAt("WED", "FRI", "SAT");
// 还支持混合
var triggerBuilder = Triggers.WeeklyAt(3, "FRI", 6);

// 每年第 3 月 1 日零点正
var triggerBuilder = Triggers.YearlyAt(3);
var triggerBuilder = Triggers.YearlyAt("MAR"); // JAN(一月),FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC
// 每年第 3,5,6 月 1 日零点正
var triggerBuilder = Triggers.YearlyAt(3);
var triggerBuilder = Triggers.YearlyAt(3, 5, 6);
var triggerBuilder = Triggers.YearlyAt("MAR", "MAY", "JUN");
// 还支持混合
var triggerBuilder = Triggers.YearlyAt(3, "MAY", 6);

26.1.5.5 自定义作业触发器

除了使用作业调度模块提供了 PeriodTriggerCronTrigger 以外,可自定义作业触发器,只需要继承 Trigger 并重写 GetNextOccurrence 方法即可,如实现一个间隔两秒的作业触发器。

public class TwiceSecondTrigger : Trigger
{
public override DateTime GetNextOccurrence(DateTime startAt)
{
return startAt.AddSeconds(2);
}
}

之后可通过 Triggers.CreateTriggers.Create 创建即可:

services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Create<TwiceSecondTrigger>());
});

查看作业执行结果:

info: 2022-11-20 21:13:02.4726416 +08:00 星期日 L ConsoleApp13.MyJob[0] #9
job1 job1_trigger1 2022/11/20 21:13:02 ConsoleApp13.TwiceSecondTrigger
info: 2022-11-20 21:13:04.4591328 +08:00 星期日 L ConsoleApp13.MyJob[0] #6
job1 job1_trigger1 2022/11/20 21:13:04 ConsoleApp13.TwiceSecondTrigger
info: 2022-11-20 21:13:06.4677716 +08:00 星期日 L ConsoleApp13.MyJob[0] #4
job1 job1_trigger1 2022/11/20 21:13:06 ConsoleApp13.TwiceSecondTrigger
info: 2022-11-20 21:13:08.4726987 +08:00 星期日 L ConsoleApp13.MyJob[0] #14
job1 job1_trigger1 2022/11/20 21:13:08 ConsoleApp13.TwiceSecondTrigger
info: 2022-11-20 21:13:10.4827028 +08:00 星期日 L ConsoleApp13.MyJob[0] #9
job1 job1_trigger1 2022/11/20 21:13:10 ConsoleApp13.TwiceSecondTrigger
info: 2022-11-20 21:13:12.4936247 +08:00 星期日 L ConsoleApp13.MyJob[0] #14
job1 job1_trigger1 2022/11/20 21:13:12 ConsoleApp13.TwiceSecondTrigger

另外,自定义作业触发器还支持传递参数:

参数特别说明

如果自定义作业触发器包含参数,那么必须满足以下两个条件

  • 参数必须通过唯一的构造函数传入,有且最多只能拥有一个构造函数
  • 参数的类型只能是 intstringboolnull 或由它们组成的数组类型
public class SomeSecondTrigger : Trigger
{
public SomeSecondTrigger(int seconds) // 支持多个参数
{
Seconds = seconds;
}

private int Seconds { get; }

public override DateTime GetNextOccurrence(DateTime startAt)
{
return startAt.AddSeconds(Seconds);
}
}

之后可通过 Triggers.CreateTriggers.Create 创建并传入参数。

services.AddSchedule(options =>
{
options.AddJob<MyJob>(Triggers.Create<SomeSecondTrigger>(3)); // 3 秒执行一次
});

查看作业执行结果:

info: 2022-11-20 21:33:46.3074692 +08:00 星期日 L ConsoleApp13.MyJob[0] #4
job1 job1_trigger1 2022/11/20 21:33:46 ConsoleApp13.SomeSecondTrigger
info: 2022-11-20 21:33:49.3101667 +08:00 星期日 L ConsoleApp13.MyJob[0] #6
job1 job1_trigger1 2022/11/20 21:33:49 ConsoleApp13.SomeSecondTrigger
info: 2022-11-20 21:33:52.3222046 +08:00 星期日 L ConsoleApp13.MyJob[0] #8
job1 job1_trigger1 2022/11/20 21:33:52 ConsoleApp13.SomeSecondTrigger
info: 2022-11-20 21:33:55.3270737 +08:00 星期日 L ConsoleApp13.MyJob[0] #4
job1 job1_trigger1 2022/11/20 21:33:55 ConsoleApp13.SomeSecondTrigger
info: 2022-11-20 21:33:58.3293727 +08:00 星期日 L ConsoleApp13.MyJob[0] #6
job1 job1_trigger1 2022/11/20 21:33:58 ConsoleApp13.SomeSecondTrigger
info: 2022-11-20 21:34:01.3472296 +08:00 星期日 L ConsoleApp13.MyJob[0] #4
job1 job1_trigger1 2022/11/20 21:34:01 ConsoleApp13.SomeSecondTrigger

自定义作业触发器除了可重写 GetNextOccurrence 方法之后,还提供了 ShouldRunToString 方法可重写,如:

public class SomeSecondTrigger : Trigger
{
public SomeSecondTrigger(int seconds)
{
Seconds = seconds;
}

private int Seconds { get; }

public override DateTime GetNextOccurrence(DateTime startAt)
{
return startAt.AddSeconds(Seconds);
}

public override bool ShouldRun(JobDetail jobDetail, DateTime startAt)
{
// 在这里进一步控制,如果返回 false,则作业触发器跳过执行

return base.ShouldRun(jobDetail, startAt);
}

public override string ToString()
{
return $"自定义递增 {Seconds}s 触发器";
}
}

推荐重写 GetNextRunTimeToString 方法即可,如果重写了 ToString 方法,那么可以通过 ${trigger} 输出,如:

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.CompletedTask;
}
}

查看作业执行结果:

info: 2022-11-20 21:43:07.4570694 +08:00 星期日 L ConsoleApp13.MyJob[0] #4
job1 job1_trigger1 2022/11/20 21:43:07 自定义递增 3s 触发器
info: 2022-11-20 21:43:10.4629078 +08:00 星期日 L ConsoleApp13.MyJob[0] #9
job1 job1_trigger1 2022/11/20 21:43:10 自定义递增 3s 触发器

26.1.5.6 作业触发器特性

如果 JobBuilder 配置了 IncludeAnnotations 参数且为 true,那么将会自动解析 IJob 的实现类型的所有继承 TriggerAttribute 的特性,目前作业调度模块内置了以下作业触发器特性:

  • [Period(5000)]:毫秒周期(间隔)作业触发器特性
  • [PeriodSeconds(5)]:秒周期(间隔)作业触发器特性
  • [PeriodMinutes(5)]:分钟周期(间隔)作业触发器特性
  • [Cron("* * * * *", CronStringFormat.Default)]:Cron 表达式作业触发器特性
  • [MacroAt("@secondly", 1, 3, 5)]:Cron 表达式 Macro 作业触发器特性
  • [Secondly]:每秒开始作业触发器特性
  • [Minutely]:每分钟开始作业触发器特性
  • [Hourly]:每小时开始作业触发器特性
  • [Daily]:每天(午夜)开始作业触发器特性
  • [Monthly]:每月 1 号(午夜)开始作业触发器特性
  • [Weekly]:每周日(午夜)开始作业触发器特性
  • [Yearly]:每年 1 月 1 号(午夜)开始作业触发器特性
  • [Workday]:每周一至周五(午夜)开始触发器特性
  • [SecondlyAt]:特定秒开始作业触发器特性
  • [MinutelyAt]:每分钟特定秒开始作业触发器特性
  • [HourlyAt]:每小时特定分钟开始作业触发器特性
  • [DailyAt]:每天特定小时开始作业触发器特性
  • [MonthlyAt]:每月特定天(午夜)开始作业触发器特性
  • [WeeklyAt]:每周特定星期几(午夜)开始作业触发器特性
  • [YearlyAt]:每年特定月1号(午夜)开始作业触发器特性

使用如下:

services.AddSchedule(options =>
{
options.AddJob(JobBuilder.Create<MyJob>().SetIncludeAnnotations(true));

// 也支持自定义配置 + 特性扫描
options.AddJob(JobBuilder.Create<MyJob>().SetIncludeAnnotations(true)
, Triggers.PeriodSeconds(5));
});
[Minutely]
[PeriodSeconds(5)]
[Cron("* * * * *")]
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.CompletedTask;
}
}

查看作业执行结果:

info: 2022-11-20 22:10:54.5027217 +08:00 星期日 L ConsoleApp13.MyJob[0] #9
job1 job1_trigger2 2022/11/20 22:10:54 5000ms
info: 2022-11-20 22:10:59.4948832 +08:00 星期日 L ConsoleApp13.MyJob[0] #9
job1 job1_trigger2 2022/11/20 22:10:59 5000ms
info: 2022-11-20 22:11:00.0353681 +08:00 星期日 L ConsoleApp13.MyJob[0] #6
job1 job1_trigger3 2022/11/20 22:11:00 * * * * *
info: 2022-11-20 22:11:00.0372492 +08:00 星期日 L ConsoleApp13.MyJob[0] #8
job1 job1_trigger1 2022/11/20 22:11:00 * * * * *
info: 2022-11-20 22:11:04.5094807 +08:00 星期日 L ConsoleApp13.MyJob[0] #8
job1 job1_trigger2 2022/11/20 22:11:04 5000ms

除了使用内置特性,我们还可以自定义作业触发器特性,如:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class SomeSecondAttribute : TriggerAttribute
{
public SomeSecondAttribute(int seconds)
: base(typeof(SomeSecondTrigger), seconds)
{
}
}

使用如下:

[SomeSecond(3)]
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.CompletedTask;
}
}

查看作业执行结果:

info: 2022-11-20 22:16:22.0933295 +08:00 星期日 L ConsoleApp13.MyJob[0] #9
job1 job1_trigger1 2022/11/20 22:16:22 自定义递增 3s 触发器
info: 2022-11-20 22:16:25.0823563 +08:00 星期日 L ConsoleApp13.MyJob[0] #8
job1 job1_trigger1 2022/11/20 22:16:25 自定义递增 3s 触发器
info: 2022-11-20 22:16:28.0910993 +08:00 星期日 L ConsoleApp13.MyJob[0] #6
job1 job1_trigger1 2022/11/20 22:16:28 自定义递增 3s 触发器
info: 2022-11-20 22:16:31.0937955 +08:00 星期日 L ConsoleApp13.MyJob[0] #9
job1 job1_trigger1 2022/11/20 22:16:31 自定义递增 3s 触发器
info: 2022-11-20 22:16:34.1034905 +08:00 星期日 L ConsoleApp13.MyJob[0] #6
job1 job1_trigger1 2022/11/20 22:16:34 自定义递增 3s 触发器

作业触发器特性还提供了多个属性配置,如:

  • TriggerId:作业触发器 Id,string 类型
  • Description:描述信息,string 类型
  • StartTime:起始时间,string 类型
  • EndTime:结束时间,string 类型
  • MaxNumberOfRuns:最大触发次数,long 类型,0:不限制;n:N 次
  • MaxNumberOfErrors:最大出错次数,long 类型,0:不限制;n:N 次
  • NumRetries:重试次数,int 类型,默认值 0
  • RetryTimeout:重试间隔时间,int 类型,默认值 1000
  • StartNow:是否立即启动,bool 类型,默认值 true
  • RunOnStart:是否启动时执行一次,bool 类型,默认值 false
  • ResetOnlyOnce:是否在启动时重置最大触发次数等于一次的作业,bool 类型,默认值 true

使用如下:

[PeriodSeconds(5, TriggerId = "trigger1", Description = "这是一段描述")]
public class MyJob : IJob
{

26.1.5.7 设置作业触发器构建器

TriggerBuilder 提供了和 Trigger 完全匹配的 Set[属性名] 方法来配置作业触发器各个属性,如:

 services.AddSchedule(options =>
{
var triggerBuilder = Triggers.Period(5000)
.SetTriggerId("trigger1") // 作业触发器 Id
.SetTriggerType("Furion", "Furion.Schedule.PeriodTrigger") // 作业触发器类型,支持多个重载
.SetTriggerType<PeriodTrigger>() // 作业触发器类型,支持多个重载
.SetTriggerType(typeof(PeriodTrigger)) // 作业触发器类型,支持多个重载
.SetArgs("[5000]") // 作业触发器参数,支持多个重载
.SetArgs(5000) // 作业触发器参数,支持多个重载
.SetDescription("作业触发器描述") // 作业触发器描述
.SetStatus(TriggerStatus.Ready) // 作业触发器状态
.SetStartTime(DateTime.Now) // 作业触发器起始时间
.SetEndTime(DateTime.Now.AddMonths(1)) // 作业触发器结束时间
.SetLastRunTime(DateTime.Now.AddSeconds(-5)) // 作业触发器最近运行时间
.SetNextRunTime(DateTime.Now.AddSeconds(5)) // 作业触发器下一次运行时间
.SetNumberOfRuns(1) // 作业触发器触发次数
.SetMaxNumberOfRuns(100) // 作业触发器最大触发器次数
.SetNumberOfErrors(1) // 作业触发器出错次数
.SetMaxNumberOfErrors(100) // 作业触发器最大出错次数
.SetNumRetries(3) // 作业触发器出错重试次数
.SetRetryTimeout(1000) // 作业触发器重试间隔时间
.SetStartNow(true) // 作业触发器是否立即启动
.SetRunOnStart(false) // 作业触发器是否启动时执行一次
.SetResetOnlyOnce(true) // 作业触发器是否在启动时重置最大触发次数等于一次的作业
;

options.AddJob<MyJob>(triggerBuilder);
});

26.1.5.8 多种格式字符串输出

TriggerTriggerBuilder 都提供了多种将自身转换成特定格式的字符串。

  1. 转换成 JSON 字符串
var json = trigger.ConvertToJSON();

字符串打印如下:

{
"triggerId": "job1_trigger1",
"jobId": "job1",
"triggerType": "Furion.Schedule.PeriodSecondsTrigger",
"assemblyName": "Furion",
"args": "[5]",
"description": null,
"status": 2,
"startTime": null,
"endTime": null,
"lastRunTime": "2022-11-20T22:25:03.8176033+08:00",
"nextRunTime": "2022-11-20T22:25:08.8385903+08:00",
"numberOfRuns": 1,
"maxNumberOfRuns": 0,
"numberOfErrors": 0,
"maxNumberOfErrors": 0,
"numRetries": 0,
"retryTimeout": 1000,
"startNow": true,
"runOnStart": false,
"resetOnlyOnce": true,
"updatedTime": "2022-11-20T22:25:03.8386511+08:00"
}
  1. 转换成 SQL 字符串
// 输出新增 SQL,使用 CamelCase 属性命名
var insertSql = trigger.ConvertToSQL("tbName"
, PersistenceBehavior.Appended
, NamingConventions.CamelCase);
// 更便捷拓展
var insertSql = trigger.ConvertToInsertSQL("tbName", NamingConventions.CamelCase);

// 输出删除 SQL,使用 Pascal 属性命名
var deleteSql = trigger.ConvertToSQL("tbName"
, PersistenceBehavior.Removed
, NamingConventions.Pascal);
// 更便捷拓展
var deleteSql = trigger.ConvertToDeleteSQL("tbName", NamingConventions.Pascal);

// 输出更新 SQL,使用 UnderScoreCase 属性命名
var updateSql = trigger.ConvertToSQL("tbName"
, PersistenceBehavior.Updated
, NamingConventions.UnderScoreCase);
// 更便捷拓展
var updateSql = trigger.ConvertToUpdateSQL("tbName", NamingConventions.UnderScoreCase);

字符串打印如下:

-- 新增语句
INSERT INTO tbName(
[triggerId],
[jobId],
[triggerType],
[assemblyName],
[args],
[description],
[status],
[startTime],
[endTime],
[lastRunTime],
[nextRunTime],
[numberOfRuns],
[maxNumberOfRuns],
[numberOfErrors],
[maxNumberOfErrors],
[numRetries],
[retryTimeout],
[startNow],
[runOnStart],
[resetOnlyOnce],
[updatedTime]
)
VALUES(
'job1_trigger1',
'job1',
'Furion.Schedule.PeriodSecondsTrigger',
'Furion',
'[5]',
NULL,
2,
NULL,
NULL,
'2022/11/20 22:27:47',
'2022/11/20 22:27:52',
1,
0,
0,
0,
0,
1000,
1,
0,
1,
'2022/11/20 22:27:47'
);

-- 删除语句
DELETE FROM tbName
WHERE [TriggerId] = 'job1_trigger1' AND [JobId] = 'job1';

-- 更新语句
UPDATE tbName
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/11/20 22:27:47',
[next_run_time] = '2022/11/20 22:27:52',
[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,
[updated_time] = '2022/11/20 22:27:47'
WHERE [trigger_id] = 'job1_trigger1' AND [job_id] = 'job1';
  1. 转换成 Monitor 字符串
var monitor = trigger.ConvertToMonitor();

字符串打印如下:

┏━━━━━━━━━━━  Trigger ━━━━━━━━━━━
┣ Furion.Schedule.PeriodSecondsTrigger

┣ triggerId: job1_trigger1
┣ jobId: job1
┣ triggerType: Furion.Schedule.PeriodSecondsTrigger
┣ assemblyName: Furion
┣ args: [5]
┣ description:
┣ status: Running
┣ startTime:
┣ endTime:
┣ lastRunTime: 2022/11/20 22:30:41
┣ nextRunTime: 2022/11/20 22:30:46
┣ numberOfRuns: 1
┣ maxNumberOfRuns: 0
┣ numberOfErrors: 0
┣ maxNumberOfErrors: 0
┣ numRetries: 0
┣ retryTimeout: 1000
┣ startNow: True
┣ runOnStart: False
┣ resetOnlyOnce: True
┣ updatedTime: 2022/11/20 22:30:41
┗━━━━━━━━━━━ Trigger ━━━━━━━━━━━
  1. 简要字符串输出
var str = trigger.ToString();
<job3 job3_trigger1> 这是一段描述
<job2 job2_trigger1> 这是一段描述 * * * * * *
<job1 job1_trigger1> 这是一段描述 5000ms

26.1.5.9 自定义 SQL 输出配置

版本说明

以下内容仅限 Furion 4.8.2 + 版本使用。

services.AddSchedule(options =>
{
options.Trigger.ConvertToSQL = (tableName, columnNames, trigger, behavior, naming) =>
{
// 生成新增 SQL
if (behavior == PersistenceBehavior.Appended)
{
return trigger.ConvertToInsertSQL(tableName, naming);
}
// 生成更新 SQL
else if (behavior == PersistenceBehavior.Updated)
{
return trigger.ConvertToUpdateSQL(tableName, naming);
}
// 生成删除 SQL
else if (behavior == PersistenceBehavior.Removed)
{
return trigger.ConvertToDeleteSQL(tableName, naming);
}

return string.Empty;
};
});
  • ConvertToSQL 委托参数说明
    • tableName:数据库表名称,string 类型
    • columnNames:数据库列名:string[] 类型,只能通过 索引 获取
    • jobDetail:作业信息 Trigger 对象
    • behavior:持久化 PersistenceBehavior 类型,用于标记新增,更新还是删除操作
    • naming:命名法 NamingConventions 类型,包含 CamelCase(驼峰命名法)Pascal(帕斯卡命名法)UnderScoreCase(下划线命名法)
注意事项

如果在该委托方法中调用 trigger.ConvertToSQL(..) 方法会导致死循环。

26.1.6 作业计划 Scheduler 及构建器

26.1.6.1 关于作业计划

所谓的作业计划(Scheduler)是将作业信息(JobDetail),作业触发器(Trigger)和作业处理程序(IJob)关联起来,并添加到作业调度器中等待调度执行。

作为计划(Scheduler)类型对外是不公开的,但提供了对应的 IScheduler 接口进行操作。

26.1.6.2 关于作业计划构建器

作业计划 Scheduler 是作业调度模块提供运行时的只读类型,那么我们该如何创建或变更 Scheduler 对象呢?

Sch