天天看點

Spring定時任務-任務執行和排程—官方原版

作者:Doker多克

Spring架構分别通過TaskExecutor和TaskScheduler接口為任務的異步執行和排程提供了抽象。Spring還提供了支援應用程式伺服器環境中的線程池或CommonJ委托的那些接口的實作。最終,在公共接口後面使用這些實作,消除了JavaSE5、JavaSE6和JakartaEE環境之間的差異。

Spring還具有內建類,以支援Timer(自1.3以來JDK的一部分)和Quartz Scheduler的排程。您可以分别使用FactoryBean和可選的Timer或Trigger執行個體引用來設定這兩個排程器。此外,Quartz Scheduler和Timer都有一個友善類,它允許您調用現有目标對象的方法(類似于普通的MethodInvokingFactoryBean操作)。

一、 Spring TaskExecutor 概念

執行器是線程池概念的JDK名稱。“executor”命名是因為無法保證底層實作實際上是一個池。執行器可以是單線程的,甚至可以是同步的。Spring的抽象隐藏了JavaSE和JakartaEE環境之間的實作細節。

Spring的TaskExecutor接口與java.util.concurrent.Executor接口相同。事實上,最初,它存在的主要原因是在使用線程池時不需要Java5。該接口有一個方法(execute(Runnable task)),該方法根據線程池的語義和配置接受要執行的任務。

建立TaskExecutor最初是為了在需要時為其他Spring元件提供線程池抽象。ApplicationEventMulticaster、JMS的AbstractMessageListenerContainer和Quartz內建等元件都使用TaskExecutor抽象來池線程。然而,如果您的bean需要線程池行為,您也可以根據自己的需要使用此抽象。

1.1 TaskExecutor 類型

Spring包括許多預先建構的TaskExecutor實作。很可能,你永遠不需要實作你自己的。Spring提供的變體如下:

  • SyncTaskExecutor:此實作不會異步運作調用。相反,每次調用都發生在調用線程中。它主要用于不需要多線程的情況,例如在簡單的測試用例中。
  • SimpleAsyncTaskExecutor:此實作不重用任何線程。相反,它為每個調用啟動一個新線程。然而,它确實支援一個并發限制,即在釋放槽之前阻止任何超過該限制的調用。如果您正在尋找真正的池,請參閱此清單後面的ThreadPoolTaskExecutor。
  • ConcurrentSkExecutor:此實作是java.util.concurrent.Executor執行個體的擴充卡。還有一種替代方法(ThreadPoolTaskExecutor)将Executtor配置參數公開為bean财産。很少需要直接使用ConcurrentTaskExecutor。但是,如果ThreadPoolTaskExecutor不夠靈活,無法滿足您的需要,則ConcurrentTaskExecutor是另一種選擇。
  • ThreadPoolTaskExecutor:此實作最常用。它公開用于配置java.util.concurrent.ThreadPoolExecutor的bean财産,并将其包裝在TaskExecuttor中。如果您需要适應不同類型的java.util.concurrent.Executor,我們建議您改用ConcurrentSkExecutor。
  • DefaultManagedTaskExecutor:此實作在JSR-236相容的運作時環境(如Jakarta EE應用程式伺服器)中使用JNDI獲得的ManagedExecutorService,以取代CommonJ WorkManager。

1.2 TaskExecutor 使用

Spring的TaskExecutor實作用作簡單的JavaBeans。在下面的示例中,我們定義了一個bean,它使用ThreadPoolTaskExecutor異步列印一組消息:

import org.springframework.core.task.TaskExecutor;

public class TaskExecutorExample {

    private class MessagePrinterTask implements Runnable {

        private String message;

        public MessagePrinterTask(String message) {
            this.message = message;
        }

        public void run() {
            System.out.println(message);
        }
    }

    private TaskExecutor taskExecutor;

    public TaskExecutorExample(TaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    public void printMessages() {
        for(int i = 0; i < 25; i++) {
            taskExecutor.execute(new MessagePrinterTask("Message" + i));
        }
    }
}

           

正如您所看到的,您不是從池中檢索線程并自己執行它,而是将Runnable添加到隊列中。然後,TaskExecutor使用其内部規則來決定任務何時運作。

為了配置TaskExecutor使用的規則,我們公開了簡單的bean财産:

<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    <property name="corePoolSize" value="5"/>
    <property name="maxPoolSize" value="10"/>
    <property name="queueCapacity" value="25"/>
</bean>

<bean id="taskExecutorExample" class="TaskExecutorExample">
    <constructor-arg ref="taskExecutor"/>
</bean>
           

二、Spring TaskScheduler概述

除了TaskExecutor抽象之外,Spring還有一個TaskScheduler SPI,它具有多種方法來排程将來某個時刻運作的任務。以下清單顯示了TaskScheduler接口定義:

public interface TaskScheduler {

    Clock getClock();

    ScheduledFuture schedule(Runnable task, Trigger trigger);

    ScheduledFuture schedule(Runnable task, Instant startTime);

    ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);

    ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);

    ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);

    ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);

           

最簡單的方法是一個名為schedule的方法,它隻需要一個Runnable和一個Instant。這會導緻任務在指定時間後運作一次。所有其他方法都能夠安排任務重複運作。固定速率和固定延遲方法用于簡單的周期性執行,但接受觸發器的方法要靈活得多。

1.Trigger 接口

Trigger接口本質上受到JSR-236的啟發。觸發器的基本思想是,可以根據過去的執行結果甚至任意條件來确定執行時間。如果這些确定考慮了先前執行的結果,則該資訊在TriggerContext中可用。Trigger接口本身非常簡單,如下表所示:

public interface Trigger {

    Instant nextExecution(TriggerContext triggerContext);
}

           

TriggerContext是最重要的部分。它封裝了所有相關資料,如果需要,将來可以進行擴充。TriggerContext是一個接口(預設使用SimpleTriggerContext實作)。下面的清單顯示了Trigger實作的可用方法。

public interface TriggerContext {

    Clock getClock();

    Instant lastScheduledExecution();

    Instant lastActualExecution();

    Instant lastCompletion();
}

           

2.2. Trigger 接口實作

Spring提供了Trigger接口的兩種實作。最有趣的是CronTrigger。它支援基于cron表達式的任務排程。例如,以下任務計劃在每小時15分鐘後運作,但僅在工作日的朝九晚五“工作時間”内運作:

scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
           

另一個實作是PeriodicTrigger,它接受一個固定的周期、一個可選的初始延遲值和一個布爾值,以訓示該周期應該被解釋為固定速率還是固定延遲。由于TaskScheduler接口已經定義了以固定速率或固定延遲排程任務的方法,是以應盡可能直接使用這些方法。PeriodicTrigger實作的價值在于,您可以在依賴Trigger抽象的元件中使用它。例如,允許交替使用周期性觸發器、基于cron的觸發器,甚至自定義觸發器實作可能很友善。這樣的元件可以利用依賴注入,這樣您就可以在外部配置這樣的觸發器,進而輕松地修改或擴充它們。

2.3. TaskScheduler 實作

與Spring的TaskExecutor抽象一樣,TaskScheduler安排的主要好處是應用程式的排程需求與部署環境分離。當部署到應用程式伺服器環境時,這個抽象級别尤其重要,因為應用程式本身不應該直接建立線程。對于這樣的場景,Spring提供了一個TimerManagerTaskScheduler,它委托給WebLogic或WebSphere上的CommonJ TimerManager,以及一個更新的DefaultManagedTaskScheduler,在Jakarta EE環境中委托給JSR-236 ManagedScheduledExecutorService。兩者通常都配置有JNDI查找。

每當不需要外部線程管理時,一個更簡單的替代方案就是在應用程式中設定本地ScheduledExecutorService,它可以通過Spring的ConcurrentTaskScheduler進行調整。為了友善起見,Spring還提供了ThreadPoolTaskScheduler,它在内部委托給ScheduledExecutorService,以提供與ThreadPoolTaskExecutor類似的通用bean樣式配置。這些變體對于寬松的應用程式伺服器環境中的本地嵌入式線程池設定也非常适用 — 特别是在Tomcat和Jetty上。

三、排程和異步執行的注解支援

Spring 為任務排程和異步方法提供了注釋支援 執行。

3.1. 啟用排程注解

要啟用對@Scheduled和@Async注釋的支援,可以将@EnableScheduling和@EnableAsync添加到@Configuration類之一,如下例所示:

@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}

           

您可以選擇應用程式的相關注釋。例如,如果隻需要對@Scheduled的支援,則可以省略@EnableAsync。對于更細粒度的控制,可以另外實作SchedulingConfigurer接口、AsyncConfigurer接口或兩者。有關詳細資訊,請參閱SchedulingConfigurer和AsyncConfigurer javadoc。

如果您喜歡XML配置,可以使用<task:annotation-driven>元素,如下例所示:

<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>
           

注意,對于前面的XML,提供了一個executor引用來處理與帶有@Async注釋的方法相對應的任務,而提供了排程器引用來管理帶有@Scheduled注釋的方法。

3.2. @Scheduled注解

您可以将@Scheduled注釋與觸發器中繼資料一起添加到方法中。例如,以下方法每五秒(5000毫秒)調用一次,具有固定的延遲,這意味着該時間段是從每次前一次調用的完成時間開始計算的。

@Scheduled(fixedDelay = 5000)
public void doSomething() {
    // something that should run periodically
}           

預設情況下,毫秒将用作固定延遲、固定速率和初始延遲值的時間機關。如果您想使用不同的時間機關,例如秒或分鐘,可以通過@Scheduled中的timeUnit屬性進行配置。

例如,前面的示例也可以編寫如下。

@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
    // something that should run periodically
}           

如果需要固定速率執行,可以在注釋中使用fixedRate屬性。以下方法每五秒調用一次(在每次調用的連續開始時間之間測量)。

@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
    // something that should run periodically
}
           

對于固定延遲和固定速率的任務,可以通過訓示在第一次執行方法之前等待的時間量來指定初始延遲,如下面的fixedRate示例所示。

@Scheduled(initialDelay = 1000, fixedRate = 5000)
public void doSomething() {
    // something that should run periodically
}
           

如果簡單的周期性排程不夠表達,可以提供cron表達式。以下示例僅在工作日運作:

@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
    // something that should run on weekdays only
}
           

從Spring Framework 4.3開始,任何範圍的bean都支援@Scheduled方法。

確定您在運作時沒有初始化同一@Scheduled注釋類的多個執行個體,除非您确實希望排程對每個此類執行個體的回調。與此相關的是,請確定不要在用@Scheduled注釋并在容器中注冊為正常Springbean的bean類上使用@Configurationable。否則,您将獲得兩次初始化(一次通過容器,一次通過@Configurationable方面),結果是每個@Scheduled方法被調用兩次。

3.3 @Async 注解

您可以在方法上提供@Async注釋,以便異步調用該方法。換句話說,調用方在調用時立即傳回,而方法的實際執行發生在已送出給Spring TaskExecutor的任務中。在最簡單的情況下,可以将注釋應用于傳回void的方法,如下例所示:

@Async
void doSomething() {
    // this will be run asynchronously
}
           

與用@Scheduled注釋注釋的方法不同,這些方法可能需要參數,因為它們是由調用者在運作時以“正常”方式調用的,而不是由容器管理的計劃任務調用的。例如,以下代碼是@Async注釋的合法應用程式:

@Async
void doSomething(String s) {
    // this will be run asynchronously
}

           

即使傳回值的方法也可以異步調用。但是,此類方法需要具有Future類型的傳回值。這仍然提供了異步執行的好處,是以調用者可以在調用Future上的get()之前執行其他任務。以下示例顯示如何在傳回值的方法上使用@Async:

@Async
Future<String> returnSomething(int i) {
    // this will be run asynchronously
}
           
@異步方法不僅可以聲明正常java.util.concurrent.Future傳回類型,還可以聲明Spring的org.springframework.util.concurrent.ListenableFuture,或者從Spring 4.2開始,JDK 8的java.util.coccurrent.CompletableFuture,以便與異步任務進行更豐富的互動,并與進一步的處理步驟立即組合。

不能将@Async與生命周期回調(如@PostConstruct)結合使用。要異步初始化Spring bean,目前必須使用單獨的初始化Spring bean來調用目标上的@Async注釋方法,如下例所示:

public class SampleBeanImpl implements SampleBean {

    @Async
    void doSomething() {
        // ...
    }

}

public class SampleBeanInitializer {

    private final SampleBean bean;

    public SampleBeanInitializer(SampleBean bean) {
        this.bean = bean;
    }

    @PostConstruct
    public void initialize() {
        bean.doSomething();
    }

}
           

3.4 Executor Qualification with @Async

預設情況下,在方法上指定@Async時,所使用的執行器是在啟用異步支援時配置的執行器,即,如果使用XML或AsyncConfigurer實作(如果有),則為“注釋驅動”元素。但是,當需要訓示在執行給定方法時應使用預設值以外的執行器時,可以使用@Async注釋的value屬性。以下示例顯示了如何執行此操作:

@Async("otherExecutor")
void doSomething(String s) {
    // this will be run asynchronously by "otherExecutor"
}
           

在這種情況下,“otherExecutor”可以是Spring容器中任何Executor bean的名稱,也可以是與任何Executoor關聯的限定符的名稱(例如,使用<qualifier>元素或Spring的@qualifier注釋指定)。

3.5@Async異常管理

當@Async方法具有Future類型的傳回值時,很容易管理在方法執行期間引發的異常,因為在Future結果上調用get時會引發此異常。然而,對于void傳回類型,異常是未捕獲的,無法傳輸。您可以提供AsyncUnaughtExceptionHandler來處理此類異常。以下示例顯示了如何執行此操作:

public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        // handle exception
    }
}

           

3.6代碼執行個體:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
           
/**
 * 在spring boot的啟動類上面添加 @EnableScheduling 注解
 */
@SpringBootApplication
@EnableScheduling
public class ScheduleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ScheduleApplication.class,args);
    }
}
           

@Scheduled注解的另外兩個重要屬性:fixedRate和fixedDelay

  • fixedDelay:上一個任務結束後多久執行下一個任務
  • fixedRate:上一個任務的開始到下一個任務開始時間的間隔
@Component
public class ScheduleDoker {
/**
 * 測試fixedRate,每2s執行一次
 * @throws Exception
 */
@Scheduled(fixedRate = 2000)
public void fixedRate() throws Exception {
    System.out.println("fixedRate開始執行時間:" + new Date(System.currentTimeMillis()));
    //休眠1秒
    Thread.sleep(1000);
    System.out.println("fixedRate執行結束時間:" + new Date(System.currentTimeMillis()));
}

fixedRate開始執行時間:Sun Feb 12 19:59:05 CST 2022
fixedRate執行結束時間:Sun Feb 12 19:59:06 CST 2022
fixedRate開始執行時間:Sun Feb 12 19:59:07 CST 2022
fixedRate執行結束時間:Sun Feb 12 19:59:08 CST 2022
fixedRate開始執行時間:Sun Feb 12 19:59:09 CST 2022
fixedRate執行結束時間:Sun Feb 12 19:59:10 CST 2022
    
/**
 * 等上一次執行完等待1s執行
 * @throws Exception
 */
@Scheduled(fixedDelay = 1000)
public void fixedDelay() throws Exception {
    System.out.println("fixedDelay開始執行時間:" + new Date(System.currentTimeMillis()));
    //休眠兩秒
    Thread.sleep(1000 * 2);
    System.out.println("fixedDelay執行結束時間:" + new Date(System.currentTimeMillis()));
}

fixedDelay執行結束時間:Sun Feb 12 13:07:23 CST 2022
fixedDelay開始執行時間:Sun Feb 12 13:07:24 CST 2022
fixedDelay執行結束時間:Sun Feb 12 13:07:26 CST 2022
fixedDelay開始執行時間:Sun Feb 12 13:07:27 CST 2022
fixedDelay執行結束時間:Sun Feb 12 13:07:29 CST 2022
}           

四、task命名空間

從版本3.0開始,Spring包含一個用于配置TaskExecutor和TaskScheduler執行個體的XML命名空間。它還提供了一種友善的方式來配置要使用觸發器排程的任務

五、cron 表達式

所有 Spring cron 表達式都必須符合相同的格式,無論您是在@Scheduled注釋、任務:計劃任務元素、 或其他地方。 格式正确的 cron 表達式(例如 )由六個空格分隔的時間和日期組成 字段,每個字段都有自己的有效值範圍:* * * * * *

┌───────────── second (0-59)

│ ┌───────────── minute (0 - 59)

│ │ ┌───────────── hour (0 - 23)

│ │ │ ┌───────────── day of the month (1 - 31)

│ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)

│ │ │ │ │ ┌───────────── day of the week (0 - 7)

│ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN)

│ │ │ │ │ │

* * * * * *

有一些規則适用:

  • 字段可以是星号(*),始終代表“first-last”。對于月日或星期日字段,可以使用問号(?)代替星号。
  • 逗号(,)用于分隔清單中的項目。
  • 用連字元(-)分隔的兩個數字表示一系列數字。指定的範圍包含在内。
  • 在帶/的範圍(或*)之後指定數字值在該範圍内的間隔。
  • 英文名稱也可以用于月份和星期幾字段。使用特定日期或月份的前三個字母(大小寫無關緊要)。
  • “月日”和“星期日”字段可以包含L字元,其含義不同。
    • 在月日字段中,L代表該月的最後一天。如果後面跟着一個負偏移量(即L-n),則表示該月的第n天到最後一天。
    • 在星期幾字段中,L代表一周的最後一天。如果字首為數字或三個字母的名稱(dL或DDDL),則表示當月的最後一天(d或DDD)。
  • “月日”字段可以是nW,它代表一個月中最近的一個工作日。如果n落在星期六,這将産生前一個星期五。如果n在星期天,這将生成後一個星期一,如果n為1并且落在星期天(即:1W代表一個月中的第一個工作日),也會發生這種情況。
  • 如果月日字段為LW,則表示該月的最後一個工作日。
  • 星期幾字段可以是d#n(或DDD#n),表示一個月中第n個星期d(或DDD)。

以下是一些示例:

Cron 表達式 意義
0 0 * * * * 每天每個小時之巅
*/10 * * * * * 每十秒
0 0 8-10 * * * 每天8點、9點及10點
0 0 6,19 * * * 每天上午 6:00 和晚上 7:00
0 0/30 8-10 * * * 每天 8:00、8:30、9:00、9:30、10:00 和 10:30
0 0 9-17 * * MON-FRI 工作日朝九晚五的整點
0 0 0 25 DEC ? 每個聖誕節午夜
0 0 0 L * * 每月最後一天午夜
0 0 0 L-3 * * 每月倒數第三天的午夜
0 0 0 * * 5L 每月最後一個星期五午夜
0 0 0 * * THUL 每月最後一個星期四午夜
0 0 0 1W * * 每月第一個工作日的午夜
0 0 0 LW * * 每月最後一個工作日的午夜
0 0 0 ? * 5#2 每月第二個星期五午夜
0 0 0 ? * MON#1 每月第一個星期一午夜

5.1. 宏

對于人類來說,諸如0 0***之類的表達式很難解析,是以在出現錯誤時很難修複。為了提高可讀性,Spring支援以下宏,這些宏表示常用的序列。您可以使用這些宏而不是六位數的值,例如:@Scheduled(cron=“@hourly”)。

意義
@yearly(或@annually) 每年一次(0 0 0 1 1 *)
@monthly 每月一次(0 0 0 1 * *)
@weekly 每周一次(0 0 0 * * 0)
@daily(或@midnight) 每天一次 (),或0 0 0 * * *
@hourly 每小時一次,(0 0 * * * *)