在后端开发中,定时任务是高频需求------比如每小时同步数据、每天凌晨生成报表、每周清理日志等。而Cron表达式作为定时任务的核心配置方式,看似简单,却常常因为理解偏差导致任务"不听话":明明想每5分钟执行一次,结果变成每天5点执行;想每月8号触发,却因为周几配置冲突导致任务失效。
本文将从Cron表达式的核心原理出发,结合实际开发场景,用通俗的语言拆解每一个细节,搭配可直接运行的代码示例,再分享进阶技巧和避坑指南,让你彻底掌握Cron表达式的使用。
一、Cron表达式是什么?核心结构解析
Cron表达式是一个用于定义定时规则的字符串,通过"时间域"的组合,精确指定任务的执行时间。它由7个连续的域组成,每个域对应一个时间单位,格式如下:
sh
[秒] [分] [时] [日] [月] [周] [年]
其中,"年"域是可选的(大部分框架支持省略,默认覆盖所有年份),日常开发中最常用的是前6个域。每个域都有明确的取值范围和允许的特殊字符,下表是详细说明:
| 时间域 | 取值范围 | 允许的特殊字符 | 核心说明 |
|---|---|---|---|
| 秒(Seconds) | 0~59 | , - * / | 最小时间单位,支持步长、范围等配置 |
| 分(Minutes) | 0~59 | , - * / | 与"秒"域规则一致,控制分钟级触发 |
| 时(Hours) | 0~23 | , - * / | 0代表凌晨0点,23代表晚上11点 |
| 日(DayofMonth) | 1~31(需符合当月天数) | , - * / ? L W C | 与"周"域互斥,需用?表示不关心 |
| 月(Month) | 1~12 或 JAN~DEC(英文缩写) | , - * / | 1对应1月,DEC对应12月 |
| 周(DayofWeek) | 1~7 或 SUN~SAT(英文缩写) | , - * / ? L C # | 1=周日、2=周一...7=周六,与"日"互斥 |
| 年(Year) | 1970~2099 | , - * / | 可选,省略则不限制年份 |
注意:如果配置的数值超出对应域的范围(比如秒设为60),框架会直接抛出调度异常(如SchedulerException),导致定时任务无法启动。
二、核心通配符详解:每个符号都有"专属场景"
Cron表达式的灵活性全靠特殊字符支撑,每个符号都对应特定的使用场景,下面结合"定义+示例"的方式,让你一眼看懂用法:
1. *:"每"------匹配域的任意值
表示"该时间单位的每一个时刻",是最常用的通配符。
- 示例1:
* * * * * ?→ 每秒执行一次(秒域*=每秒,分域*=每分,时域*=每时...) - 示例2:
0 * * * * ?→ 每分钟的第0秒执行(秒域固定0,分域*=每分) - 示例3:
0 0 * * * ?→ 每小时的0分0秒执行(整点触发)
2. ?:"不关心"------仅用于日和周域
因为"日"和"周"是互斥的(比如指定每月8号,就无需关心是周几;指定每周三,就无需关心是几号),所以必须用?表示"不指定该域的值"。
- 示例1:
0 0 0 8 * ?→ 每月8号的0点0分0秒执行(日域=8,周域?=不关心周几) - 示例2:
0 15 10 ? * WED→ 每周三上午10:15执行(周域=WED,日域?=不关心几号)
3. -:"范围"------连续时间区间
表示"从A到B的连续范围",包含起点和终点。
- 示例1:
0 5-20 * * * ?→ 每分钟的第0秒,从5分到20分之间,每秒执行一次(分域5-20=5~20分) - 示例2:
0 0 9-17 * * ?→ 朝九晚五(9点到17点)的每个整点执行
4. /:"步长"------间隔时间触发
格式为"起始值/间隔值",表示"从起始值开始,每隔X个单位执行一次"。
- 示例1:
0 0/5 * * * ?→ 每5分钟执行一次(分域0/5=从0分开始,每5分触发) - 示例2:
0 10/20 * * * ?→ 从10分开始,每20分钟执行一次(10分、30分、50分触发) - 示例3:
5/10 * * * * ?→ 从5秒开始,每10秒执行一次(5秒、15秒、25秒...触发)
5. ,:"枚举"------多个离散时间点
表示"匹配多个指定的值",用逗号分隔。
- 示例1:
0 8,12,35 * * * ?→ 每天8分、12分、35分的第0秒执行 - 示例2:
0 0 10,14,16 * * ?→ 每天10点、14点、16点(上午10点、下午2点、4点)执行
6. L:"最后"------月或周的末尾
仅用于日和周域,代表"最后一个",可搭配数字使用。
- 示例1:
0 15 10 L * ?→ 每月最后一天的上午10:15执行(日域L=当月最后一日) - 示例2:
0 0 0 ? * 5L→ 每月最后一个周四执行(周域5=周四,L=最后一个)
7. W:"工作日"------最近的周一至周五
仅用于日域,且必须跟在具体数字后,表示"离指定日期最近的有效工作日(周一到周五)",不跨月。
- 示例1:
0 0 0 5W * ?→ 若5号是工作日,则5号执行;若5号是周六,4号(周五)执行;若5号是周日,6号(周一)执行 - 示例2:
0 0 0 1W * ?→ 若1号是周六,只能推到2号(周一)执行,不能跨月到上个月
8. LW:"月末工作日"------每月最后一个周五
L和W连用,仅用于日域,表示"当月最后一个工作日(即最后一个周五)"。
- 示例:
0 15 10 LW * ?→ 每月最后一个周五的上午10:15执行
9. #:"第N个周几"------每月固定周次
仅用于周域,格式为"周几#N",表示"每月第N个指定周几"(1=周日、2=周一...7=周六)。
- 示例1:
0 15 10 ? * 2#3→ 每月第三个周一执行(周域2=周一,#3=第三个) - 示例2:
0 0 9 ? * 6#2→ 每月第二个周六的上午9点执行
三、实战代码:Spring Boot整合Cron定时器
理论讲完,直接上可运行的代码!Spring Boot的@Scheduled注解是最常用的定时任务实现方式,下面从环境配置到具体示例,一步到位:
1. 环境准备(Maven依赖)
在pom.xml中添加Spring Boot定时任务依赖(Spring Boot 2.x+版本无需额外导入,核心依赖已包含):
xml
<!-- 核心Spring Boot依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.10</version> <!-- 可根据实际版本调整 -->
</dependency>
2. 启动类开启定时任务
在Spring Boot启动类上添加@EnableScheduling注解,开启定时任务支持:
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling // 开启定时任务
public class CronDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CronDemoApplication.class, args);
}
}
3. 定时任务实现类(含10个实用示例)
创建CronTaskService类,每个方法对应一个常见的定时场景,注解中配置Cron表达式,方法内添加业务逻辑(这里用日志打印模拟):
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
public class CronTaskService {
private static final Logger logger = LoggerFactory.getLogger(CronTaskService.class);
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 示例1:每5分钟执行一次(核心常用场景)
@Scheduled(cron = "0 0/5 * * * ?")
public void taskEvery5Minutes() {
String now = sdf.format(new Date());
logger.info("【每5分钟执行】当前时间:{},执行数据同步任务", now);
// 实际业务逻辑:比如同步数据库数据、调用第三方接口等
}
// 示例2:每天凌晨5点执行(定时报表生成)
@Scheduled(cron = "0 0 5 * * ?")
public void taskEveryDay5AM() {
String now = sdf.format(new Date());
logger.info("【每天5点执行】当前时间:{},生成昨日销售报表", now);
}
// 示例3:每周六凌晨1点执行(日志清理)
@Scheduled(cron = "0 0 1 ? * SAT")
public void taskEverySaturday1AM() {
String now = sdf.format(new Date());
logger.info("【每周六1点执行】当前时间:{},清理30天前的系统日志", now);
}
// 示例4:周一至周五上午10:15执行(工作日提醒)
@Scheduled(cron = "0 15 10 ? * MON-FRI")
public void taskWorkday1015() {
String now = sdf.format(new Date());
logger.info("【工作日10:15执行】当前时间:{},发送今日工作提醒", now);
}
// 示例5:2024年每天上午10:15执行(年度任务)
@Scheduled(cron = "0 15 10 * * ? 2024")
public void task2024EveryDay1015() {
String now = sdf.format(new Date());
logger.info("【2024年每日10:15】当前时间:{},执行年度数据统计", now);
}
// 示例6:每天10点、14点、16点执行(定时检查)
@Scheduled(cron = "0 0 10,14,16 * * ?")
public void taskThreeTimesADay() {
String now = sdf.format(new Date());
logger.info("【每日3次执行】当前时间:{},检查服务健康状态", now);
}
// 示例7:朝九晚五(9-17点)每半小时执行(办公时间任务)
@Scheduled(cron = "0 0/30 9-17 * * ?")
public void taskWorkHourEvery30Min() {
String now = sdf.format(new Date());
logger.info("【工作时间每30分钟】当前时间:{},同步办公系统数据", now);
}
// 示例8:每月最后一个工作日上午10:15执行(月末结算)
@Scheduled(cron = "0 15 10 LW * ?")
public void taskLastWorkdayOfMonth() {
String now = sdf.format(new Date());
logger.info("【月末最后工作日】当前时间:{},执行月度财务结算", now);
}
// 示例9:每月第三个周五上午10:15执行(季度会议提醒)
@Scheduled(cron = "0 15 10 ? * 6#3")
public void taskThirdFridayOfMonth() {
String now = sdf.format(new Date());
logger.info("【每月第三个周五】当前时间:{},发送季度会议提醒", now);
}
// 示例10:每天14:00-14:05期间,每分钟执行一次(短期高频任务)
@Scheduled(cron = "0 0-5 14 * * ?")
public void task1400To1405EveryMin() {
String now = sdf.format(new Date());
logger.info("【14:00-14:05每分钟】当前时间:{},执行短期数据校验", now);
}
}
4. 运行效果
启动Spring Boot应用后,控制台会打印对应的日志,例如:
2024-05-20 10:15:00.001 INFO 12345 --- [ scheduling-1] c.example.crondemo.CronTaskService : 【工作日10:15执行】当前时间:2024-05-20 10:15:00,发送今日工作提醒
2024-05-20 10:30:00.002 INFO 12345 --- [ scheduling-1] c.example.crondemo.CronTaskService : 【工作时间每30分钟】当前时间:2024-05-20 10:30:00,同步办公系统数据
四、常见"踩坑"指南:这些错误千万别犯
1. 步长表达式写错(最高频错误)
- 错误写法:
0 5 * * * ?→ 意为"每天5分0秒执行"(分域=5,不是每5分钟) - 正确写法:
0 0/5 * * * ?→ 分域0/5=从0分开始,每5分钟执行 - 原因:
/的前面是"起始值",不是"间隔值",如果写5/5,会从5分开始,每5分钟执行(5分、10分、15分...)
2. 日和周域同时指定具体值
- 错误写法:
0 0 0 8 * WED→ 日域=8,周域=WED(周三),冲突 - 正确写法:
0 0 0 8 * ?或0 0 0 ? * WED - 原因:框架无法判断"每月8号"和"每周三"哪个优先级高,必须用?忽略其中一个域
3. W通配符跨月问题
- 错误认知:
0 0 0 31W * ?→ 若31号是周六,想跨月到下个月1号(周一)执行 - 实际效果:只能在当月寻找最近工作日,若31号是周六,会在30号(周五)执行,不会跨月
- 原因:W的规则是"不跨月",避免出现跨月执行的不可预期问题
4. 周域数字对应错误
- 错误写法:
0 0 0 ? * 1→ 认为1=周一 - 实际效果:1=周日,7=周六(国际标准)
- 解决:用英文缩写(SUN~SAT)更直观,避免数字混淆
五、进阶拓展:让Cron表达式更灵活
1. 动态修改Cron表达式(无需重启应用)
上面的示例中,Cron表达式是硬编码在注解中的,若需要修改执行时间,必须重启应用。可以通过"配置文件+@Value"或"数据库存储"实现动态配置:
方式1:配置文件读取(支持热更新)
- 在
application.yml中配置:
yaml
cron:
task:
every5Minutes: 0 0/5 * * * ?
daily5AM: 0 0 5 * * ?
- 任务类中读取配置:
java
@Component
public class DynamicCronTaskService {
@Value("${cron.task.every5Minutes}")
private String every5MinutesCron;
// 动态配置的定时任务(需结合SchedulingConfigurer实现热更新,这里简化示例)
@Scheduled(cron = "${cron.task.daily5AM}")
public void dynamicTaskDaily5AM() {
String now = sdf.format(new Date());
logger.info("【动态配置-每日5点】当前时间:{},执行动态任务", now);
}
}
方式2:数据库存储(完全动态)
通过SchedulingConfigurer接口,从数据库读取Cron表达式,支持运行时修改(需配合定时刷新逻辑),核心代码:
java
@Component
public class DatabaseCronConfig implements SchedulingConfigurer {
@Autowired
private CronMapper cronMapper; // 自定义Mapper,查询数据库中的cron表达式
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 从数据库查询表达式(假设表中存储了task_key=every5Minutes的cron值)
String cron = cronMapper.selectCronByKey("every5Minutes");
// 注册任务
taskRegistrar.addCronTask(() -> {
String now = sdf.format(new Date());
logger.info("【数据库动态配置】当前时间:{},执行数据库配置任务", now);
}, cron);
}
}
2. 常用Cron在线工具
- 解析验证:https://cron.qqe2.com/(输入表达式,实时查看执行时间)
- 表达式生成:https://cron-generator.org/(可视化配置,生成对应Cron表达式,适合新手)
- 避坑检查:https://crontab.guru/(校验表达式合法性,提示错误原因)
3. 多框架支持
除了Spring Boot的@Scheduled,以下框架也支持Cron表达式:
- Quartz:分布式定时任务框架,支持复杂调度(如任务依赖、失败重试),Cron表达式用法一致
- Elastic Job:基于Quartz的分布式任务框架,适合微服务场景
- Linux Crontab:系统级定时任务,格式略有差异(Linux Crontab是5个域:分 时 日 月 周)
六、总结
Cron表达式的核心是"7个时间域+9个通配符",掌握每个符号的场景化用法,再结合实际代码练习,就能避免大部分踩坑。日常开发中,优先使用Spring Boot的@Scheduled注解实现简单定时任务,复杂场景(分布式、动态配置)可选用Quartz或Elastic Job。
记住3个关键原则:
- 日和周域互斥,必须用?忽略一个;
- /表示"起始+步长",不是单纯的间隔;
- 不确定的表达式,先用在线工具验证再上线。
通过本文的讲解和示例,相信你已经能熟练运用Cron表达式处理各类定时任务需求。如果遇到复杂场景,不妨结合动态配置或分布式框架,让定时任务更灵活、更可靠。