不提你可能不知道,spring定时任务的数字星期域不符合常规的cron定义

简介: 大家都知道,使用Spring的定时任务非常的简单方便,只需要在配置类上添加@EnableScheduling注解,同时在定时方法上添加@Scheduled(cron = "* * 1 * * *")便可以设置一个每天1点定时跑的任务

大家都知道,使用Spring的定时任务非常的简单方便,只需要在配置类上添加@EnableScheduling注解,同时在定时方法上添加@Scheduled(cron = "* * 1 * * *")便可以设置一个每天1点定时跑的任务。


当然,本文不是为了介绍Schedule定时器的用法的,这个网上一大堆,就不重复造轮子了。为了说说强哥在使用Spring定时任务遇到的问题,需要先简单介绍下cron表达式:


一个cron表达式有至少6个(也可能7个)有空格分隔的时间元素。 按顺序依次为 秒(0~59) 分钟(0~59) 小时(0~23) 天(月)(0~31,但是你需要考虑你月的天数) 月(0~11) 天(星期)(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT) 年份(1970-2099)


好了,根据上面的解释, 如果要生成一个每个星期一中午12点的定时任务,cron表达式该怎么写呢?答案如下:


0 0 12 ? * 2


最后一个2代表的是周一MON的意思,也就是说星期域的设置是从星期天SUN(即数字1)开始的。但是,如果你将这个表达式放入到Spring的@Scheduled注解中,你会发现,定时器并没有在星期一的中午执行任务,而是在下一天也就是每个月的星期二中午12点执行任务。


也就是说,在Spring中,执行定时任务时,星期这一字段如果是以数字表示的话,会比常规的cron表达式晚一天。


其实这种情况不光是在@Scheduled注解中,如果你在代码中直接使用Spring的CronTask类,传递cron表达式的时候同样会有这种问题。这到底是是为什么呢?


让我们从Spring源码着手来看看原因。查看CronTask类的源码,我们最常使用的是如下构造器:


public CronTask(Runnable runnable, String expression) {    this(runnable, new CronTrigger(expression));  }


关键则在于生成CronTrigger,那么我们继续查看CronTrigger的源码,会看到这样的代码:


public CronTrigger(String expression) {  this.sequenceGenerator = new CronSequenceGenerator(expression);}


继续查看CronSequenceGenerator


public CronSequenceGenerator(String expression) {    this(expression, TimeZone.getDefault());  }


这里用到了默认的时区,也就是你当前机器上的时区。继续查看this即构造方法:


public CronSequenceGenerator(String expression, TimeZone timeZone) {    this.expression = expression;    this.timeZone = timeZone;    parse(expression);  }


关键来了,parse就是用于转换传入的cron表达式的,继续跟踪:


/**   * Parse the given pattern expression.   */  private void parse(String expression) throws IllegalArgumentException {    String[] fields = StringUtils.tokenizeToStringArray(expression, " ");    if (!areValidCronFields(fields)) {      throw new IllegalArgumentException(String.format(          "Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression));    }    doParse(fields);  }


同样进入关键代码doParse(fields)


private void doParse(String[] fields) {    setNumberHits(this.seconds, fields[0], 0, 60);    setNumberHits(this.minutes, fields[1], 0, 60);    setNumberHits(this.hours, fields[2], 0, 24);    setDaysOfMonth(this.daysOfMonth, fields[3]);    setMonths(this.months, fields[4]);    setDays(this.daysOfWeek, replaceOrdinals(fields[5], "SUN,MON,TUE,WED,THU,FRI,SAT"), 8);
    if (this.daysOfWeek.get(7)) {      // Sunday can be represented as 0 or 7      this.daysOfWeek.set(0);      this.daysOfWeek.clear(7);    }  }


而setDay代码如下:


private void setDays(BitSet bits, String field, int max) {    if (field.contains("?")) {      field = "*";    }    setNumberHits(bits, field, 0, max);  }


从这两段代码便可以看出为什么Spring的星期用数字表达的话会不符合常规的cron表达式的端倪了。


对于星期,先将该域的英文缩写SUN-SAT替换成对应的数字(0-6),接着将该域中的字符"?"替换成"*",然后使用基础解析算法处理。最后,由于周日对应的值有两个0和7,因此对daysOfWeek位数组的第0位和第7位取或,将结果保存到第0位,并清除第7位。


也就是说SUM在这里其实代表的是0,而不是cron表达式中的1,其他星期也依次类推,也就是比常规的cron表达式小1,从代码中的注释也可以看出星期设置的不同:


// Sunday can be represented as 0 or 7this.daysOfWeek.set(0);


好吧,这个就是为什么我们将cron表达式用于Spring定时器的时候,用数字表示星期会出现问题的原因,而@Scheduled注解传入的cron表达式最后也是用到CronSequenceGenerator进行处理。


可是网上写Spring定时器的文章中,都是提cron表达式的解释,却很少能发现对这个问题的说明。强哥看到了许多文章,就只有一篇中有提到关于星期问题的描述,如下:


注:第六位(星期几)中的数字可能表达不太正确,可以使用英文缩写来表示,如:Sun


但是原因并没有说明出来。不过,也正如上面说提到的,既然数字容易产生混乱和错误,那么我们最好就是使用英文缩写进行处理cron表达式中的日期,强哥也亲自试验过,英文缩写的方式是不会有上述的少一天的问题的。


不清楚为什么Spring代码设计人员要以这种方式来处理定时器中的星期字段,极易引发错误。不过可能与Linux的计划任务Cron有关,因为在Crontab中,星期域也是从0~7,0和7均为星期天,同时Crontab中的cron表达式没有秒域,以五个或六个数字来表示。(看到这些各种不一的用法是不是心里千万只草拟马在崩腾)


所以,大家如果有遇到在Spring中使用cron表达式做定时任务的话,最好使用英文缩写来处理星期域,这样能够保证星期不会出错。

相关文章
|
2月前
|
Java 数据库 Spring
Spring Boot 实现定时任务的动态增删启停
Spring Boot 实现定时任务的动态增删启停
29 0
|
2月前
|
druid Java 数据库
Spring Boot的定时任务与异步任务
Spring Boot的定时任务与异步任务
|
5月前
|
Java Spring 容器
Spring注解开发定义bean及纯注解开发模式
Spring注解开发定义bean及纯注解开发模式
35 0
|
4月前
|
Java 调度 Maven
Spring Task 自定义定时任务类
Spring Task 自定义定时任务类
36 0
|
10天前
|
Java 开发者 Spring
Spring AOP的切点是通过使用AspectJ的切点表达式语言来定义的。
【5月更文挑战第1天】Spring AOP的切点是通过使用AspectJ的切点表达式语言来定义的。
23 5
|
10天前
|
Java 调度 Maven
Springboot实战篇--Springboot框架通过@Scheduled实现定时任务
Spring Boot的Scheduled定时任务无需额外Maven依赖,通过`@EnableScheduling`开启。任务调度有两种方式:fixedRate和fixedDelay,前者任务结束后立即按设定间隔执行,后者在任务完成后等待设定时间再执行。更灵活的是cron表达式,例如`0 0 3 * * ?`表示每天3点执行。实现定时任务时,需注意默认单线程执行可能导致的任务交错,可通过自定义线程池解决。
|
11天前
|
XML Java 数据格式
如何在Spring AOP中定义和应用通知?
【4月更文挑战第30天】如何在Spring AOP中定义和应用通知?
16 0
|
11天前
|
消息中间件 安全 Java
在Spring Bean中,如何通过Java配置类定义Bean?
【4月更文挑战第30天】在Spring Bean中,如何通过Java配置类定义Bean?
20 1
|
14天前
|
前端开发 Java 数据格式
【Spring系列笔记】定义Bean的方式
在Spring Boot应用程序中,定义Bean是非常常见的操作,它是构建应用程序的基础。Spring Boot提供了多种方式来定义Bean,每种方式都有其适用的场景和优势。
31 2
|
16天前
|
XML Java 数据格式
Spring Bean的定义(含创建Bean的三种方式)
Spring Bean的定义(含创建Bean的三种方式)