SpringBoot運(yùn)行時(shí)修改定時(shí)任務(wù)Cron表達(dá)式的實(shí)現(xiàn)方案
一、引言
在項(xiàng)目開(kāi)發(fā)中,定時(shí)任務(wù)是一個(gè)常見(jiàn)的需求。Spring Boot 通過(guò) @Scheduled 注解提供了簡(jiǎn)便的定時(shí)任務(wù)實(shí)現(xiàn)方式,但默認(rèn)情況下,一旦應(yīng)用啟動(dòng),定時(shí)任務(wù)的 Cron 表達(dá)式就無(wú)法動(dòng)態(tài)調(diào)整。
然而,在實(shí)際業(yè)務(wù)場(chǎng)景中,我們常常需要根據(jù)運(yùn)行狀態(tài)或用戶(hù)配置動(dòng)態(tài)調(diào)整定時(shí)任務(wù)的執(zhí)行頻率。
本文將介紹如何在 Spring Boot 應(yīng)用運(yùn)行期間動(dòng)態(tài)修改定時(shí)任務(wù)的 Cron 表達(dá)式,實(shí)現(xiàn)定時(shí)任務(wù)的靈活調(diào)度。
二、Spring 定時(shí)任務(wù)機(jī)制概述
2.1 Spring 定時(shí)任務(wù)的實(shí)現(xiàn)方式
Spring Boot 支持三種定時(shí)任務(wù)的實(shí)現(xiàn)方式:
@Scheduled 注解:最簡(jiǎn)單的方式,直接在方法上添加注解 SchedulingConfigurer 接口:通過(guò)實(shí)現(xiàn)該接口,可以進(jìn)行更靈活的配置 TaskScheduler 接口:最底層的 API,提供最大的靈活性
傳統(tǒng)的 @Scheduled 注解使用示例:
@Component
public class ScheduledTasks {
@Scheduled(cron = "0 0/5 * * * ?") // 每5分鐘執(zhí)行一次
public void executeTask() {
System.out.println("定時(shí)任務(wù)執(zhí)行,時(shí)間:" + new Date());
}
}
2.2 @Scheduled 注解的局限性
雖然 @Scheduled 注解使用簡(jiǎn)便,但它存在明顯的局限性:
- Cron 表達(dá)式在編譯時(shí)就確定,運(yùn)行時(shí)無(wú)法修改
- 無(wú)法根據(jù)條件動(dòng)態(tài)啟用或禁用定時(shí)任務(wù)
- 無(wú)法在運(yùn)行時(shí)動(dòng)態(tài)添加新的定時(shí)任務(wù)
- 無(wú)法獲取定時(shí)任務(wù)的執(zhí)行狀態(tài)
這些局限使得 @Scheduled 注解不適合需要?jiǎng)討B(tài)調(diào)整的場(chǎng)景。
三、動(dòng)態(tài)定時(shí)任務(wù)的實(shí)現(xiàn)方案
要實(shí)現(xiàn)定時(shí)任務(wù)的動(dòng)態(tài)調(diào)整,我們有幾種可行的方案:
3.1 基于 SchedulingConfigurer 接口的實(shí)現(xiàn)
這是最常用的方式,通過(guò)實(shí)現(xiàn) SchedulingConfigurer 接口,我們可以獲取 TaskScheduler 和 ScheduledTaskRegistrar,從而實(shí)現(xiàn)定時(shí)任務(wù)的動(dòng)態(tài)管理。
以下是基本實(shí)現(xiàn)步驟:
步驟 1:創(chuàng)建任務(wù)配置類(lèi)
package com.example.scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import java.util.Date;
@Configuration
@EnableScheduling
public class DynamicScheduleConfig implements SchedulingConfigurer {
@Autowired
private CronRepository cronRepository; // 用于存儲(chǔ)和獲取Cron表達(dá)式
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskScheduler());
// 從數(shù)據(jù)庫(kù)或配置中心獲取初始Cron表達(dá)式
String initialCron = cronRepository.getCronByTaskName("sampleTask");
// 添加可動(dòng)態(tài)修改的定時(shí)任務(wù)
taskRegistrar.addTriggerTask(
// 定時(shí)任務(wù)的執(zhí)行邏輯
() -> {
System.out.println("動(dòng)態(tài)定時(shí)任務(wù)執(zhí)行,時(shí)間:" + new Date());
},
// 定時(shí)任務(wù)觸發(fā)器,可根據(jù)需要返回下次執(zhí)行時(shí)間
triggerContext -> {
// 每次任務(wù)觸發(fā)時(shí),重新獲取Cron表達(dá)式
String cron = cronRepository.getCronByTaskName("sampleTask");
CronTrigger trigger = new CronTrigger(cron);
return trigger.nextExecution(triggerContext);
}
);
}
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 設(shè)置線(xiàn)程池大小
scheduler.setThreadNamePrefix("task-");
scheduler.initialize();
return scheduler;
}
}
步驟 2:創(chuàng)建存儲(chǔ)和管理 Cron 表達(dá)式的組件
@Repository
public class CronRepository {
// 這里可以連接數(shù)據(jù)庫(kù)或配置中心,本例使用內(nèi)存存儲(chǔ)簡(jiǎn)化演示
private final Map<String, String> cronMap = new ConcurrentHashMap<>();
public CronRepository() {
// 設(shè)置初始值
cronMap.put("sampleTask", "0 0/1 * * * ?"); // 默認(rèn)每分鐘執(zhí)行一次
}
public String getCronByTaskName(String taskName) {
return cronMap.getOrDefault(taskName, "0 0/1 * * * ?");
}
public void updateCron(String taskName, String cron) {
// 驗(yàn)證cron表達(dá)式的有效性
try {
new CronTrigger(cron);
cronMap.put(taskName, cron);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid cron expression: " + cron, e);
}
}
}
步驟 3:創(chuàng)建 REST API 用于修改 Cron 表達(dá)式
@RestController
@RequestMapping("/scheduler")
public class SchedulerController {
@Autowired
private CronRepository cronRepository;
@GetMapping("/cron/{taskName}")
public Map<String, String> getCron(@PathVariable String taskName) {
Map<String, String> result = new HashMap<>();
result.put("taskName", taskName);
result.put("cron", cronRepository.getCronByTaskName(taskName));
return result;
}
@PutMapping("/cron/{taskName}")
public Map<String, String> updateCron(
@PathVariable String taskName,
@RequestParam String cron) {
cronRepository.updateCron(taskName, cron);
Map<String, String> result = new HashMap<>();
result.put("taskName", taskName);
result.put("cron", cron);
result.put("message", "Cron expression updated successfully");
return result;
}
}
3.2 使用 Spring 的 TaskScheduler 直接管理定時(shí)任務(wù)
我們可以直接使用 TaskScheduler 接口進(jìn)行定時(shí)任務(wù)的細(xì)粒度控制:
package com.example.scheduler;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;
@Service
public class AdvancedTaskScheduler {
@Autowired
private TaskScheduler taskScheduler;
@Autowired
private CronRepository cronRepository;
// 維護(hù)所有調(diào)度任務(wù)
private final Map<String, ScheduledTask> scheduledTasks = new ConcurrentHashMap<>();
@Data
@AllArgsConstructor
private static class ScheduledTask {
private String taskName;
private String cron;
private Runnable runnable;
private ScheduledFuture<?> future;
private boolean running;
}
/**
* 注冊(cè)一個(gè)新的定時(shí)任務(wù)
*/
public void registerTask(String taskName, Runnable runnable) {
// 判斷任務(wù)是否已存在
if (scheduledTasks.containsKey(taskName)) {
throw new IllegalArgumentException("Task with name " + taskName + " already exists");
}
String cron = cronRepository.getCronByTaskName(taskName);
ScheduledFuture<?> future = taskScheduler.schedule(
runnable,
triggerContext -> {
// 動(dòng)態(tài)獲取最新的cron表達(dá)式
String currentCron = cronRepository.getCronByTaskName(taskName);
return new CronTrigger(currentCron).nextExecution(triggerContext);
}
);
scheduledTasks.put(taskName, new ScheduledTask(taskName, cron, runnable, future, true));
}
/**
* 更新任務(wù)的Cron表達(dá)式
*/
public void updateTaskCron(String taskName, String cron) {
// 更新數(shù)據(jù)庫(kù)/配置中心中的cron表達(dá)式
cronRepository.updateCron(taskName, cron);
// 重新調(diào)度任務(wù)
ScheduledTask task = scheduledTasks.get(taskName);
if (task != null) {
// 取消現(xiàn)有任務(wù)
task.getFuture().cancel(false);
// 創(chuàng)建新任務(wù)
ScheduledFuture<?> future = taskScheduler.schedule(
task.getRunnable(),
triggerContext -> {
return new CronTrigger(cron).nextExecution(triggerContext);
}
);
// 更新任務(wù)信息
task.setCron(cron);
task.setFuture(future);
}
}
/**
* 暫停任務(wù)
*/
public void pauseTask(String taskName) {
ScheduledTask task = scheduledTasks.get(taskName);
if (task != null && task.isRunning()) {
task.getFuture().cancel(false);
task.setRunning(false);
}
}
/**
* 恢復(fù)任務(wù)
*/
public void resumeTask(String taskName) {
ScheduledTask task = scheduledTasks.get(taskName);
if (task != null && !task.isRunning()) {
ScheduledFuture<?> future = taskScheduler.schedule(
task.getRunnable(),
triggerContext -> {
String currentCron = cronRepository.getCronByTaskName(taskName);
return new CronTrigger(currentCron).nextExecution(triggerContext);
}
);
task.setFuture(future);
task.setRunning(true);
}
}
/**
* 刪除任務(wù)
*/
public void removeTask(String taskName) {
ScheduledTask task = scheduledTasks.get(taskName);
if (task != null) {
task.getFuture().cancel(false);
scheduledTasks.remove(taskName);
}
}
/**
* 獲取所有任務(wù)信息
*/
public List<Map<String, Object>> getAllTasks() {
return scheduledTasks.values().stream()
.map(task -> {
Map<String, Object> map = new HashMap<>();
map.put("taskName", task.getTaskName());
map.put("cron", task.getCron());
map.put("running", task.isRunning());
return map;
})
.collect(Collectors.toList());
}
}
四、使用數(shù)據(jù)庫(kù)存儲(chǔ) Cron 表達(dá)式
對(duì)于生產(chǎn)環(huán)境,我們需要確保定時(shí)任務(wù)的配置能夠持久化,將 CronRepository 修改為使用數(shù)據(jù)庫(kù)存儲(chǔ)
@Repository
public class DatabaseCronRepository implements CronRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public String getCronByTaskName(String taskName) {
try {
return jdbcTemplate.queryForObject(
"SELECT cron FROM scheduled_tasks WHERE task_name = ?",
String.class,
taskName
);
} catch (EmptyResultDataAccessException e) {
return "0 0/1 * * * ?"; // 默認(rèn)值
}
}
@Override
public void updateCron(String taskName, String cron) {
// 驗(yàn)證cron表達(dá)式
try {
new CronTrigger(cron);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid cron expression: " + cron, e);
}
// 更新數(shù)據(jù)庫(kù)
int updated = jdbcTemplate.update(
"UPDATE scheduled_tasks SET cron = ? WHERE task_name = ?",
cron, taskName
);
if (updated == 0) {
// 如果記錄不存在,則插入
jdbcTemplate.update(
"INSERT INTO scheduled_tasks (task_name, cron) VALUES (?, ?)",
taskName, cron
);
}
}
}
數(shù)據(jù)庫(kù)表結(jié)構(gòu):
CREATE TABLE scheduled_tasks (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
task_name VARCHAR(100) NOT NULL UNIQUE,
cron VARCHAR(100) NOT NULL,
description VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
五、總結(jié)
在一些相對(duì)簡(jiǎn)單的業(yè)務(wù)或項(xiàng)目或服務(wù)器資源有限的情況下,可以基于本文思路進(jìn)行簡(jiǎn)單擴(kuò)展,即可實(shí)現(xiàn)定時(shí)任務(wù)管理,無(wú)需額外引入其他定時(shí)任務(wù)組件。
如果是一些相對(duì)復(fù)雜的項(xiàng)目對(duì)定時(shí)任務(wù)有更多需求和可靠性保證的要求,可以引入如xxl-job、ElasticJob等任務(wù)調(diào)度組件。
以上就是SpringBoot運(yùn)行時(shí)修改定時(shí)任務(wù)Cron表達(dá)式的實(shí)現(xiàn)方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot修改Cron表達(dá)式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IDEA如何配置本地tomcat啟動(dòng)項(xiàng)目
這篇文章主要介紹了IDEA如何配置本地tomcat啟動(dòng)項(xiàng)目問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
jpa使用uuid策略后無(wú)法手動(dòng)設(shè)置id的問(wèn)題及解決
這篇文章主要介紹了jpa使用uuid策略后無(wú)法手動(dòng)設(shè)置id的問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
使用java實(shí)現(xiàn)日志工具類(lèi)分享
這篇文章主要介紹的Java代碼工具類(lèi)是用于書(shū)寫(xiě)日志信息到指定的文件,并且具有刪除之前日志文件的功能,需要的朋友可以參考下2014-03-03
java使用CollectionUtils工具類(lèi)判斷集合是否為空方式
這篇文章主要介紹了java使用CollectionUtils工具類(lèi)判斷集合是否為空方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
JAVA查詢(xún)MongoDB的幾種方法小結(jié)
本文主要介紹了JAVA查詢(xún)MongoDB的幾種方法小結(jié),通過(guò)閱讀本文,讀者可以了解如何使用Java查詢(xún)MongoDB,并在實(shí)際應(yīng)用中應(yīng)用這些技能,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08

