1、概述
Cron 表达式是一个用于配置定时任务的字符串,它由六个或七个空格分隔的字段组成,分别表示时间的不同单位。Cron 表达式通常用于作业调度系统中,如 Unix/Linux 系统的 cron 守护进程,以及一些现代的作业调度框架,如 Quartz 和 Spring Scheduler等。
例如:* * * * * *
每个星号 () 代表一个时间单位,从左到右依次表示:
秒(Seconds):0-59
分钟(Minutes):0-59
小时(Hours):0-23
日期(Day of Month):1-31
月份(Month):1-12 或 JAN-DEC
星期几(Day of Week):0-7(0 和 7 都代表周日)或 SUN-SAT
年份(Year):可选字段,1970-2099
除了使用星号 () 表示所有可能的时间单位,Cron 表达式还支持以下特殊字符:
逗号 (,):用于枚举值,例如在分钟字段中使用 5,20,30 表示在每小时的第 5、20、30 分钟触发。
连字符 (-):用于范围,例如在小时字段中使用 5-10 表示从早上 5 点到上午 10 点之间每小时的开始时刻触发。
斜杠 (/):用于指定时间的间隔,例如在分钟字段中使用 0/15 表示每 15 分钟触发一次。
此外,还有一些特殊字符可以用于特定的场景:
问号 (?):用在日期或星期字段中,表示不指定具体的值。
L:用在日期字段中表示该月最后一天,用在星期字段中表示周六(或数字 7)。
W:用在日期字段中,表示最接近指定日期的工作日(周一至周五)。
#:用在星期字段中,用于指定月份中的第几个星期几,例如 6#3 表示该月的第三个星期五。
Cron 表达式提供了一种灵活且强大的方式来定义何时运行作业,使得用户能够根据需要精确地控制任务的触发时间。
2、生成cron表达式方式
有没有想过对 cron 表达式进行检验的。例如:如下截图中输入表达式后,点击 校验定时参数
校验cron表达式的正确性。
1、对于cron表达式可以考虑是否借鉴xxl-job的实现。
2、由运维人员手动输入cron表达式
3、调用第3方插件生成cron表达式
jQuery Cron:这是一个基于 jQuery 的库,可以很容易地集成到网页中,提供一个用户友好的界面来生成 Cron 表达式。
Vue Cron:如果你在使用 Vue.js,那么 Vue Cron 是一个很好的选择,它是一个基于 Vue.js 的 Cron 表达式生成器组件。
Ant Design Vue Cron:这是另一个基于 Vue.js 的 Cron 表达式生成器组件,适用于使用 Ant Design Vue 的项目。
3、cron表达式校验
那么生成的cron表达式生成后需要校验,校验有以下两种方法:
3-1、cron-utils
引入依赖 注意:getHandler、validate两个方法
<!-- https://mvnrepository.com/artifact/com.cronutils/cron-utils -->
<dependency>
<groupId>com.cronutils</groupId>
<artifactId>cron-utils</artifactId>
<version>9.2.1</version>
</dependency>
@Slf4j
@Service
public class TimingStrategyService {
private static final int NEXT_N_TIMES = 5;
private static final List<String> TIPS = Collections.singletonList("It is valid, but has not trigger time list!");
private final Map<TimeExpressionType, TimingStrategyHandler> strategyContainer;
public TimingStrategyService(List<TimingStrategyHandler> timingStrategyHandlers) {
// init
strategyContainer = new EnumMap<>(TimeExpressionType.class);
for (TimingStrategyHandler timingStrategyHandler : timingStrategyHandlers) {
strategyContainer.put(timingStrategyHandler.supportType(), timingStrategyHandler);
}
}
/**
* 计算接下来几次的调度时间
*
* @param timeExpressionType 定时表达式类型
* @param timeExpression 表达式
* @param startTime 起始时间(include)
* @param endTime 结束时间(include)
* @return 调度时间列表
*/
public List<String> calculateNextTriggerTimes(TimeExpressionType timeExpressionType, String timeExpression, Long startTime, Long endTime) {
TimingStrategyHandler timingStrategyHandler = getHandler(timeExpressionType);
List<Long> triggerTimeList = new ArrayList<>(NEXT_N_TIMES);
Long nextTriggerTime = System.currentTimeMillis();
do {
nextTriggerTime = timingStrategyHandler.calculateNextTriggerTime(nextTriggerTime, timeExpression, startTime, endTime);
if (nextTriggerTime == null) {
break;
}
triggerTimeList.add(nextTriggerTime);
} while (triggerTimeList.size() < NEXT_N_TIMES);
if (triggerTimeList.isEmpty()) {
return TIPS;
}
return triggerTimeList.stream().map(t -> DateFormatUtils.format(t, OmsConstant.TIME_PATTERN)).collect(Collectors.toList());
}
/**
* 计算下次的调度时间
*
* @param preTriggerTime 上次触发时间(nullable)
* @param timeExpressionType 定时表达式类型
* @param timeExpression 表达式
* @param startTime 起始时间(include)
* @param endTime 结束时间(include)
* @return 下次的调度时间
*/
public Long calculateNextTriggerTime(Long preTriggerTime, TimeExpressionType timeExpressionType, String timeExpression, Long startTime, Long endTime) {
if (preTriggerTime == null || preTriggerTime < System.currentTimeMillis()) {
preTriggerTime = System.currentTimeMillis();
}
return getHandler(timeExpressionType).calculateNextTriggerTime(preTriggerTime, timeExpression, startTime, endTime);
}
/**
* 计算下次的调度时间并检查校验规则
*
* @param timeExpressionType 定时表达式类型
* @param timeExpression 表达式
* @param startTime 起始时间(include)
* @param endTime 结束时间(include)
* @return 下次的调度时间
*/
public Long calculateNextTriggerTimeWithInspection( TimeExpressionType timeExpressionType, String timeExpression, Long startTime, Long endTime) {
Long nextTriggerTime = calculateNextTriggerTime(null, timeExpressionType, timeExpression, startTime, endTime);
if (TimeExpressionType.INSPECT_TYPES.contains(timeExpressionType.getV()) && nextTriggerTime == null) {
throw new PowerJobException("time expression is out of date: " + timeExpression);
}
return nextTriggerTime;
}
public void validate(TimeExpressionType timeExpressionType, String timeExpression, Long startTime, Long endTime) {
if (endTime != null) {
if (endTime <= System.currentTimeMillis()) {
throw new PowerJobException("lifecycle is out of date!");
}
if (startTime != null && startTime > endTime) {
throw new PowerJobException("lifecycle is invalid! start time must earlier then end time.");
}
}
getHandler(timeExpressionType).validate(timeExpression);
}
private TimingStrategyHandler getHandler(TimeExpressionType timeExpressionType) {
TimingStrategyHandler timingStrategyHandler = strategyContainer.get(timeExpressionType);
if (timingStrategyHandler == null) {
throw new PowerJobException("No matching TimingStrategyHandler for this TimeExpressionType:" + timeExpressionType);
}
return timingStrategyHandler;
}
}
校验控制器
注意:RequiredArgsConstructor注解
@RequiredArgsConstructor 是 Lombok 库中的一个注解,用于自动生成构造函数。这个注解的作用是在编译时自动生成一个构造函数,该构造函数只包含那些以 final 修饰或者以 @NonNull 注解的未初始化的字段。这样,Lombok 会为你创建一个构造函数,这些字段将成为构造函数的参数,并在构造函数内部被初始化。
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class ExampleService {
private final AnotherService anotherService;
private final SomeRepository someRepository;
// … 方法实现
}
在这个例子中,ExampleService 类使用了 @RequiredArgsConstructor 注解,它有一个 final 字段 anotherService 和 someRepository。Spring 会自动识别这个构造函数,并在创建 ExampleService 的实例时自动注入 AnotherService 和 SomeRepository 的实例。
请注意,为了使自动注入工作,AnotherService 和 SomeRepository 也应该是 Spring 管理的 beans,通常通过在它们各自的类上使用 @Service、@Repository 或 @Component 等注解来标记。
使用 Lombok 的 @RequiredArgsConstructor 可以让您的代码更加简洁,避免了手动编写构造函数和注解每个构造参数的麻烦。然而,您应该谨慎使用 Lombok,确保团队成员都了解其工作原理,并且在项目中一致使用,以避免潜在的维护问题。
import com.google.common.collect.Lists;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import tech.powerjob.common.enums.TimeExpressionType;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.server.core.scheduler.TimingStrategyService;
import java.util.List;
@RestController
@RequestMapping("/validate")
@RequiredArgsConstructor
public class ValidateController {
private final TimingStrategyService timingStrategyService;
@GetMapping("/timeExpression")
public ResultDTO<List<String>> checkTimeExpression(TimeExpressionType timeExpressionType,
String timeExpression,
@RequestParam(required = false) Long startTime,
@RequestParam(required = false) Long endTime
) {
try {
timingStrategyService.validate(timeExpressionType, timeExpression, startTime, endTime);
return ResultDTO.success(timingStrategyService.calculateNextTriggerTimes(timeExpressionType, timeExpression, startTime, endTime));
} catch (Exception e) {
return ResultDTO.success(Lists.newArrayList(ExceptionUtils.getMessage(e)));
}
}
}
响应类ResultDTO
@Getter
@Setter
@ToString
public class ResultDTO<T> implements PowerSerializable {
private boolean success;
private T data;
private String message;
public static <T> ResultDTO<T> success(T data) {
ResultDTO<T> r = new ResultDTO<>();
r.success = true;
r.data = data;
return r;
}
public static <T> ResultDTO<T> failed(String message) {
ResultDTO<T> r = new ResultDTO<>();
r.success = false;
r.message = message;
return r;
}
public static <T> ResultDTO<T> failed(Throwable t) {
return failed(ExceptionUtils.getStackTrace(t));
}
}
3-2、quartz
引入依赖
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
CornValidatorUtil.java
import org.quartz.CronExpression;
import java.text.ParseException;
public class CornValidatorUtil {
public static String validateCornExpression(String cornExpression) {
try {
// 尝试解析Cron表达式
new CronExpression(cornExpression);
return null; // 如果没有异常抛出,则表达式有效
} catch (ParseException e) {
// 解析失败,返回错误信息
return e.getMessage();
}
}
// public static void main(String[] args) {
// String cornExpression = "0 0 0/1 0 0 ? ";
// String errorMessage = validateCornExpression(cornExpression);
// if (errorMessage != null) {
// System.out.println("Cron表达式无效: " + errorMessage);
// } else {
// System.out.println("Cron表达式有效");
// }
// }
}