65.9K
CodeProject 正在变化。 阅读更多。
Home

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

2020年7月30日

CPOL

9分钟阅读

viewsIcon

33000

downloadIcon

1743

使用 Quartz 调度器制作调度作业的 exe

引言

在这里,我们将学习 Quartz 并使用 C# 进行任务调度。目的是创建一个简单的 Windows 窗体(桌面)应用程序并调度一个或多个作业。桌面应用程序将包含以下功能:

  1. 选项
    1. 开始/暂停
    2. 在作业运行过程中进行确认并强制停止
    3. 跳过时间重叠的任务
  2. 可视化信息
    1. 下一个预计开始时间
    2. 实际开始时间
    3. 结束时间
    4. 任务结果(是否能够无错误运行)
  3. 在同一个应用程序中运行多个作业
  4. 通过配置文件为每个作业配置调度时间

项目

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 窗口上显示的行数最大值
  • public 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”,但本月没有第五个指定的星期几,则当月将不会发生任何运行。

 

更多细节

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 日:初始版本
© . All rights reserved.