從資料庫取得 Spring Boot 定時任務的 Cron 表達式
1. 概述
Spring Boot 讓我們可以輕鬆調度任務,例如使用@Scheduled註解。有時,我們不想硬編碼調度參數,例如 cron 表達式。相反,我們希望使用資料庫中的 cron 表達式來調度任務。
在本教程中,我們將首先探討實現該目標的兩種方法。此外,我們還將討論何時使用哪種方法。
2. 問題介紹
在使用 Spring 調度時,最直接的方法通常是將 cron 表達式直接放在@Scheduled,例如:
@Scheduled(cron = "*/5 * * * * ?")
public void run() {
// job logic
}
這種方法雖然有效,但它也將日程安排硬編碼到應用程式中。
當我們想要從資料庫中取得 cron 表達式時,問題就出現了。這是因為我們不能簡單地這樣寫:
private String cronExpression = loadFromDatabase();
@Scheduled(cron = cronExpression)
public void run() {
// invalid approach
}
如果編譯這段程式碼,編譯器會報錯:
java: element value must be a constant expression
原因在於註解中的cron屬性不能從任意執行時間變數填入。實際上,我們需要 Spring 可以解析的間接尋址方式,或完全放棄基於註解的調度方式。
在本教程中,我們將探討這兩種方法。但在深入探討解決方案之前,讓我們先做一些準備工作。
3. 準備資料庫中的數據
本教學使用小型 H2 記憶體資料庫。此外,為簡單起見,我們將省略 Spring Data JPA 配置。
3.1 建立簡單表格
我們希望 Spring Boot 應用程式在啟動時自動建立資料庫表並向其中插入一些資料。因此,讓我們準備一個schema.sql檔:
CREATE TABLE IF NOT EXISTS cron_config (
id BIGINT PRIMARY KEY,
cron_expression VARCHAR(255) NOT NULL
);
如我們所見, cron_config表有意設計得非常簡潔。我們只需要一個識別符和 cron 表達式本身。
接下來,我們建立一個data.sql文件,向表中插入一行資料:
INSERT INTO cron_config (id, cron_expression) VALUES (1, '*/5 * * * * ?');
我們插入一行程式碼,以便應用程式在啟動時立即擁有一個定時任務值。因此,預設情況下,該定時任務每五秒鐘運行一次。
3.2. 準備Entity和Repository類
對應的 JPA 實體同樣很小:
@Entity
@Table(name = "cron_config")
public class CronEntity {
@Id
private Long id;
private String cronExpression;
// ... the default constructor, getters and setters are omitted
}
該存儲庫同樣簡單,並提供了我們需要的 CRUD 操作:
public interface CronConfigRepository extends JpaRepository<CronEntity, Long> {
}
有了這些元件,我們就可以從資料庫讀取和更新 cron 值。
現在,讓我們看看如何告訴 Spring 使用我們儲存在資料庫中的 cron 表達式來排程作業。
4. 使用cronLoader Bean
在第一種方法中,我們希望保留@Scheduled註解,但避免直接在註解中放置 cron 表達式。相反,我們將建立一個 Spring Bean,用於從資料庫讀取 cron 表達式,並在@Scheduled's cron屬性中引用該 Bean。
4.1. 定義cronLoader Bean 以載入 Cron 表達式
我們首先建立一個小型配置類,該類別定義了一個名為cronLoader bean:
@Configuration
public class CronLoaderConfig {
@Bean
String cronLoader(CronConfigRepository repository) {
return repository.findById(1L)
.map(CronEntity::getCronExpression)
.orElseThrow(() -> new RuntimeException("Cron expression not found in DB"));
}
}
如上面的程式碼所示,這個 bean 非常簡單明了。它從id = 1行中讀取 cron 表達式,並將其作為**String** .
4.2. 從@Scheduled引用 Bean
接下來,我們可以在計劃任務中透過 SpEL 引用該 bean :
@Component
public class AnnotationScheduledJob {
private static final Logger log = LoggerFactory.getLogger(AnnotationScheduledJob.class);
@Scheduled(cron = "#{@cronLoader}")
public void run() {
log.info("✅ [{}] Job executed - cron loaded from DB via @Scheduled", now());
}
}
關鍵在於:我們仍然使用@Scheduled,但 cron 表達式來自cronLoader bean,而不是直接嵌入到註解中。
此外,我們的任務只是列印一行執行時間,以便我們可以驗證執行時間是否與資料庫中的 cron 表達式相符。
接下來,我們來驗證一下作業排程是否如預期般運作。
4.3 啟動應用程式並觀察控制台輸出
啟動 Spring Boot 應用程式後,我們可以在控制台日誌中看到以下幾行:
...
... AnnotationScheduledJob : ✅ [10:00:05.002] Job executed - cron loaded from DB via @Scheduled
... AnnotationScheduledJob : ✅ [10:00:10.001] Job executed - cron loaded from DB via @Scheduled
... AnnotationScheduledJob : ✅ [10:00:15.000] Job executed - cron loaded from DB via @Scheduled
... AnnotationScheduledJob : ✅ [10:00:20.001] Job executed - cron loaded from DB via @Scheduled
...
由於我們的data.sql將「 */5 * * * * ?”作為 cron 表達式插入,因此我們大約每五秒鐘就能看到一次基於註解的作業日誌。這為我們提供了一種直接的方法來驗證 cron 值是否已從資料庫載入。
4.4. cronLoader Bean 方法的局限性
這種方法雖然方便,但有一個重要的限制。
Spring 在應用程式啟動時建立cronLoader bean 時會載入 cron 值。之後,計劃任務會繼續使用已解析的值。
如果我們稍後更新cron_config表,正在執行的應用程式的調度不會自動變更。實際上,我們通常需要重啟應用程式才能使新值生效。
因此,只有當可以在啟動時載入一次日程安排時,這種方法才適用。
5. 使用 Spring 的SchedulingConfigurer
如果我們希望計劃任務在不重啟的情況下回應資料庫變更,就需要更動態的解決方案。接下來,讓我們深入探討。
5.1. 實現SchedulingConfigurer
與其依賴@Scheduled註解,我們可以實現 Spring 的SchedulingConfigurer並註冊我們自己的觸發任務。
首先,我們建立一個實作SchedulingConfigurer DynamicScheduledConfig配置類別:
@Configuration
public class DynamicScheduledConfig implements SchedulingConfigurer {
private static final Logger log = LoggerFactory.getLogger(DynamicScheduledConfig.class);
private final CronConfigRepository repository;
public DynamicScheduledConfig(CronConfigRepository repository) {
this.repository = repository;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(() -> log.info(
"✅ [{}] DynamicScheduledConfig executed - cron re-read from DB per execution",
now()), triggerContext -> {
String cronExpression = repository.findById(1L)
.map(CronEntity::getCronExpression)
.orElseThrow(() -> new RuntimeException("Cron expression not found in DB"));
return new CronTrigger(cronExpression).nextExecution(triggerContext);
});
}
}
接下來,我們來了解configureTasks()方法的工作原理:
首先,我們使用addTriggerTask(…)方法註冊一個任務。 addTriggerTask addTriggerTask()方法有兩個參數:一個Runnable (「What」),其中包含我們要在此任務中執行的工作;以及一個Trigger物件(「When」),用於指定何時觸發該任務。
在我們的範例中,我們的任務只是簡單地列印一條包含當前時間的日誌。
Trigger參數是實作中的「關鍵」部分。它會在每次任務完成後計算下一次執行時間:
- 使用注入的
repository從資料庫中取得 cron 表達式 - 根據 cron 表達式動態建立一個新的
CronTrigger對象,Spring 會要求該觸發器執行下一個執行時間。
這意味著,如果我們更改資料庫中的 cron 表達式,觸發器回呼函數會在計算下次執行時間時立即使用新值。這與cronLoader bean 方法的主要區別在於:每次 Spring 計算下次執行時間時,都會從資料庫重新讀取 cron 表達式。
5.2. 一個簡單的 REST 控制器,用於更新資料庫中的 Cron 表達式
為了方便測試,我們建立一個簡單的 REST 控制器,以便透過 HTTP 請求快速更新資料庫記錄:
@RestController
public class CronController {
private static final Logger log = LoggerFactory.getLogger(CronController.class);
private final CronConfigRepository repository;
public CronController(CronConfigRepository repository) {
this.repository = repository;
}
@GetMapping("/cron")
public String updateCron() {
CronEntity cronEntity = repository.findById(1L)
.orElseThrow(() -> new RuntimeException("Cron expression not found in database"));
cronEntity.setCronExpression("*/10 * * * * ?");
repository.save(cronEntity);
String msg = "[DB] ⏰ Updated cron expression in DB to: */10 * * * * ?";
log.info(msg);
return msg;
}
}
如範例所示,為簡單起見,此端點始終將 cron 設定為每 10 秒運行一次。
接下來,我們來驗證SchedulingConfigurer方法是否總是能根據資料庫變更重新安排作業。
5.3 啟動應用程式並觀察控制台輸出
為了方便示範過程中查看日誌,我們可以暫時註解掉AnnotationScheduledJob中基於註解的調度行,以便只執行動態任務:
@Component
public class AnnotationScheduledJob {
// ...
// @Scheduled(cron = "#{@cronLoader}") <-- comment out
public void run() {
// ...
}
}
接下來,我們啟動 Spring Boot 應用程式。應用啟動後,計劃任務也會隨之開始。控制台中將出現類似如下的日誌輸出:
... DynamicScheduledConfig : ✅ [10:05:05.001] DynamicScheduledConfig executed - cron re-read from DB per execution
... DynamicScheduledConfig : ✅ [10:05:10.001] DynamicScheduledConfig executed - cron re-read from DB per execution
... DynamicScheduledConfig : ✅ [10:05:15.001] DynamicScheduledConfig executed - cron re-read from DB per execution
... DynamicScheduledConfig : ✅ [10:05:20.000] DynamicScheduledConfig executed - cron re-read from DB per execution
因為data.sql檔案仍然包含“*/5 * * * * ?” ,所以動態任務最初大約每五秒鐘運行一次。
接下來,我們呼叫“/cron”端點來更新 cron 表達式。例如,我們可以執行以下curl命令來發送 GET 請求:
curl http://localhost:8080/cron
然後,在 Spring Boot 應用程式控制台中,我們可以看到以下日誌,這表示 cron 表達式已變更為「每 10 秒一次」:
... CronController : [DB] ⏰ Updated cron expression in DB to: */10 * * * * ?
之後,動態任務應該開始遵循新的時間間隔。換句話說,後續執行的間隔應該從每五秒一次改為每十秒一次:
... DynamicScheduledConfig : ✅ [10:05:30.000] DynamicScheduledConfig executed - cron re-read from DB per execution
... DynamicScheduledConfig : ✅ [10:05:40.002] DynamicScheduledConfig executed - cron re-read from DB per execution
... DynamicScheduledConfig : ✅ [10:05:50.000] DynamicScheduledConfig executed - cron re-read from DB per execution
... DynamicScheduledConfig : ✅ [10:05:60.000] DynamicScheduledConfig executed - cron re-read from DB per execution
這體現了SchedulingConfigurer:我們可以更改資料庫中的 cron 表達式,讓正在運行的應用程式無需重新啟動即可適應。
6. 結論
在本文中,我們探討了在 Spring Boot 中從資料庫中取得排程任務的 cron 值的兩種方法。
使用cronLoader bean 方法,我們可以保持實作簡單,並繼續使用@Scheduled,但我們只在啟動期間載入一次 cron 值。
借助SchedulingConfigurer,我們可以控制任務註冊,並在 Spring 計算下次執行時間時從資料庫重新讀取 cron 表達式。這使得調度具有動態性,並允許運行時更新。
因此,如果我們只需要一個啟動時的值,那麼使用 bean 支援的@Scheduled設定通常就足夠了。但如果我們需要真正動態的調度, SchedulingConfigurer是更好的選擇。
與往常一樣,範例的完整原始程式碼可在 GitHub 上找到。