spring定时任务的数字星期域不符合常规的cron定义

大家都知道,使用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 7
this.daysOfWeek.set(0);

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

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


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

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

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

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

关注公众号获取更多内容,有问题也可在公众号提问哦:

 

强哥叨逼叨

叨逼叨编程、互联网的见解和新鲜事

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值