石英调度器:永远不会执行的 Cron 表达式

119

我知道这里有一个重复的问题,可能正好是我的情况,虽然它值得一些更好的解释,我会在这里尝试提供。

我使用Spring应用程序上下文工作,从事Java网络应用程序。在此上下文中,我使用Quartz定义了定时作业。这些作业由.properties文件中定义的cron触发。

Spring上下文嵌入在war中,而.properties文件位于应用服务器(特别是Tomcat)上。

这很好,并允许根据环境(开发、集成、生产等)定义不同的cron表达式。

现在,在本机计算机上运行此应用程序时,我不希望执行这些作业。是否有一种方式编写永远不会触发的cron表达式?


就像其他问题中一样,唯一的标准方法是以 # 注释字符开头的命令。 - Barmar
1
我可能没有表达清楚:虽然Spring/Quartz框架使用cron语法,但这不是一个crontab:cron用于XML字段中:<property name="cronExpression" value="<expression>" />你不能通过注释该行来停用作业。谢谢,我会使用建议的方法指定一个未来很久的年份,到那时我们所知道的计算机已经消失了。 - Chop
显然,Quartz cron表达式并不像Unix cron表达式那样,因为Unix cron没有秒或年。cron标签可能不适用于此问题。 - Barmar
你说得对,我已经遇到了一些不一致之处。我会删除“cron”标签。 - Chop
9个回答

104

TL;DR

在Quartz 1中,你可以使用这个cron: 59 59 23 31 12 ? 2099 (最后一个有效日期)。
在Quartz 2中,你可以使用这个cron: 0 0 0 1 1 ? 2200

使用远期表达式

使用org.quartz.CronExpression进行了一些快速测试。

String exp = "0 0 0 1 1 ? 3000";
boolean valid = CronExpression.isValidExpression(exp);
System.out.println(valid);
if (valid) {
    CronExpression cronExpression = new CronExpression(exp);
    System.out.println(cronExpression.getNextValidTimeAfter(new Date()));
}

当我执行String exp = "# 0 0 0 1 1 ?";时,isValid测试返回false
尽管使用了上面给出的示例,输出如下:
true
null

含义:

  • 该表达式是有效的;
  • 没有即将到来的日期与此表达式匹配。

然而,为了使调度程序接受cron触发器,后者必须匹配未来的某个日期。

我尝试了几年,并发现一旦年份超过2300年,Quartz似乎就不再关心了(尽管我没有在Quartz 2的文档中找到年份的最大值的提及)。可能有更简洁的方法来解决这个问题,但现在这样做已经满足了我的需求。

因此,最终我建议使用的cron表达式是0 0 0 1 1 ? 2200

Quartz 1 变体

请注意,在Quartz 1中,2099是最后一个有效的年份。因此,您可以根据Maciej Matys的建议调整cron表达式:59 59 23 31 12?2099

备选方案:使用过去的日期

Arnaud Denoyelle提出了一种更优雅的解决方案,我的测试证实这是一个正确的表达式:不要选择一个遥远的未来日期,而是选择一个遥远的过去日期: 0 0 0 1 1?1970(根据Quartz文档,这是第一个有效的表达式)。 但是,这个解决方案并不起作用。 hippofluff指出,Quartz会检测到过去的表达式永远不会再次执行,因此会抛出异常。
org.quartz.SchedulerException: Based on configured schedule, the given trigger will never fire.

这似乎已经在Quartz中很长时间了。

教训:测试并不是绝对可靠的

这凸显了我的测试的一个弱点:如果您想测试一个CronExpression,请记住它必须有一个nextValidTime1。否则,您将传递给它的调度程序将仅使用上述异常拒绝它。

我建议按以下方式调整测试代码:

String exp = "0 0 0 1 1 ? 3000";
boolean valid = CronExpression.isValidExpression(exp);
if (valid) {
    CronExpression cronExpression = new CronExpression(exp);
    valid = cronExpression.getNextValidTimeAfter(new Date()) != null;
}
System.out.println("Can I use <" + exp + ">? " + (valid ? "Go ahead!" : "This shall fail."));

这里有:无需思考,只需阅读输出。


1. 这是我在测试Arnaud的解决方案时忘记的部分,让我变成了傻瓜并证明我的测试不是无法出错的。

3
为什么不选 0 0 0 1 1 ? 1970?Quartz 会拒绝它吗? - Arnaud Denoyelle
5
在使用Arnaud的建议时,我的Grails应用程序中出现了Quartz 2.1.0抛出"根据配置的计划,给定的触发器永远不会被触发"的SchedulerException,并且启动失败。 - hippofluff
3
不用担心,感谢更新答案!+1 :) 顺便提一下,未来日期允许的时间范围是100年。任何计划在100年后结束的事情都将无法安排。不确定为什么他们会选择100年,但如果一个应用程序实际上运行足够长的时间以触发它,我会非常惊讶的。尽管如此,如果这项技术在100年内甚至被知晓,那就好了。 - hippofluff
2
我检查了代码[1],用于年份的魔法值(对我来说2200失败了) (javadoc[2]说最大为2199,教程[3]说最大为2099)。实际的最大值是: public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR)+100;所以今年的最大值是2116年, 明年的最大值是2117年。[1] https://fisheye.terracotta.org/browse/Quartz/trunk/quartz-core/src/main/java/org/quartz/CronExpression.java?r=2426#to257 [2] http://quartz-scheduler.org/api/2.2.1/org/quartz/CronExpression.html [3] http://quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger - DelGurth
1
@Chop 是的,谢谢你提供这个易于使用的工具。我在Terracotta上提出了一个文档错误,因为目前它仍然令人困惑,即使在实际代码中,他们也使用不同的最大年份检查。 - DelGurth
显示剩余4条评论

53
技术上讲,Quartz年份字段的有效值为1970-2099,因此2300不是预期的值。我假设您确实需要这样做,并且您的Quartz版本试图强制执行有效的cron语法(1-31天,1-12个月等等)。
我目前在Rails的Resque-scheduler中使用以下代码,该代码接受经过验证的crontab格式的计划信息,以创建仅手动运行的测试作业:
cron: "0 5 31 2 *"

工作将在清晨2月31日耐心等待再运行。如果要在Quartz crontrigger中使用等效内容,请尝试此行或其变体:
0 0 5 31 2 ?

1
我确实减少了年份,以便Quartz接受我给他的cron表达式。你的解决方案确实非常优雅。谢谢! - Chop
9
哼,终于有机会测试这个提案了。看起来 Quartz 拒绝了这个 cron 表达式,因为它检测到它永远不会执行... - Chop
1
“0 5 31 2 *” 这个在Magento的定时任务中似乎运行良好。 - toon81
这在Quartz中可能不起作用,但它完美地作为一个标准的服务器cron工作,您希望将执行推迟到以后某个日期,当您再次激活它时。 - Michael Yaeger
4
无效的 cron 表达式 "0 0 5 31 2 ?" 导致下一个触发器无限制地运行,因此无法正常工作。 - Frankie Drake
显示剩余3条评论

25

尝试一下这个时间设置: 59 59 23 31 12 ? 2099


你能解释一下使用它而不是0 0 0 1 1 ? 2200的优势吗?我的会在2200年新年前夕触发,而你的只会在一百年前触发,我理解得对吗?如果没有,我认为越远越好,你同意吗? - Chop
11
这是最后一个有效的Quartz表达式,使用您的Quartz拒绝启动工作至少需要Quartz 1.6和Spring 2.5.6SEC3。 - metyl
我查看了文档,你是正确的,这是最后一个有效的表达式。不过,0 0 0 1 1 ? 2200 在最新版本的Quartz中仍然有效。然而,一个更聪明的解决方案已经被提出(请参见我自己答案中的编辑)。 - Chop

21
如果您在使用 @Scheduled(cron="") 表达式时(技术上不是使用quartz,而是spring中很常见的),则无法使用 7 段式表示未来一年的解决方案,但可以使用以下选项:
  • 如果您使用的是 spring 5.1+ (springBoot 2.1+),只需使用 "${your.cron.prop:-} 并且不设置属性以禁用执行 - 参见 @Scheduled。或将属性本身设置为“-”(如果您在使用 yml,则确保使用引号)。
  • 完全禁用具有 @Scheduled 方法的 bean/service,例如通过使用 @ConditionalOnProperty("my.scheduleproperty.active") 注释并且不设置属性(或将其设置为 false

19

嗨,你可以尝试这个方法,它永远不会执行你的调度器,只需在cron中传递-即可。

 @Scheduled(cron = "${schedular.cron.expression}")

schedular.cron.expression=-

1
这实际上是最好的答案。自从Spring 5.2以来,您可以使用“-”作为特殊的cron表达式来禁用触发器。 - Rens Verhage

8

在尝试解决类似的问题 - 禁用cron表达式时,我找到了这个东西,但是遇到了需要一个有效未来的时间表日期的相同问题。

我也因为无法在cron计划中指定一年而遇到了使用7值语法的问题。

所以我使用了这个: 0 0 3 ? 2 MON#5

下次执行的时间是:

  1. 2044年2月29日星期一上午3点
  2. 2072年2月29日星期一上午3点
  3. 2112年2月29日星期一上午3点
  4. 2140年2月29日星期一上午3点
  5. 2168年2月29日星期一上午3点

因此,从实质上讲,它被禁用了。 :)

啊。 不幸的是,这只适用于Quartz调度程序语法 - Spring CronTrigger语法不允许MON#5表示第五个星期一

所以最好的办法是0 0 3 29 2?这只有在2月29日(闰年)的凌晨3点才会执行。


这是我最喜欢的解决方法!!值得一枚徽章 :-) - drizin
1
我猜更好的版本应该是0 0 29 2 mon#1,等待下一个二月二十九日也是该月第一个星期一(剧透警告...)。 - coconup

2
现在,当我在自己的计算机上本地运行此应用程序时,我不希望执行这些作业。有没有一种方法可以编写一个永远不会触发的cron表达式?
如果您想在计算机上禁用调度,有几种方法可以实现。
首先,您可以将Quartz的配置移到基于@Profile的配置中,并在本地不启用此配置文件。如果未激活该配置文件,则Quartz根本不会启动。
另一种方法是将Quartz配置为不自动启动。您可以在dev配置文件中注册BeanPostProcessor来设置SchedulerFactoryBean#setAutoStartup()。虽然这个线程非常旧,但Spring Boot通过注册SchedulerFactoryBeanCustomizer bean提供了一种替代方法来完成同样的事情。

0
我们可以使用@reboot计划,它会在服务器启动时运行一次。我之前提供了@once作为答案,后来意识到这不是一个可用选项,而是我们公司自定义的一个选项,用于Spark作业输入。所以我正在更正我的答案。参考 -> https://ostechnix.com/a-beginners-guide-to-cron-jobs/

根据情况而定,这可能是一个解决方案。我不记得具体细节,但有些情况下无法应用此方法。例如,如果您的计划任务依赖于计算机上不可用的服务,那么这种方法就行不通了。尽管如此,我非常喜欢这个答案。它不像是折腾和绕过工具,而是一个真正的功能。也许您可以添加文档链接和摘录以提供更多信息? - Chop

-1

对于那些天数少于31天的月份,使用31。 因此,0 0 31 2 * 对于二月 或者 0 0 31 5 * 对于五月 0 0 31 6 对于六月 * 0 0 31 9 * 对于九月 0 0 31 11 * 对于十一月

应该这样做以防止cron执行。 这些都是有效的cron表达式,并且可以在https://crontab.guru/#0_0_31_2_*上进行验证。


1
正如您所看到的,这已经被提出,尽管 cron 表达式是有效的,但 Quartz 拒绝了它。不过还是感谢您的想法。 - Chop

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接