在 Spring Boot 项目里玩定时任务这事,真不是什么新鲜玩意儿了。大多数人一开始都是这么搞的:@Scheduled(cron = "..."),然后 cron 表达式直接写死在注解里,或者干脆放在配置文件里,启动时读取。
这么写没啥问题,任务能跑就行,但只适合那种“写死就完事”的场景。比如每天凌晨三点执行一个清理脚本,或者每小时跑一遍日志归档,这种频率不怎么改动的任务,固定配置确实很香。
但问题也随之而来:一旦你想在项目运行时修改任务的执行频率,比如从每 10 秒执行一次改成每 15 秒,咋办?重新改配置?重启服务?太原始了,哥!
那有没有一种办法,既能在项目启动时设置默认频率,又能在运行中随时动态修改,还不用重启?有的,今天咱就来搞定这事儿!
最普通的写法,问题在哪?
先说说常规操作,我们平时一般是这么写:
@Scheduled(cron = "0/10 * * * * ?")
public void task() {
log.info("当前时间:" + LocalDateTime.now());
}
这种写法有什么问题?一旦 cron 需要变更,比如业务方说:
“现在流量变大了,任务不要 10 秒跑一次了,隔 30 秒跑一遍就行。”
你改了 cron,再怎么也得重启服务吧?这在很多生产环境下是完全不允许的,尤其是一些对可用性要求很高的系统,重启就是原罪。
所以,我们得搞一个可以“动态修改执行频率”的定时任务。
思路:自己管理调度器,让 cron 可修改
Spring Boot 本身提供了接口SchedulingConfigurer,通过实现它可以动态注册任务。
我们不直接用@Scheduled注解了,而是使用ScheduledTaskRegistrar来手动注册任务,并配合CronTrigger控制执行频率。
这个接口长这样:
public interface SchedulingConfigurer {
void configureTasks(ScheduledTaskRegistrar taskRegistrar);
}
在configureTasks这个方法里,你可以随心所欲地指定任务内容、触发器类型、执行频率等等。比如下面这样,我们从配置文件里读取一个 cron 表达式:
@Component
@PropertySource("classpath:/task-config.ini")
@Slf4j
publicclass ScheduleTask implements SchedulingConfigurer {
@Value("${printTime.cron}")
private String cron;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(
() -> log.info("当前时间:{}", LocalDateTime.now()),
triggerContext -> {
CronTrigger cronTrigger = new CronTrigger(cron);
return cronTrigger.nextExecutionTime(triggerContext);
});
}
}
看清楚了没?关键是这个CronTrigger,它会在每次任务调度前根据当前的 cron 表达式来重新计算下一次的执行时间。也就是说,只要这个 cron 是个变量,你随时改它,任务执行频率就跟着变了。
那怎么改这个 cron 呢?来个接口啊!
说到这,不就是一个 set 方法的事嘛?写个 controller 让前端传个 cron 表达式,set 进去不就完了?
@RestController
@RequestMapping("/test")
publicclass TestController {
privatefinal ScheduleTask scheduleTask;
@Autowired
public TestController(ScheduleTask scheduleTask) {
this.scheduleTask = scheduleTask;
}
@GetMapping("/updateCron")
public String updateCron(String cron) {
log.info("新 cron 表达式:{}", cron);
scheduleTask.setCron(cron);
return"cron 修改成功!";
}
}
你随便调用这个接口,比如:
GET /test/updateCron?cron=0/15 * * * * ?
然后后台任务就会变成每 15 秒执行一次,立刻生效,根本不用重启,优雅极了!
CronTrigger 的局限:只能精确到秒
不过,这种方案也有它的小缺陷。cron 表达式本质上是基于“时间点”触发的,而不是“时间间隔”触发的。
比如你写0/10 * * * * ?,意思是“每分钟的第 0 秒开始,每隔 10 秒执行一次”。那要是你服务在 00:00:07 启动,它可能要等到 00:00:10 才会第一次触发。
还有个问题是,它不太适合设置大于 59 秒的时间间隔。你要是想每 5 分钟执行一次,还得自己写成0 */5 * * * ?,不够直观。
所以,如果你想更精确地控制间隔时间,比如“启动后每隔 15 秒执行一次”,用 cron 反而不太灵活。
更灵活的选择:PeriodicTrigger
这时候,另一个 Trigger 登场了:PeriodicTrigger。这个触发器不像 cron 表达式那么死板,直接设置时间间隔,单位是毫秒,实用性超强。
我们来看代码:
@Component
@Slf4j
publicclass ScheduleTask implements SchedulingConfigurer {
private Long timer = 10000L; // 默认10秒
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(
() -> log.info("当前时间:{}", LocalDateTime.now()),
triggerContext -> {
PeriodicTrigger periodicTrigger = new PeriodicTrigger(timer);
return periodicTrigger.nextExecutionTime(triggerContext);
});
}
public void setTimer(Long timer) {
this.timer = timer;
}
}
然后 controller 也照着加一个接口:
@GetMapping("/updateTimer")
public String updateTimer(Long timer) {
log.info("新的定时时间间隔:{}", timer);
scheduleTask.setTimer(timer);
return "定时时间修改成功!";
}
你可以这样调用:
GET /test/updateTimer?timer=15000
后台任务立刻就变成 15 秒执行一次,配合日志一看,非常清楚。
到这一步,基本就搞定了
这套方案说白了,就是:不要用写死的注解,自己注册任务 + 自己控制调度器 + 自己开放配置接口,既灵活又可控。
你既可以用 cron 表达式实现复杂的时间点调度,比如“每个月最后一天23:59执行”,也可以用 PeriodicTrigger 实现纯间隔的任务,比如“每隔 20 秒执行一次”。
最后
这类需求其实在业务中还挺常见的,比如:
某个接口调用频率不稳定,需要根据系统压力动态调整任务间隔;
运维要求上线后能随时调整定时任务,而不希望改代码;
想做一个定时任务控制面板,让非技术人员都能操作调度频率。
这时候,如果你还在靠@Scheduled注解加个 cron 写死,真就是时代的眼泪了。
还是那句话:代码就该写得灵活点,自己爽,别人用起来也顺。
最后,我为大家打造了一份deepseek的入门到精通教程,完全免费:https://www.songshuhezi.com/deepseek
领取专属 10元无门槛券
私享最新 技术干货