概述
Cron表达式是一个字符串,以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,即两种语法格式:
- Seconds Minutes Hours DayofMonth Month DayofWeek Year,即:秒 分 时 天 月 星期 年份
- Seconds Minutes Hours DayofMonth Month DayofWeek
一般情况下,第七个字符Year可省略不写。
除此以外,也有五段表达式的,如crontab,没有秒的概念。绝大多数情况下,都是6个字符,本文讨论的也是6个字符。
知识点:
- 每个字符都允许设置
, - * /
四个特殊字符; - 每个元素可以是一个值(如6),连续区间(
9-12
),间隔时间(8-18/4
)(/
表示每隔4个单位),列表(1,3,5
),*
通配符; - 日期,即第4位还支持
? L W C
四个特殊字符; - 星期,即第6位还支持
? L C #
四个特殊字符,可用3位大写英文字母表示(不常用),即1==SUN
,另外1表示周日; - L:last,表示最后,只能出现在第4和6个字符位。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发;
- W:表示有效工作日(周一到周五),只能出现在第4位,系统将在离指定日期的最近的有效工作日触发事件。例如:在第4位使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。W的最近寻找不会跨过月份;
- LW:两个字符连用,表示在某个月最后一个工作日,即最后一个星期五;
#
:用于确定每个月第几个星期几,只能出现在第6位。如4#2
表示某月的第二个星期三;- 星期和日字段(第4和6位互斥)有冲突,必须指定一个,两者不能同时指定;
*
指任意一天算指定,?
不算指定;不能两者都是*
;结论:这两个符号有且只能有一个必是问号?。
调研
在线工具
很多,因为cron表达式有各种不同的类型,不同类型直接还是有一些细微的差别。
https://www.bejson.com/othertools/cron/
spring scheduling
在spring-context artifact的springframework.scheduling包下面,CronSequenceGenerator
quartz
org.quartz.CronExpression
cron-utils
官网:http://cron-parser.com/
GitHub
https://awesomeopensource.com/project/jmrozanec/cron-utils
https://www.openhub.net/p/cron-utils
maven
<dependency><groupId>com.cronutils</groupId><artifactId>cron-utils</artifactId><version>9.1.5</version>
</dependency>
cron-parser
GitHub
https://suhasjavablog.wordpress.com/2014/04/01/how-to-generate-a-cron-expression-from-a-date-object/
实践
校验cron表达式合法性
参考下面checkValid
方法。
构建cron表达式
如下图所示一个实际需求,需实现定时调度,其中周几、小时、分钟可配置化:
对应到cron表达式里面,也就是第2、3、6位字符需要支持可配置化。
基于cron-utils写的一个工具类;
import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinition;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.parser.CronParser;
import com.google.common.collect.Lists;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;@Component
public class CronUtil {private static final Logger LOGGER = LoggerFactory.getLogger(CronUtil.class);private static final String QUESTION = "?";private static final String ASTERISK = "*";private static final String COMMA = ",";/*** 替换 分钟、小时、日期、星期*/private static final String ORIGINAL_CRON = "0 %s %s %s * %s";/*** 检查cron表达式的合法性** @param cron cron exp* @return true if valid*/public boolean checkValid(String cron) {try {// SPRING应该是使用最广泛的类型,但假若任务调度依赖于xxl-job平台,则需要调整为CronType.QUARTZCronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.SPRING);CronParser parser = new CronParser(cronDefinition);parser.parse(cron);} catch (IllegalArgumentException e) {LOGGER.error(String.format("cron=%s not valid", cron));return false;}return true;}public String buildCron(List<Integer> minutes, List<Integer> hours, List<Integer> weekdays) {String minute;if (minutes.equals(this.getInitMinutes())) {minute = ASTERISK;} else {minute = StringUtils.join(minutes, COMMA);}String hour;if (hours.equals(this.getInitHours())) {hour = ASTERISK;} else {hour = StringUtils.join(hours, COMMA);}String weekday;if (weekdays.equals(this.getInitWeekdays())) {weekday = QUESTION;} else {weekday = StringUtils.join(weekdays, COMMA);}// 重点:星期和日字段冲突,判断周日的前端输入if (weekday.equals(QUESTION)) {return String.format(ORIGINAL_CRON, minute, hour, ASTERISK, weekday);} else {return String.format(ORIGINAL_CRON, minute, hour, QUESTION, weekday);}}/*** 解析db cron expression展示到前端** @param cron cron* @return minutes/hours/weekdays*/public CustomCronField parseCon(String cron) {if (!this.checkValid(cron)) {return null;}List<String> result = Arrays.asList(cron.trim().split(" "));CustomCronField field = new CustomCronField();if (result.get(1).contains(COMMA)) {field.setMinutes(Arrays.stream(result.get(1).split(COMMA)).map(Integer::parseInt).collect(Collectors.toList()));} else if (result.get(1).equals(ASTERISK)) {field.setMinutes(this.getInitMinutes());} else {field.setMinutes(Lists.newArrayList(Integer.parseInt(result.get(1))));}if (result.get(2).contains(COMMA)) {field.setHours(Arrays.stream(result.get(2).split(COMMA)).map(Integer::parseInt).collect(Collectors.toList()));} else if (result.get(2).equals(ASTERISK)) {field.setHours(this.getInitHours());} else {field.setHours(Lists.newArrayList(Integer.parseInt(result.get(2))));}if (result.get(5).contains(COMMA)) {field.setWeekdays(Arrays.stream(result.get(5).split(COMMA)).map(Integer::parseInt).collect(Collectors.toList()));} else if (result.get(5).equals(QUESTION)) {field.setWeekdays(this.getInitWeekdays());} else {field.setWeekdays(Lists.newArrayList(Integer.parseInt(result.get(5))));}return field;}private List<Integer> initArray(Integer num) {List<Integer> result = Lists.newArrayListWithCapacity(num);for (int i = 0; i <= num; i++) {result.add(i);}return result;}private List<Integer> getInitMinutes() {return this.initArray(59);}private List<Integer> getInitHours() {return this.initArray(23);}private List<Integer> getInitWeekdays() {return this.initArray(7).subList(1, 8);}@Datapublic static class CustomCronField {private List<Integer> minutes;private List<Integer> hours;private List<Integer> weekdays;}
}
表达式类型
cron-utils给出的cron表达式类型枚举类
public enum CronType {CRON4J,QUARTZ,UNIX,SPRING;private CronType() {}
}
Spring类型和Quartz类型的区别,在最后一位符号:
而cron表达式的规则里面:第6位,即Day of week ,*
号是包括?
的。
xxl-job平台使用的是QUARTZ类型:
证明:xxl-job使用的是quartz类型:
证明:Spring类型是Quartz类型的超集,即兼容Quartz:
结论:
- 如果开发的功能依赖于xxl-job调度任务,需要明确使用的xxl-job的版本,及使用的cron表达式类型,然后在代码里面写相同的类型;
- 对于其他任何调度系统,一定要先明确其支持的cron表达式类型,否则会出现任务没有执行的情况
Java(Spring)与Java(Quartz)
根据crontab,Java语言有两种,区别:
- Quartz支持7位,第7位可选;
- 第6位,只支持1-7;而Spring支持0-7,0和7都表示sun;
预测cron表达式最近10次执行时间
实现效果预览,类似于xxl-job的这个功能:
截图为公司内部基于xxl-job的二次开发任务调度平台;在xxl-job GitHub源码里面搜了下,没有看到具体的实现代码逻辑。
于是自己基于cron-utils
实现如下:
public static List<String> getExecutionTimeByNum(String cronStr, Integer num) {CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.SPRING));Cron cron = parser.parse(cronStr);ExecutionTime time = ExecutionTime.forCron(cron);ZonedDateTime now = ZonedDateTime.now();ZonedDateTime next = getNext(time, now);List<ZonedDateTime> timeList = new ArrayList<>(num);timeList.add(next);for (int i = 1; i < num; i++) {next = getNext(time, next);timeList.add(next);}DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");List<String> resultList = new ArrayList<>(num);for (ZonedDateTime item : timeList) {String result = item.format(format);resultList.add(result);}return resultList;
}private static ZonedDateTime getNext(ExecutionTime time, ZonedDateTime current) {return time.nextExecution(current).get();
}
在调用方法`getExecutionTimeByNum``前,可以先校验一下合法性。
判断cron是否是按天执行
/*** 判断cron是否是按天执行* 如果按天执行cron需以(* * ?)结尾* @return true 是以* * ?结尾*/
public static Boolean datasetCron(String cron) {return StringUtils.isNotBlank(cron) && cron.matches(".* \\* \\* \\?$");
}// 判断是否按天更新
boolean day = "*".equals(dataset.getCronExp().split(" ")[3]);