Java解析cron表达式

tech2024-07-09  63

概述

Cron表达式是一个字符串,以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,即两种语法格式:

Seconds Minutes Hours DayofMonth Month DayofWeek YearSeconds Minutes Hours DayofMonth Month DayofWeek

一般情况下,第七个字符省略不写;除此以外,也有五段表达式的,如crontab,没有秒的概念。绝大多数情况下,都是6个字符,本文讨论的也是6个字符。

知识点:

每个字符都允许设置, - * /四个特殊字符;日期,即第4位还支持? L W C四个特殊字符;星期,即第6位还支持? / L C #四个特殊字符,可用3位大写英文字母表示(不常用),即1==SUN,另外1表示周日;L:表示最后,只能出现在第4和6个字符位。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。W:表示有效工作日(周一到周五),只能出现在第4位,系统将在离指定日期的最近的有效工作日触发事件。例如:在第4位使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。W的最近寻找不会跨过月份 。LW:两个字符连用,表示在某个月最后一个工作日,即最后一个星期五。 #:用于确定每个月第几个星期几,只能出现在第6位。例如在4#2,表示某月的第二个星期三。星期和日字段,有冲突,两者不能同时指定,*指任意一天算指定,?不算指定。这两个符号有且只能有一个必是问号?:

调研

在线工具

https://cron.qqe2.com/ https://www.bejson.com/othertools/cron/ https://www.bejson.com/othertools/cronvalidate/

spring scheduling

在spring-context artifact的springframework.scheduling包下面,CronSequenceGenerator

quartz

org.quartz.CronExpression

cron-utils

官网:http://cron-parser.com/ GitHub GitHub-examples 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.1</version> </dependency>

cron-parser

GitHub https://suhasjavablog.wordpress.com/2014/04/01/how-to-generate-a-cron-expression-from-a-date-object/

实践

如下图所示一个实际需求,需实现定时调度,其中周几、小时、分钟可配置化: 对应到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 { CronDefinition 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); } @Data public static class CustomCronField { private List<Integer> minutes; private List<Integer> hours; private List<Integer> weekdays; } }
最新回复(0)