Cron 表达式反解析

2024年11月25日 Cron 表达式反解析 极客笔记

Cron 表达式反解析

Cron 表达式是一种标准的时间表达式,用来描述定时任务的执行时间,通常被用于批处理、系统定时任务等场景中。由于其简洁、灵活的特性,Cron 表达式已经成为了定时任务时间设定的事实标准,深受开发者的喜爱和使用。

然而,在某些情况下,我们需要根据 Cron 表达式来计算下一次的执行时间、反解析 Cron 表达式以确定它具体表示的时间,这时候就需要使用 Cron 表达式反解析的技巧了。在本文中,我们将会全面介绍 Cron 表达式反解析的原理和相关实现技巧。

Cron 表达式语法

在了解 Cron 表达式反解析的原理之前,我们先来回顾一下 Cron 表达式的基本语法:

*    *    *    *    *    *    *
┬    ┬    ┬    ┬    ┬    ┬    ┬
│    │    │    │    │    │    │
│    │    │    │    │    │    └─年份(可选)
│    │    │    │    │    └───星期(0-6,0 表示星期日,可用英文缩写表示,也可以使用英文单词)
│    │    │    │    └────月份(1-12 或者 JAN-DEC,可用英文缩写表示,也可以使用英文单词)
│    │    │    └────────日(1-31)
│    │    └─────────────小时(0-23)
│    └──────────────────分(0-59)
└───────────────────────秒(0-59)

如上,它有 7 个字段,分别表示秒、分、小时、日、月、周、年,每个字段可以由数字、星号、斜杆、连字符、逗号、问号等符号进行组合,以描述特定的时间点或时间段。

比如,0 0 2 ? * 1 表示每周一早上 2 点启动任务,0 0/5 14 * * ? 表示每天下午 2 点到晚上 2 点 55 分,每隔 5 分钟启动任务。

Cron 表达式反解析原理

在进行 Cron 表达式反解析的时候,一般分为两步:

  1. 根据 Cron 表达式计算出指定时间点
  2. 将指定时间点转换成 Cron 表达式

对于第一步,我们可以通过一些开源的工具或者自己编写代码实现。比如,CronSequenceGenerator 这个类就是 Spring 中提供的一个计算 Cron 表达式的工具。

CronSequenceGenerator generator = new CronSequenceGenerator("0 0 2 ? * 1");
Date next = generator.next(new Date());
System.out.println(next);
// 输出:Mon Jun 07 02:00:00 CST 2021

这里通过 CronSequenceGenerator 类,以“0 0 2 ? * 1”为参数创建了一个 Cron 表达式生成器,然后调用 next() 方法计算出下一个满足条件的时间点,并输出了计算结果。

对于第二步,我们需要按照 Cron 表达式的语法规则逐一研究指定的时间点,并找到可以表达这个时间点的最精简表达式。比如,对于指定的时间点 Mon Jun 07 02:00:00 CST 2021,我们可以根据 Cron 表达式的语法规则来进行分析:

  1. 根据日期和时间计算出每个字段的数值
   秒:0   分:0
   时:2
   日:无需计算(?)
   月:6
   周:2(星期一)
   年:2021
   ```

2. 根据数值组合出最精简的 Cron 表达式

```bash
   0 0 2 ? * 2
   ```

   这个表达式表示:秒为 0,分为 0,时为 2,日为无需计算(`?`),月为 6,周为 2(星期一),年为 2021。

需要注意的是,反解析 Cron 表达式的过程是不唯一的,同一个时间点可能会有多种表达方式。而且,Cron 表达式并不直观,对于一些时间点,可能会有多个合理的表达方式,我们可以根据实际需求选择最合适的表达方式。

## 代码实现

下面,我们来看一下如何用 Java 代码实现 Cron 表达式反解析的功能。

### 依赖

需要依赖以下两个库:

```groovy
implementation 'com.cronutils:cron-utils:9.1.0'
implementation 'joda-time:joda-time:2.10.12'

根据时间点反解析 Cron 表达式

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import com.cronutils.descriptor.CronDescriptor;
import com.cronutils.model.Cron;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.parser.CronParser;

public class CronExpParser {

    public static String generateCronExpression(String datetimeString, String cronExpressionString) {
        DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
        DateTime dateTime = DateTime.parse(datetimeString, formatter);
        // 转换为 UTC 时间
        DateTime utcDateTime = dateTime.withZone(DateTimeZone.UTC);
        CronDescriptor descriptor = CronDescriptor.instance(Locale.CHINA);
        CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ));
        Cron cron = parser.parse(cronExpressionString);
        String computedExpression = descriptor.describe(cron).replaceAll(" ", "");
        return utcDateTime.toString("ss mm HH dd MM ? yyyy", Locale.CHINA).replaceFirst("\\d{4}$",
                computedExpression.substring(computedExpression.lastIndexOf('/') + 1));
    }
}

这里 generateCronExpression 是反解析 Cron 表达式的核心方法。它接受两个参数:一个是时间点字符串(格式为 yyyy-MM-dd HH:mm:ss),一个是待反解析的 Cron 表达式字符串。

该方法使用 Joda-Time 库将时间点字符串转换成 DateTime 对象,并把它转换成 UTC 时间。然后使用 cron-utils 库中的 CronParser 类来解析 Cron 表达式,并使用 CronDescriptor 类生成一个描述字符串。最后通过字符串操作拼接出反解析后的 Cron 表达式。

注意,这里我们使用了 Locale.CHINA 来设置语言环境,在生成描述字符串时更符合中文语境。

指定时区的实现

上面的实现默认将时间转换为 UTC 时间进行计算。如果需要指定时区来计算,我们只需要传入时区信息即可:

public static String generateCronExpressionWithTimeZone(String datetimeString, String cronExpressionString,
        String timeZoneString) {
    DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss").withZone(DateTimeZone.UTC);
    DateTime dateTime = formatter.parseDateTime(datetimeString);
    DateTimeZone timeZone = DateTimeZone.forID(timeZoneString);
    DateTimeZone.setDefault(timeZone);
    CronDescriptor descriptor = CronDescriptor.instance(Locale.CHINA);
    CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ));
    Cron cron = parser.parse(cronExpressionString);
    String computedExpression = descriptor.describe(cron).replaceAll(" ", "");
    return dateTime.withZone(timeZone).toString("ss mm HH dd MM ? yyyy", Locale.CHINA).replaceFirst("\\d{4}$",
            computedExpression.substring(computedExpression.lastIndexOf('/') + 1));
}

在这个方法中,我们新增了一个 `timeZoneString` 参数来指定时间所在的时区。我们使用`DateTimeZone` 类将时间转换为指定时区,在生成 Cron 表达式之前通过 `DateTimeZone.setDefault()` 将默认时区设置为指定时区。

### 根据 Cron 表达式计算下一次执行时间

```java
public static String getNextExecutionTime(String cronExpressionString, String datetimeString) {
    DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss").withZone(DateTimeZone.UTC);
    DateTime dateTime = formatter.parseDateTime(datetimeString);
    CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ));
    ZonedDateTime zonedDateTime = dateTime.toDateTime(DateTimeZone.UTC).toInstant().atZone(ZoneId.systemDefault());
    ZonedDateTime nextZonedDateTime = ExecutionTime.forCron(parser.parse(cronExpressionString)).nextExecution(zonedDateTime).get();
    return nextZonedDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

代码中的 getNextExecutionTime 方法用于计算下一次执行时间。它接受两个参数,一个是 Cron 表达式字符串,一个是时间点字符串。该方法先将时间点字符串转换成 DateTime 对象再转换成 ZonedDateTime 对象,然后使用 cron-utils 库中的 ExecutionTime 类来计算出下一次执行的时间点。

计算出的时间点是一个 ZonedDateTime 对象,可以通过在该对象上应用格式化来获取它的字符串表示形式。

结论

Cron 表达式反解析是一项常见的开发任务,在实际项目中常常会用到。本文介绍了 Cron 表达式的语法规则和反解析原理,并提供了 Java 代码实现。我们可以根据自己的需求及具体场景选择合适的实现方式,便于实现更加精准、灵活的定时任务。

本文链接:http://so.lmcjl.com/news/18459/

展开阅读全文