1. 簡介
在本教程中,我們將學習如何安排任務僅在一次執行後運行。 計劃任務通常用於自動化流程,例如生成報告或發送通知。 通常,我們將這些任務設置為週期性地運行。 然而,在某些情況下,我們可能希望安排任務在未來某個時間僅執行一次,例如初始化資源或執行數據遷移。
我們將探索在 Spring Boot 應用程序中安排任務僅執行一次的多種方法。 從使用帶有初始延遲的 <em >@Scheduled</em> 註解到更靈活的方法,如 <em >TaskScheduler</em> 和自定義觸發器,我們將學習如何確保我們的任務僅執行一次,並且不會出現意外的重複執行。
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 只有一個 調用。
3. 使用 @Scheduled 僅指定初始延遲
在 Spring 中,最簡單的方法之一是使用帶有初始延遲的 <em>@Scheduled</em> 註解,同時省略 <em>fixedDelay</em> 或 <em>fixedRate</em> 屬性。 `通常,我們使用 @Scheduled 來以定期間隔運行任務,但當僅指定 initialDelay 時,任務將在指定延遲後執行一次,不會重複執行:
@Scheduled(initialDelay = 5000)
public void doTaskWithInitialDelayOnly() {
// ...
}在這種情況下,我們的方法將在包含該方法的組件初始化後運行 5 秒(5000 毫秒)。由於我們沒有指定任何速率屬性,因此該方法不會在初始執行後重復執行。這種方法在我們需要在應用程序啓動後僅運行一次任務或出於某種原因延遲執行任務時非常有用。
例如,這對於在應用程序啓動後幾秒鐘運行 CPU 密集型任務非常方便,從而允許其他服務和組件在消耗資源之前正確初始化。 然而,這種方法的侷限性在於調度是靜態的。我們無法在運行時動態調整延遲或執行時間。 此外,@Scheduled 註解要求該方法必須是 Spring 管理的組件或服務的一部分。
3.1. 在 Spring 6 之前
在 Spring 6 之前,無法省略 delay 和 rate 屬性,因此我們唯一的選擇是指定一個理論上不可達的延遲:
@Scheduled(initialDelay = 5000, fixedDelay = Long.MAX_VALUE)
public void doTaskWithIndefiniteDelay() {
// ...
}在此示例中,任務將在初始的 5 秒延遲後執行,後續執行將不會在數百萬年內發生,從而使其成為一次性任務。雖然這種方法有效,但如果我們需要靈活性或更簡潔的代碼,則不理想。
4. 創建不帶後續執行的週期觸發器
我們的最終選項是實現週期觸發器。 使用它比使用任務調度器在我們需要更可重用、更復雜的調度邏輯時更有優勢。 我們可以覆蓋 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;
}一個空完成(completion)意味着它尚未觸發,因此我們允許它調用默認實現。否則,我們返回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 進行動態調度,以及創建自定義觸發器,以確保任務僅執行一次。
每種方法都提供了不同的控制級別,因此我們選擇最適合我們用例的方法。