cron表达式的双重人格:星期和数字到底如何对应?
写在前面
cron在希腊语中是时间的意思,而cron表达式(cron expression)则是遵循特定规则,用于描述定时设置的字符串,常用于执行定时任务。本文总结了不同环境(如平台、库等)下,cron expression在用数字表示星期上的区别,并进行实际验证。目的是消除疑惑,还原真相,以免读者在开发中“踩坑”。本文不详述cron含义及cron expression规范,有兴趣的读者请参考[1],[2],[3]和[4]。想直奔结论的读者请直接跳至结论。同时,建议您阅读建议小节。
正文
现象
在下刚接触cron expression,发现对于cron expression中如何用数字表示星期,网上流传着两个不同版本。版本1:1-6分别表示星期一-星期六,而0和7都可以表示星期日,如[2];版本2:1表示星期日,2表示星期一,……,7表示星期六,如[3]和[4]。
结论
到底哪个对呢?经验证,都对。于是,得出结论如下:
cron expression是“人格分裂者”:
- 正常人格(版本1):1-6分别表示星期一-星期六,而0和7都可以表示星期日。存在于:类Unix系统中的定时任务管理服务cron,Golang的定时任务库cron,Spring-Task等。
- 别扭人格(版本2):1-7分别表示星期日-星期六,即除1表示星期日外,其余都是数字n表示星期n-1。存在于:Quartz、Oracle Role Manager等。
验证
【必读】
- 本文对Ubuntu下cron服务,Golang的cron库,Spring-Task,以及Quartz进行了验证,其中,Ubuntu版本为20.04 LTS,cron库版本为3.0.1,Spring版本为5.2.7,Quartz版本为2.3.0。
- 验证2、3、4所需材料(包括源码、库、配置文件、编译运行脚本等,以下简称“验证材料”)已上传到这里(提取码: 4pkz),有兴趣的读者下载即可一键运行,只要您已安装并正确配置Java和Golang,并保证网络畅通(Golang的cron库需要在线下载)。您无需准备Spring库和Quartz库,它们已包含于在下为您准备的验证材料中。您还需要根据您执行代码的时间修改代码中的cron expression。
- 本文不会讨论各代码段的技术细节,给出代码仅仅是为了验证本文结论。
- Oracle Role Manager不知如何验证,暂不验证。这里恳请知道的读者不吝赐教。
验证1 Ubuntu下cron服务
注意,截图中涉及私人信息的部分已码掉。验证1的操作可参考这里,这里和这里。
step1 准备工作如下图:
step2 如上图最后一步所示,使用 crontab -e 命令打开配置文件编辑窗口,添加 * * * * 4 date >> /tmp/test_cron.txt 一行,意思是每逢星期四,就以每隔1分钟的频率将 date 命令执行结果以追加方式写入 /tmp/test_cron.txt 。如下图:
step3 结果如下:
验证2 Golang的cron库
代码如下:
1 package main 2 3 import ( 4 "fmt" 5 "github.com/robfig/cron/v3" 6 "time" 7 ) 8 9 func main() { 10 // 开启秒字段支持 11 c := cron.New(cron.WithSeconds()) 12 // 含义是: 每逢周二,就每秒执行一次 13 _, _ = c.AddFunc("0/1 * * * * 2", func() { 14 fmt.Println(time.Now().Format(time.ANSIC)) 15 }) 16 // 启动 17 c.Start() 18 // 防止程序直接退出 19 time.Sleep(time.Second * 3) 20 }
结果如下:
验证3 Spring-Task
Spring实现定时任务有两种方式,xml方式和注解方式,下面给出注解方式代码,xml实现包含于验证材料中。共分为三个文件:
1 import org.springframework.scheduling.annotation.Scheduled; 2 import java.util.Date; 3 4 public class MyTask //定义要执行的任务(函数) 5 { 6 //含有cron expression的注解,因为是周一进行的实验,所以条件改为“每逢周一,就每秒执行一次” 7 @Scheduled(cron = "0/1 * * ? * 1") 8 public void show() 9 { 10 System.out.printf("%tc%n",new Date()); 11 } 12 }
注意,上面的cron expression中,'?'不能改为'*'。'?'意味着“忽略,不考虑该维度”;'*'意味着“任意,所有”。如果将'?'改为'*',Spring会认为发生了“每天”和“每周一”的冲突。
下面的代码大体上相当于配置文件,没仔细研究过。
1 import java.util.concurrent.Executor; 2 import java.util.concurrent.Executors; 3 import org.springframework.context.annotation.Bean; 4 import org.springframework.context.annotation.Configuration; 5 import org.springframework.scheduling.annotation.EnableScheduling; 6 import org.springframework.scheduling.annotation.SchedulingConfigurer; 7 import org.springframework.scheduling.config.ScheduledTaskRegistrar; 8 9 @Configuration 10 @EnableScheduling 11 public class Config implements SchedulingConfigurer 12 { 13 14 @Bean 15 public MyTask bean() 16 { 17 return new MyTask(); 18 } 19 20 @Override 21 public void configureTasks(ScheduledTaskRegistrar taskRegistrar) 22 { 23 taskRegistrar.setScheduler(taskExecutor()); 24 } 25 26 @Bean(destroyMethod="shutdown") 27 public Executor taskExecutor() 28 { 29 return Executors.newScheduledThreadPool(2); 30 } 31 }
最后是测试文件:
1 import org.springframework.context.annotation.AnnotationConfigApplicationContext; 2 import org.springframework.context.support.AbstractApplicationContext; 3 4 public class Test 5 { 6 public static void main(String[] args) 7 { 8 AbstractApplicationContext context = new AnnotationConfigApplicationContext(Config.class); 9 try 10 { 11 Thread.sleep(3000); 12 } 13 catch (InterruptedException e) 14 { 15 e.printStackTrace(); 16 } 17 System.exit(0); 18 } 19 }
结果如下:
验证4 Quartz
代码如下,分两个文件:
首先,编写定时任务:
1 import org.quartz.Job; 2 import org.quartz.JobExecutionContext; 3 import org.quartz.JobExecutionException; 4 import java.util.Date; 5 6 public class MyJob implements Job 7 { 8 public void execute(final JobExecutionContext jobExecutionContext) throws JobExecutionException 9 { 10 System.out.printf("%tc%n",new Date()); 11 } 12 }
其次,调用定时任务:
1 import static org.quartz.JobBuilder.newJob; 2 import static org.quartz.SimpleScheduleBuilder.simpleSchedule; 3 import static org.quartz.TriggerBuilder.newTrigger; 4 import static org.quartz.TriggerBuilder.*; 5 import static org.quartz.CronScheduleBuilder.*; 6 import static org.quartz.DateBuilder.*; 7 import org.quartz.JobDetail; 8 import org.quartz.Scheduler; 9 import org.quartz.SchedulerException; 10 import org.quartz.Trigger; 11 import org.quartz.impl.StdSchedulerFactory; 12 13 public class Test 14 { 15 public static void main(String[] args) 16 { 17 try { 18 Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); 19 20 JobDetail job = newJob(MyJob.class) .withIdentity("job1", "group1") .build(); 21 22 Trigger trigger = newTrigger() 23 .withIdentity("trigger1", "group1") 24 .startNow() 25 .withSchedule( 26 //注意与上面各cron expression不同,这里用2表示星期一 27 cronSchedule("0/1 * * ? * 2") 28 ).build(); 29 30 scheduler.scheduleJob(job, trigger); 31 32 scheduler.start(); 33 34 Thread.sleep(3000); 35 36 scheduler.shutdown(); 37 } 38 catch (SchedulerException e) 39 { 40 e.printStackTrace(); 41 } 42 catch (InterruptedException e) 43 { 44 e.printStackTrace(); 45 } 46 } 47 }
结果如下:
总结:通过实验,发现除Quartz采用版本2外,其余环境均采用版本1。
建议
事实上,不同定时库采用的cron expression都是在最早的Unix cron中的cron expression上加入自己的小改动、小扩展形成的,因此彼此很可能会大同小异,所以给出以下建议:
- 在使用具体的库时最好仔细阅读相应的官方文档。
- 使用星期的英文缩写(如SUN,MON等,一般不区分大小写)代替数字来表示星期,这样既直观又能避免不同环境下数字表示星期的坑。
参考
[1] cron:维基百科中文。
[2] cron & cron expression:维基百科英文
[3] Quartz中的cron expression。
[4] Oracle中的cron expression。
写在后面
遗憾的是,国内许多博客(如这里和这里)都直接采用版本2来介绍,却不指明是在什么样的环境中,这很容易让初学者(比如在下)以为世界上的cron expression只有一种,或者让先学过版本1的人(比如在下)产生深深的疑惑。所以,在下特地写文澄清。不为显摆,实在是不想再有人产生像在下之前那样的困惑。
另外还需说明的一点是,两个版本的cron expression还有其它一些区别,如版本2支持更多的特殊字符(如L、W、#等),且不允许在表示日和星期的位置上同时设置'*'。
由衷感谢本文中所有引文的作者。
本文验证材料可从这里获取,提取码: 4pkz,如链接失效,请您在评论区留言或私信,在下会及时更新。
由于在下才疏学浅,错误疏漏之处在所难免,欢迎读者批评指正,您的批评是在下前进的不竭动力。