C#: 使用 Quartz 进行调度,我的第一个桌面调度作业!





5.00/5 (5投票s)
使用 Quartz 调度器制作调度作业的 exe
引言
在这里,我们将学习 Quartz 并使用 C# 进行任务调度。目的是创建一个简单的 Windows 窗体(桌面)应用程序并调度一个或多个作业。桌面应用程序将包含以下功能:
- 选项
- 开始/暂停
- 在作业运行过程中进行确认并强制停止
- 跳过时间重叠的任务
- 可视化信息
- 下一个预计开始时间
- 实际开始时间
- 结束时间
- 任务结果(是否能够无错误运行)
- 在同一个应用程序中运行多个作业
- 通过配置文件为每个作业配置调度时间
项目
Schedule.Single
只运行单个作业的调度上下文。
Schedule.Multiple
运行多个作业的调度上下文。例如,我们运行名为“Email
”和“Hr
”的两个作业。
必备组件
先决条件如下:
核心
在这里,我们将创建一个共享/核心 DLL 项目。它将包含所有适配器、事件和 Quartz 相关的代码。所有其他 UI 或客户端项目都需要从此项目引用。
调度委托
一些事件辅助委托
public delegate void TaskExecution();
public delegate void TaskExecutionComplete(Exception exception);
public delegate bool ShouldTaskExecution();
调度接口
这是一个简单的调度结构。
/// <summary>
/// Schedule adapter
/// </summary>
public interface ISchedule
{
string Name { get; }
event TaskExecution TaskStarted;
event TaskExecution TaskVetoed;
event TaskExecutionComplete TaskExecuted;
event ShouldTaskExecution TaskShouldBeVetoed;
bool IsStarted();
bool IsTaskRunning();
bool IsPaused();
void Start();
void Pause();
void Resume();
DateTime? NextEstimatedFireTime();
}
string Name { get; }
:调度的名称event TaskExecution TaskStarted;
:当调度作业开始时触发event TaskExecution TaskVetoed;
:如果调度作业被跳过则触发event TaskExecutionComplete TaskExecuted;
:当调度作业完成时触发event ShouldTaskExecution TaskShouldBeVetoed;
:定义是否应该停止运行计划好的作业bool IsStarted();
:检查调度是否已启动bool IsTaskRunning();
:检查调度作业是否仍在进行中bool IsPaused();
:检查调度是否已暂停void Start();
:开始调度作业void Pause();
:暂停已运行的调度作业void Resume(); r
:恢复调度DateTime? NextEstimatedFireTime();
:获取下一个预计的运行时间
调度作业监听器
IJobListener
来自 Quartz 库,我们通过实现它来创建自己的调度作业监听器。在这里,我们添加了额外的事件。
public class ScheduleJobListener : IJobListener
{
public event TaskExecution Started;
public event TaskExecution Vetoed;
public event TaskExecutionComplete Executed;
public ScheduleJobListener(string name)
{
Name = name;
}
public void JobToBeExecuted(IJobExecutionContext context)
{
Started?.Invoke();
}
public void JobExecutionVetoed(IJobExecutionContext context)
{
Vetoed?.Invoke();
}
public void JobWasExecuted
(IJobExecutionContext context, JobExecutionException jobException)
{
Executed?.Invoke(jobException);
}
public string Name { get; }
}
调度触发器监听器
ITriggerListener
来自 Quartz 库,我们通过实现它来创建自己的调度触发器监听器。在这里,我们添加了额外的事件。
public class ScheduleTriggerListener : ITriggerListener
{
public event ShouldTaskExecution ShouldVeto;
public ScheduleTriggerListener(string name)
{
Name = name;
}
public void TriggerFired(ITrigger trigger, IJobExecutionContext context)
{
}
public bool VetoJobExecution(ITrigger trigger, IJobExecutionContext context)
{
if (ShouldVeto == null)
{
return false;
}
else
{
return ShouldVeto();
}
}
public void TriggerMisfired(ITrigger trigger)
{
}
public void TriggerComplete(ITrigger trigger, IJobExecutionContext context,
SchedulerInstruction triggerInstructionCode)
{
}
public string Name { get; }
}
请在此处阅读更多关于作业监听器和调度触发器部分的内容:这里。
计划
这是我们的主调度基类,实现了我们之前共享的 ISchedule
接口。
public abstract class Schedule : ISchedule
{
public event TaskExecution TaskStarted;
public event TaskExecution TaskVetoed;
public event TaskExecutionComplete TaskExecuted;
public event ShouldTaskExecution TaskShouldBeVetoed;
protected readonly IScheduler Scheduler;
public readonly string ScheduleName;
public string Name
{
get
{
return ScheduleName;
}
}
private string TriggerName
{
get
{
return ScheduleName + "Trigger";
}
}
private string JobName
{
get
{
return ScheduleName + "Job";
}
}
private string JobListenerName
{
get
{
return ScheduleName + "JobListener";
}
}
private string TriggerListenerName
{
get
{
return ScheduleName + "TriggerListener";
}
}
private bool _isStarted;
protected Schedule(IScheduler scheduler, string name)
{
if (String.IsNullOrEmpty(name))
{
throw new NullReferenceException("Schedule Name required");
}
if (scheduler == null)
{
throw new NullReferenceException("Scheduler required");
}
ScheduleName = name;
Scheduler = scheduler;
}
/// <summary>
/// this is not cluster aware
/// </summary>
/// <returns></returns>
/*https://stackoverflow.com/questions/24568282/check-whether-the-job-is-running-or-not*/
public bool IsTaskRunning()
{
bool value = Scheduler.GetCurrentlyExecutingJobs().Any(x =>
x.JobDetail.Key.Name.Equals(JobName, StringComparison.OrdinalIgnoreCase));
return value;
}
/*https://stackoverflow.com/questions/21527841/schedule-multiple-jobs-in-quartz-net*/
protected void ScheduleTask(JobBuilder jobBuilder, TriggerBuilder triggerBuilder)
{
IJobDetail job = jobBuilder.WithIdentity(JobName).Build();
ITrigger trigger = AssignTriggerName(triggerBuilder).Build();
Scheduler.ScheduleJob(job, trigger);
}
private TriggerBuilder AssignTriggerName(TriggerBuilder triggerBuilder)
{
return triggerBuilder.WithIdentity(TriggerName);
}
protected abstract Tuple<JobBuilder, TriggerBuilder> Settings();
public bool IsStarted()
{
bool value = _isStarted;
return value;
}
public void Start()
{
Tuple<JobBuilder, TriggerBuilder> setting = Settings();
ScheduleTask(setting.Item1, setting.Item2);
AttachJobListener();
_isStarted = true;
}
public bool IsPaused()
{
TriggerKey key = new TriggerKey(TriggerName);
bool value = Scheduler.GetTriggerState(key) == TriggerState.Paused;
return value;
}
public void Pause()
{
Scheduler.PauseJob(new JobKey(JobName));
}
public void Interrupt()
{
Scheduler.Interrupt(new JobKey(JobName));
}
public void Resume()
{
Scheduler.ResumeJob(new JobKey(JobName));
}
/*
* https://stackoverflow.com/questions/16334411/
* quartz-net-rescheduling-job-with-new-trigger-set
*/
protected void Reschedule(TriggerBuilder triggerBuilder)
{
TriggerKey key = new TriggerKey(TriggerName);
ITrigger trigger = AssignTriggerName(triggerBuilder).Build();
Scheduler.RescheduleJob(key, trigger);
}
/*
*https://www.quartz-scheduler.net/documentation/quartz-2.x/
*tutorial/trigger-and-job-listeners.html
*/
protected void AttachJobListener()
{
ScheduleJobListener jobListener = new ScheduleJobListener(JobListenerName);
jobListener.Started += TaskStarted;
jobListener.Vetoed += TaskVetoed;
jobListener.Executed += TaskExecuted;
Scheduler.ListenerManager.AddJobListener
(jobListener, KeyMatcher<JobKey>.KeyEquals(new JobKey(JobName)));
ScheduleTriggerListener triggerListener =
new ScheduleTriggerListener(TriggerListenerName);
triggerListener.ShouldVeto += TaskShouldBeVetoed;
Scheduler.ListenerManager.AddTriggerListener
(triggerListener, KeyMatcher<TriggerKey>.KeyEquals(new TriggerKey(TriggerName)));
}
public DateTime? NextEstimatedFireTime()
{
TriggerKey key = new TriggerKey(TriggerName);
DateTimeOffset? timeUtc = Scheduler.GetTrigger(key).GetNextFireTimeUtc();
DateTime? dateTime = timeUtc == null
? (DateTime?)null
: timeUtc.Value.ToLocalTime().DateTime;
return dateTime;
}
protected TriggerBuilder CreateTrigger(string cronJobExpression)
{
return TriggerBuilder.Create().WithCronSchedule(cronJobExpression);
}
}
在 Quartz 中,我们将相同的调度作业附加到多个触发器上,这是一种多对多关系。为了更好地控制,我们在这里设计系统,使其能够将一个调度作业只附加到一个触发器上,就像一对一的关系一样。同样,一个调度应该只有一个作业和一个触发器。
调度上下文
这是调度的基上下文类。一个调度上下文可以包含多个调度。
public abstract class ScheduleContext
{
protected NameValueCollection Props { get; set; }
protected IScheduler Scheduler { get; private set; }
/// <summary>
/// Start Schedule
/// </summary>
protected void Start()
{
var factory = Props == null ? new StdSchedulerFactory() :
new StdSchedulerFactory(Props);
Scheduler = factory.GetScheduler();
Scheduler.Start(); /*impt*/
}
//public abstract void Rebuild();
public void PauseAll()
{
Scheduler.PauseAll();
}
public void ResumeAll()
{
Scheduler.ResumeAll();
}
/*http://www.quartz-scheduler.org/documentation/quartz-2.x/cookbook/ShutdownScheduler.html*/
/// <summary>
/// force stop
/// </summary>
public void Stop()
{
if (!Scheduler.IsShutdown)
{
Scheduler.Shutdown();
Scheduler = null;
}
}
/// <summary>
/// scheduler will not allow this method to return until
/// all currently executing jobs have completed.
/// (hang up, if triggered middle of a job)
/// </summary>
public void WaitAndStop()
{
if (!Scheduler.IsShutdown)
{
Scheduler.Shutdown(true);
Scheduler = null;
}
}
}
创建调度
调度作业
在这里,我们创建一个将在调度中使用的作业。
[PersistJobDataAfterExecution] /*save temp data*/
[DisallowConcurrentExecution] /*impt: no multiple instances executed concurrently*/
public class TestScheduleJob : IJob
{
public void Execute(IJobExecutionContext context)
{
/*Add tasks we need to do*/
}
}
计划
让我们使用我们的调度作业创建一个调度。
public class TestSchedule : Core.Schedule
{
public TestSchedule(IScheduler scheduler) : base(scheduler, "TestSchedule")
{
}
protected override Tuple<JobBuilder, TriggerBuilder> Settings()
{
string cronJobExpression = ConfigurationManager.AppSettings["CronJobExpression"];
TriggerBuilder triggerBuilder =
TriggerBuilder.Create().WithCronSchedule(cronJobExpression);
JobBuilder jobBuilder = JobBuilder.Create<TestScheduleJob>();
return new Tuple<JobBuilder, TriggerBuilder>(jobBuilder, triggerBuilder);
}
}
override Tuple<JobBuilder, TriggerBuilder> Settings()
:该方法使用我们之前创建的调度作业,并通过读取 app.config 文件中的 cron 表达式来创建触发器。
<!--12 AM to 11 PM every 5 second-->
<add key="CronJobExpression" value="0/5 0-59 0-23 * * ?" />
调度上下文
在这里,我们创建了一个调度上下文。现在我们只使用一个调度。但对于多个窗口,会有两个或多个,根据需要。
public class TestScheduleContext : ScheduleContext
{
private TestSchedule _testSchedule;
public TestSchedule TestSchedule
{
get
{
_testSchedule = _testSchedule ?? new TestSchedule(Scheduler);
return _testSchedule;
}
}
public TestScheduleContext()
{
Props = new NameValueCollection
{
{"quartz.scheduler.instanceName", nameof(TestScheduleContext)}
};
Start();
}
}
UI
让我们开始创建一个 Windows 窗体。
public readonly uint MaxLine;
public readonly string DateTimeDisplayFormat;
public readonly uint VetoedTimeOffset;
public readonly BackgroundWorker Bw;
private TestScheduleContext _scheduleContext;
private DateTime _lastEstimatedStartTime;
public Form1()
{
InitializeComponent();
MaxLine = Convert.ToUInt16(ConfigurationManager.AppSettings["ReachTextBoxMaxLine"]);
DateTimeDisplayFormat = ConfigurationManager.AppSettings["DateTimeDisplayFormat"];
VetoedTimeOffset = Convert.ToUInt16(ConfigurationManager.AppSettings
["VetoedTimeOffsetMilliSeconds"]);
Bw = new BackgroundWorker();
Bw.WorkerSupportsCancellation = true;
Bw.DoWork += StartScheduler;
Bw.RunWorkerCompleted += BgWorkerCompleted;
Bw.RunWorkerAsync();
}
public readonly uint MaxLine;
:要在 UI 窗口上显示的行数最大值- p
ublic readonly string DateTimeDisplayFormat;
:显示DateTime
字符串的格式 public readonly uint VetoedTimeOffset;
:要否决任何作业的偏移时间
我们将从 app.config 文件中读取上述值。我们还需要在后台运行事物,所以我们将使用 BackgroundWorker
。
private void StartScheduler(object sender, DoWorkEventArgs e)
{
/*close backgraound worker*/
if (Bw.CancellationPending)
{
e.Cancel = true;
return;
}
/*schedule*/
_scheduleContext = new TestScheduleContext();
/*control*/
this.Invoke((MethodInvoker)delegate
{
chkBoxStartEnd.Appearance = Appearance.Button;
chkBoxStartEnd.TextAlign = ContentAlignment.MiddleCenter;
chkBoxStartEnd.MinimumSize = new Size(75, 25); //To prevent shrinkage!
chkBoxStartEnd.Text = "Start";
});
}
private void BgWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
Log.WriteError("SchedulerTest", e.Error);
this.Invoke((MethodInvoker)delegate
{
richTextBox1.AddLine("Error to Run!");
richTextBox1.ScrollToCaret();
});
}
}
开始/暂停
它实际上是 UI 中的一个切换按钮。
StartSchedule()
:启动/恢复调度上下文及其单个调度。此方法内部有一些事件附件,请检查它们。它会在 UI 中打印下一个预计的开始日期和时间。PausedSchedule()
:暂停调度上下文并在 UI 中打印暂停状态。
private void chkBoxStartEnd_Click(object sender, EventArgs e)
{
if (chkBoxStartEnd.Checked)
{
StartSchedule();
chkBoxStartEnd.Text = "Pause";
}
else
{
PausedSchedule();
chkBoxStartEnd.Text = "Start";
}
}
private void StartSchedule()
{
if (!_scheduleContext.TestSchedule.IsStarted())
{
_scheduleContext.TestSchedule.TaskStarted += BeforeStart;
_scheduleContext.TestSchedule.TaskVetoed += Vetoed;
_scheduleContext.TestSchedule.TaskExecuted += AfterEnd;
_scheduleContext.TestSchedule.TaskShouldBeVetoed += TaskShouldBeVetoed;
_scheduleContext.TestSchedule.Start(); /*this context contains only one schedule*/
}
else
{
_scheduleContext.TestSchedule.Resume();
}
PrintNextEstimatedStartTime(_scheduleContext.TestSchedule.NextEstimatedFireTime());
}
private void PausedSchedule()
{
_scheduleContext.TestSchedule.Pause();
if (_scheduleContext.TestSchedule.IsTaskRunning())
{
MessageBox.Show("Task still running, will be paused after task ends.");
return;
}
this.Invoke((MethodInvoker)delegate
{
richTextBox1.AddLine("Paused", MaxLine);
richTextBox1.ScrollToCaret();
});
}
通过点击开始按钮,调度将启动,并显示 **暂停** 按钮。
如果我们点击 **暂停** 按钮,应用程序将尝试停止当前正在运行的调度。如果一个调度作业正在进行中,它将显示一个确认窗口。
开始之前和结束之后
BeforeStart()
:在每个调度作业开始之前触发。它会在 UI 中打印实际的开始日期和时间。AfterEnd(Exception exception)
:在每个调度作业结束之后触发。它会在 UI 中打印结束日期和时间以及结果部分。如果运行调度作业时出现任何错误,结果将显示为 **错误**,否则将打印 **成功**。我们在这里还使用了错误日志记录器Log.WriteError("SchedulerTest", exception)
。它还将打印下一个预计的开始日期和时间。
private void PrintNextEstimatedStartTime(DateTime? dateTime)
{
string msg = "Estimated: ";
if (dateTime != null)
{
_lastEstimatedStartTime = ((DateTime) dateTime);
msg += _lastEstimatedStartTime.ToString(DateTimeDisplayFormat);
}
this.Invoke((MethodInvoker)delegate
{
richTextBox1.AddLine(msg, MaxLine);
richTextBox1.ScrollToCaret();
});
}
private void BeforeStart()
{
string startDateTime = DateTime.Now.ToString(DateTimeDisplayFormat);
this.Invoke((MethodInvoker)delegate
{
richTextBox1.AppendText("\t\t" + "Started: " + startDateTime);
richTextBox1.ScrollToCaret();
});
}
private void AfterEnd(Exception exception)
{
this.Invoke((MethodInvoker)delegate
{
richTextBox1.AppendText("\t\t" + "Ended: " +
DateTime.Now.ToString(DateTimeDisplayFormat));
richTextBox1.ScrollToCaret();
});
string status = String.Empty;
if (exception != null)
{
status = "Error";
Log.WriteError("SchedulerTest", exception);
}
else
{
status = "Success";
}
this.Invoke((MethodInvoker)delegate
{
richTextBox1.AppendText("\t\t" + "Result: " + status);
richTextBox1.ScrollToCaret();
});
if (_scheduleContext.TestSchedule.IsPaused())
{
this.Invoke((MethodInvoker)delegate
{
richTextBox1.AddLine("Paused", MaxLine);
richTextBox1.ScrollToCaret();
});
}
else
{
PrintNextEstimatedStartTime(_scheduleContext.TestSchedule.NextEstimatedFireTime());
}
}
否决
bool TaskShouldBeVetoed()
:确定是否需要跳过当前调度作业。void Vetoed()
:在每个调度作业被否决后触发。它会在 UI 中打印否决日期和时间以及下一个预计的开始日期和时间。在这种情况下,不会打印结束日期和时间以及结果部分。
private bool TaskShouldBeVetoed()
{
/*string compare*/
//var value = DateTime.Now.ToString(DateTimeDisplayFormat) !=
// _lastEstimatedStartTime.ToString(DateTimeDisplayFormat);
/*compare with offset*/
DateTime nowDateTime = DateTime.Now.TrimMilliseconds();
DateTime tillDateTime = _lastEstimatedStartTime.TrimMilliseconds().AddMilliseconds
(VetoedTimeOffset);
bool dateTimeNowInRange = nowDateTime <= tillDateTime;
var value = !dateTimeNowInRange;
return value;
}
private void Vetoed()
{
string dateTime = DateTime.Now.ToString(DateTimeDisplayFormat);
this.Invoke((MethodInvoker)delegate
{
richTextBox1.AppendText("\t\t" + "Vetoed: " + dateTime);
richTextBox1.ScrollToCaret();
});
PrintNextEstimatedStartTime(_scheduleContext.TestSchedule.NextEstimatedFireTime());
}
关闭窗体
如果有人点击窗口关闭按钮,我们将检查是否有任何调度作业正在运行。另外,重要的是要确保应用程序不再在后台运行。我只在窗体应用程序中发现了这个问题,而在控制台应用程序中没有。
CloseApp()
:确保应用程序在不运行在后台的情况下停止。Form1_FormClosing(object sender, FormClosingEventArgs e)
:当有人点击关闭按钮时触发。需要将此方法附加到窗体的FormClosing
事件。如果调度作业正在进行中,它将显示一个确认窗口。
private void CloseApp()
{
/*stop shedule*/
_scheduleContext.Stop();
/*close backgraound worker
*https://stackoverflow.com/questions/4732737/how-to-stop-backgroundworker-correctly
*/
if (Bw.IsBusy)
{
Bw.CancelAsync();
}
while (Bw.IsBusy)
{
Application.DoEvents();
}
/*kill all running process
* https://stackoverflow.com/questions/8507978/exiting-a-c-sharp-winforms-application
*/
System.Diagnostics.Process.GetCurrentProcess().Kill();
Application.Exit();
Environment.Exit(0);
}
/*attached with forms, FormClosing event*/
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (_scheduleContext.TestSchedule.IsTaskRunning())
{
/*A schedule is already running*/
var window = MessageBox.Show(
"A task is in progress, do you still want to close?",
"Close Window",
MessageBoxButtons.YesNo);
if (window == DialogResult.Yes)
{
CloseApp();
}
e.Cancel = (window == DialogResult.No);
}
else
{
CloseApp();
}
}
app.config
<!--Rich text box: Max number of lines to be displayed-->
<add key="ReachTextBoxMaxLine" value="720"/>
<!--Display date formate-->
<add key="DateTimeDisplayFormat" value="dd-MMM-yyyy hh:mm:ss tt"/>
<!--0 second-->
<add key="VetoedTimeOffsetMilliSeconds" value="0" />
<!--12 AM to 11 PM every 5 second-->
<add key="CronJobExpression" value="0/5 0-59 0-23 * * ?" />
项目
这是一个 **Visual Studio 2017** 解决方案。您可能需要设置启动项目。
Schedule.Core
(DLL 项目)Schedule.Ui.Core
(UI 核心)Schedule.Single
(可部署的可执行文件项目)Schedule.Multiple
(可部署的可执行文件项目)
该代码对于未经测试的输入可能会抛出意外错误。如果有,请告诉我。
即将推出
我们可以在以下环境中执行相同的操作:
- ASP.NET 控制台项目
- ASP.NET CORE 控制台项目
稍后将分享项目...
Cron 表达式入门
让我们了解一下 cron 表达式是什么,如何使用它,以及一些示例。
Cron 表达式
语法
<second> <minute> <hour> <day-of-month> <month> <day-of-week> <year>
- 语法应包含 **6-7 个字段**(最少 6 个)
- **<month-day>** 或 **<day-of-week>** 中必须有一个是 **?** (两个 **?** 不能同时使用)
Cron 表达式字段和值
字段名称 | 是否必填 | 允许的值 | 允许的特殊字符 |
秒 | 是 | 0-59 | , - * / |
分钟数 | 是 | 0-59 | , - * / |
小时数 | 是 | 0-23 | , - * / |
月份中的日期 | 是 | 1-31 | , - * ? / L W |
月 | 是 | 1-12 或 JAN-DEC | , - * ? / |
星期中的日期 | 是 | 1-7 或 SUN-SAT | , - * ? / L # |
年份 | 否 | 空,1970-2099 | , - * |
Cron 表达式特殊字符
字符
| 在 Cron 表达式中的含义
|
*
| 用于选择字段中的**所有值**。例如,分钟字段中的 **"*"** 表示“每分钟”,月份字段中的 **"*"** 表示每个月。
|
?
| 用于表示**没有特定值**,即当值不重要时。例如,如果您希望实例在特定日期运行(例如,10 日),但不在乎那天是星期几,则可以在月份日期字段中放入“10”,在星期日期字段中放入“?”。
|
-
| 用于指定**范围**。例如,小时字段中的“10-12”表示“10 点、11 点和 12 点”。
|
,
| 用于指定**附加值**。例如,星期日期字段中的“MON,WED,FRI”表示“星期一、星期三和星期五”。
|
/
| 用于指定**增量**。例如,秒字段中的“5/15”表示“每 15 秒运行一次实例,从第 5 秒开始。这将是第 5、20、35 和 50 秒”。月份日期字段中的“1/3”表示“从本月第一天开始,每 3 天运行一次实例”。
|
L
| (“**last**”) 这个字符在月份日期和星期日期中有不同的含义。例如,月份日期字段中的值“L”表示“本月最后一天”——1 月份是 31 日,非闰年的 2 月份是 28 日。如果单独用于星期日期字段,它仅表示“7”或“SAT”。但如果用于星期日期字段且在其后有其他值,则表示“本月的最后 xxx”——例如,“6L”表示“本月的最后一个星期五”。使用“L”选项时,重要的是不要指定列表或日期范围。
|
W
| (“**weekday**”) - 用于指定**最接近**给定日期的工作日(星期一至星期五)。例如,如果您为月份日期字段指定“15W”,其含义是:“本月 15 号最近的工作日”。因此,如果 15 号是星期六,实例将在 14 号星期五运行。如果 15 号是星期日,则触发器将在 16 号星期一触发。如果 15 号是星期二,则将在 15 号星期二触发。但是,如果您为月份日期指定“1W”,而 1 号是星期六,则触发器将在 3 号星期一触发,因为它不会跨越月份的日期边界。“W”字符只能在月份日期是单个日期时指定,而不是范围或列表。
|
LW
| L 和 W 字符可以在月份日期字段中组合,以指定“**本月的最后一个工作日**”。
|
#
| 用于指定“**本月第 n 个**”日期。 例如,星期日期字段中的值“6#3”表示“本月的第三个星期五”(6=星期五,#3=本月的第 3 个)。 “2#1”= 本月的第一个星期一 “4#5”= 本月的第五个星期三。 **注意:** 如果您指定“#5”,但本月没有第五个指定的星期几,则当月将不会发生任何运行。
|
更多细节
- https://www.baeldung.com/cron-expressions
- https://medium.com/@tushar0618/cron-expression-tutorial-721d85e4c2a7
- https://autotime.ga.com/autotime/help/cron_expressions.htm
Cron 表达式示例
在线 Cron 表达式测试
每秒
* * * * * * /*does not work, need one ?*/
* * * ? * *
* * * * * ?
每分钟,第 45 秒
45 0-59 0-23 * * ?
每小时,从 00 分开始,每 10 分钟间隔
0 0/10 * * * ?
0 0,10,20,30,40,50 * * * ?
每小时,从 05 分开始,每 10 分钟间隔
0 5/10 * * * ?
每天一次,在凌晨 02:30:00
0 30 2 * * ?
晚上 10 点到早上 6 点,从 00 分开始,每 30 分钟间隔
0 0/30 22-23,23,0-6 * * ?
每小时的第 15 分钟,00 秒
0 15 * * * ?
每周六,从 00 分开始,每 30 分钟间隔
0 0/30 * ? * 7
历史
- 2020 年 7 月 30 日:初始版本