如何為 Spring Boot 應用程式僅執行一次排程任務
一、簡介
在本教程中,我們將學習如何安排任務僅運行一次。規劃任務對於自動化流程(例如報告或發送通知)很常見。通常,我們將它們設定為定期運行。儘管如此,在某些情況下,我們可能希望安排任務在將來只執行一次,例如初始化資源或執行資料遷移。
我們將探索幾種方法來安排任務在 Spring Boot 應用程式中僅運行一次。從使用具有初始延遲的@Scheduled
註解到更靈活的方法(例如TaskScheduler
和自訂觸發器),我們將學習如何確保我們的任務僅執行一次,而不會出現意外的重複。
2. 僅具有開始時間的TaskScheduler
雖然@Scheduled
註釋提供了一種直接的方法來安排任務,但它在靈活性方面受到限制。當我們需要更多地控制任務計劃(特別是一次性執行)時,Spring 的TaskScheduler
介面提供了一個更通用的替代方案。使用TaskScheduler
,我們可以以程式設計方式調度指定開始時間的任務,為動態調度場景提供更大的靈活性。
TaskScheduler
中最簡單的方法讓我們可以定義一個Runnable
任務和一個Instant
,代表我們希望它執行的確切時間。這種方法使我們能夠動態地安排任務,而不依賴固定的註釋。讓我們寫一個方法來安排任務在未來的特定時間運行:
private TaskScheduler scheduler = new SimpleAsyncTaskScheduler();
public void schedule(Runnable task, Instant when) {
scheduler.schedule(task, when);
}
TaskScheduler
中的所有其他方法都是週期性執行的,因此該方法對於一次性任務很有幫助。最重要的是,我們使用SimpleAsyncTaskScheduler
進行演示,但我們可以切換到適合我們需要運行的任務的任何其他實作。
排程任務很難測試,但我們可以使用CountDownLatch
來等待我們選擇的執行時間並確保它只執行一次。讓我們將countdown()
稱為我們的鎖存器任務,並將其安排在未來的一秒鐘:
@Test
void whenScheduleAtInstant_thenExecutesOnce() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
scheduler.schedule(latch::countDown,
Instant.now().plus(Duration.ofSeconds(1)));
boolean executed = latch.await(5, TimeUnit.SECONDS);
assertTrue(executed);
}
我們使用的是接受超時的latch.await()
版本,因此我們永遠不會無限期地等待。如果它回傳true
,我們斷言任務成功完成,而我們的鎖存器只有一次countDown()
呼叫。
3.僅使用帶有初始延遲的@Scheduled
在 Spring 中安排一次性任務的最簡單方法之一是使用具有初始延遲的@Scheduled
註解並省略fixedDelay
或fixedRate
屬性。通常,我們使用@Scheduled
定期運行任務,但是當我們僅指定initialDelay
時,任務將在指定的延遲後執行一次,並且不會重複:
@Scheduled(initialDelay = 5000)
public void doTaskWithInitialDelayOnly() {
// ...
}
在這種情況下,我們的方法將在包含此方法的元件初始化後運行 5 秒(5000 毫秒)。由於我們沒有指定任何速率屬性,因此該方法在初始執行後不會重複。當我們需要在應用程式啟動後僅運行一次任務或由於某種原因想要延遲任務的執行時,這種方法很有趣。
例如,這對於在應用程式啟動後幾秒鐘運行 CPU 密集型任務非常方便,允許其他服務和元件在消耗資源之前正確初始化。然而,這種方法的一個限制是調度是靜態的。我們無法在運行時動態調整延遲或執行時間。另外值得注意的是, @Scheduled
註解要求該方法是 Spring 管理的元件或服務的一部分。
3.1. 6月之前
在 Spring 6 之前,不可能忽略延遲或速率屬性,因此我們唯一的選擇是指定理論上無法到達的延遲:
@Scheduled(initialDelay = 5000, fixedDelay = Long.MAX_VALUE)
public void doTaskWithIndefiniteDelay() {
// ...
}
在此範例中,任務將在最初的 5 秒延遲後執行,後續執行將在數百萬年之前發生,從而有效地使其成為一次性任務。雖然這種方法有效,但如果我們需要靈活性或更簡潔的程式碼,那麼它並不理想。
4. 創造一個沒有下次執行的PeriodicTrigger
我們的最後一個選擇是實現PeriodicTrigger
。當我們需要更多可重複使用的、複雜的調度邏輯時,使用它而不是TaskScheduler
對我們有利。如果我們還沒有觸發它,我們可以重寫nextExecution()
以僅返回下一個執行時間。
讓我們先定義一個週期和初始延遲:
public class OneOffTrigger extends PeriodicTrigger {
public OneOffTrigger(Instant when) {
super(Duration.ofSeconds(0));
Duration difference = Duration.between(Instant.now(), when);
setInitialDelay(difference);
}
// ...
}
由於我們希望只執行一次,因此我們可以將任何內容設定為句點。由於我們必須傳遞一個值,因此我們將傳遞一個零。最終,我們計算我們希望執行任務的所需時刻與當前時間之間的差異,因為我們需要將Duration
傳遞給初始延遲。
然後,為了覆蓋nextExecution()
,我們檢查context
中的最後完成時間:
@Override
public Instant nextExecution(TriggerContext context) {
if (context.lastCompletion() == null) {
return super.nextExecution(context);
}
return null;
}
null
完成意味著它尚未觸發,因此我們讓它呼叫預設實作。否則,我們返回null
,這使得該觸發器僅執行一次。最後,讓我們建立一個使用它的方法:
public void schedule(Runnable task, PeriodicTrigger trigger) {
scheduler.schedule(task, trigger);
}
4.1.測試PeriodicTrigger
最後,我們可以編寫一個簡單的測試來確保觸發器能如預期運作。在此測試中,我們使用CountDownLatch
來追蹤任務是否執行。我們使用OneOffTrigger
安排任務並驗證它是否只運行一次:
@Test
void whenScheduleWithRunOnceTrigger_thenExecutesOnce() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
scheduler.schedule(latch::countDown, new OneOffTrigger(
Instant.now().plus(Duration.ofSeconds(1))));
boolean executed = latch.await(5, TimeUnit.SECONDS);
assertTrue(executed);
}
5. 結論
在本文中,我們探討了在 Spring Boot 應用程式中安排任務僅執行一次的解決方案。我們從最直接的選項開始,使用沒有固定速率的@Scheduled
註釋。然後,我們轉向更靈活的解決方案,例如使用TaskScheduler
進行動態調度並建立自訂觸發器以確保任務僅執行一次。
每種方法都提供不同程度的控制,因此我們選擇最適合我們用例的方法。與往常一樣,原始碼可以在 GitHub 上取得。