任務執行與排程

Spring Framework 分別透過 TaskExecutorTaskScheduler 介面提供了任務非同步執行和排程的抽象。Spring 還提供了這些介面的實現,支援執行緒池或在應用伺服器環境中委託給 CommonJ。最終,在通用介面後使用這些實現抽象了 Java SE 和 Jakarta EE 環境之間的差異。

Spring 還提供了整合類,支援使用 Quartz Scheduler 進行排程。

Spring TaskExecutor 抽象

Executors 是 JDK 對執行緒池概念的命名。“executor” 的命名是因為無法保證底層實現確實是執行緒池。Executor 可以是單執行緒的,甚至是同步的。Spring 的抽象隱藏了 Java SE 和 Jakarta EE 環境之間的實現細節。

Spring 的 TaskExecutor 介面與 java.util.concurrent.Executor 介面相同。實際上,最初它存在的主要原因是為了在使用執行緒池時抽象掉對 Java 5 的依賴。該介面有一個方法 (execute(Runnable task)),根據執行緒池的語義和配置接受一個任務來執行。

TaskExecutor 最初建立的目的是為了給其他 Spring 元件提供執行緒池抽象。諸如 ApplicationEventMulticaster、JMS 的 AbstractMessageListenerContainer 和 Quartz 整合等元件都使用 TaskExecutor 抽象來管理執行緒池。但是,如果您的 Bean 需要執行緒池行為,您也可以使用此抽象來滿足您的需求。

TaskExecutor 型別

Spring 包含了一些預構建的 TaskExecutor 實現。在大多數情況下,您應該不需要自己實現。Spring 提供的變體如下

  • SyncTaskExecutor: 此實現不會非同步執行呼叫。相反,每次呼叫都在呼叫執行緒中發生。它主要用於不需要多執行緒的情況,例如簡單的測試用例。

  • SimpleAsyncTaskExecutor: 此實現不重用任何執行緒。相反,它為每次呼叫啟動一個新執行緒。但是,它確實支援一個併發限制,超出限制的呼叫會被阻塞,直到有空閒槽位。如果您需要真正的執行緒池,請參閱列表後面的 ThreadPoolTaskExecutor。當啟用 "virtualThreads" 選項時,它將使用 JDK 21 的虛擬執行緒。此實現還透過 Spring 的生命週期管理支援優雅停機。

  • ConcurrentTaskExecutor: 此實現是 java.util.concurrent.Executor 例項的介面卡。有一個替代方案 (ThreadPoolTaskExecutor),它將 Executor 配置引數暴露為 bean 屬性。通常很少需要直接使用 ConcurrentTaskExecutor。但是,如果 ThreadPoolTaskExecutor 對於您的需求不夠靈活,ConcurrentTaskExecutor 是一個替代方案。

  • ThreadPoolTaskExecutor: 此實現是最常用的。它暴露了配置 java.util.concurrent.ThreadPoolExecutor 的 bean 屬性,並將其封裝在 TaskExecutor 中。如果您需要適配不同型別的 java.util.concurrent.Executor,我們建議您改用 ConcurrentTaskExecutor。它還透過 Spring 的生命週期管理提供了暫停/恢復功能和優雅停機。

  • DefaultManagedTaskExecutor: 此實現在 JSR-236 相容的執行時環境(例如 Jakarta EE 應用伺服器)中使用透過 JNDI 獲取的 ManagedExecutorService,取代 CommonJ WorkManager 來實現此目的。

使用 TaskExecutor

Spring 的 TaskExecutor 實現通常與依賴注入一起使用。在以下示例中,我們定義了一個使用 ThreadPoolTaskExecutor 非同步列印一組訊息的 bean

  • Java

  • Kotlin

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));
		}
	}
}
class TaskExecutorExample(private val taskExecutor: TaskExecutor) {

	private inner class MessagePrinterTask(private val message: String) : Runnable {
		override fun run() {
			println(message)
		}
	}

	fun printMessages() {
		for (i in 0..24) {
			taskExecutor.execute(
				MessagePrinterTask(
					"Message$i"
				)
			)
		}
	}
}

如您所見,您不是從執行緒池中獲取執行緒並自己執行,而是將 Runnable 新增到佇列中。然後 TaskExecutor 使用其內部規則來決定何時執行任務。

要配置 TaskExecutor 使用的規則,我們暴露了簡單的 bean 屬性

  • Java

  • Kotlin

  • Xml

@Bean
ThreadPoolTaskExecutor taskExecutor() {
	ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
	taskExecutor.setCorePoolSize(5);
	taskExecutor.setMaxPoolSize(10);
	taskExecutor.setQueueCapacity(25);
	return taskExecutor;
}

@Bean
TaskExecutorExample taskExecutorExample(ThreadPoolTaskExecutor taskExecutor) {
	return new TaskExecutorExample(taskExecutor);
}
@Bean
fun taskExecutor() = ThreadPoolTaskExecutor().apply {
	corePoolSize = 5
	maxPoolSize = 10
	queueCapacity = 25
}

@Bean
fun taskExecutorExample(taskExecutor: ThreadPoolTaskExecutor) = TaskExecutorExample(taskExecutor)
<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>

大多數 TaskExecutor 實現提供了一種使用 TaskDecorator 自動包裝提交的任務的方法。裝飾器應該委託給它包裝的任務,可能在任務執行之前/之後實現自定義行為。

讓我們考慮一個簡單的實現,它將在任務執行之前和之後記錄訊息

  • Java

  • Kotlin

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.core.task.TaskDecorator;

public class LoggingTaskDecorator implements TaskDecorator {

	private static final Log logger = LogFactory.getLog(LoggingTaskDecorator.class);

	@Override
	public Runnable decorate(Runnable runnable) {
		return () -> {
			logger.debug("Before execution of " + runnable);
			runnable.run();
			logger.debug("After execution of " + runnable);
		};
	}
}
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.springframework.core.task.TaskDecorator

class LoggingTaskDecorator : TaskDecorator {

	override fun decorate(runnable: Runnable): Runnable {
		return Runnable {
			logger.debug("Before execution of $runnable")
			runnable.run()
			logger.debug("After execution of $runnable")
		}
	}

	companion object {
		private val logger: Log = LogFactory.getLog(
			LoggingTaskDecorator::class.java
		)
	}
}

然後我們可以在 TaskExecutor 例項上配置我們的裝飾器

  • Java

  • Kotlin

  • Xml

@Bean
ThreadPoolTaskExecutor decoratedTaskExecutor() {
	ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
	taskExecutor.setTaskDecorator(new LoggingTaskDecorator());
	return taskExecutor;
}
@Bean
fun decoratedTaskExecutor() = ThreadPoolTaskExecutor().apply {
	setTaskDecorator(LoggingTaskDecorator())
}
<bean id="decoratedTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
	<property name="taskDecorator" ref="loggingTaskDecorator"/>
</bean>

如果需要多個裝飾器,可以使用 org.springframework.core.task.support.CompositeTaskDecorator 按順序執行多個裝飾器。

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。這將導致任務在指定時間後執行一次。所有其他方法都能夠排程任務重複執行。固定速率(fixed-rate)和固定延遲(fixed-delay)方法用於簡單的週期性執行,但接受 Trigger 的方法更加靈活。

Trigger 介面

Trigger 介面本質上受到 JSR-236 的啟發。Trigger 的基本思想是執行時間可以基於過去的執行結果或甚至任意條件來確定。如果這些確定考慮了先前執行的結果,則該資訊可在 TriggerContext 中獲取。Trigger 介面本身非常簡單,如下所示

public interface Trigger {

	Instant nextExecution(TriggerContext triggerContext);
}

TriggerContext 是最重要的部分。它封裝了所有相關資料,並在將來需要時可進行擴充套件。TriggerContext 是一個介面(預設使用 SimpleTriggerContext 實現)。以下列表顯示了 Trigger 實現可用的方法。

public interface TriggerContext {

	Clock getClock();

	Instant lastScheduledExecution();

	Instant lastActualExecution();

	Instant lastCompletion();
}

Trigger 實現

Spring 提供了 Trigger 介面的兩種實現。最有趣的是 CronTrigger。它基於 cron 表示式 啟用任務排程。例如,以下任務被排程在每個小時的 15 分鐘後執行,但僅在工作日的 9 點到 17 點的“工作時間”內執行

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

另一個實現是 PeriodicTrigger,它接受一個固定週期、一個可選的初始延遲值,以及一個布林值指示週期應解釋為固定速率還是固定延遲。由於 TaskScheduler 介面已經定義了以固定速率或固定延遲排程任務的方法,所以應儘可能直接使用這些方法。PeriodicTrigger 實現的價值在於您可以在依賴 Trigger 抽象的元件中使用它。例如,允許週期性觸發器、基於 cron 的觸發器甚至自定義觸發器實現可互換使用可能會很方便。此類元件可以利用依賴注入,以便您可以從外部配置此類 Triggers,從而輕鬆修改或擴充套件它們。

TaskScheduler 實現

與 Spring 的 TaskExecutor 抽象一樣,TaskScheduler 設計的主要優點是應用程式的排程需求與部署環境解耦。當部署到應用伺服器環境時,這種抽象級別尤為重要,因為在該環境中不應由應用程式本身直接建立執行緒。對於此類場景,Spring 提供了 DefaultManagedTaskScheduler,它在 Jakarta EE 環境中委託給 JSR-236 ManagedScheduledExecutorService

當不需要外部執行緒管理時,一個更簡單的替代方案是在應用程式內部設定一個本地 ScheduledExecutorService,這可以透過 Spring 的 ConcurrentTaskScheduler 進行適配。為了方便起見,Spring 還提供了一個 ThreadPoolTaskScheduler,它內部委託給一個 ScheduledExecutorService,以提供類似於 ThreadPoolTaskExecutor 的通用 bean 風格配置。這些變體在寬鬆的應用伺服器環境(尤其是在 Tomcat 和 Jetty 上)中也適用於本地嵌入式執行緒池設定。

從 6.1 版本開始,ThreadPoolTaskScheduler 透過 Spring 的生命週期管理提供了暫停/恢復功能和優雅停機。還有一個新選項 SimpleAsyncTaskScheduler,它與 JDK 21 的虛擬執行緒對齊,使用單個排程器執行緒,但為每個排程任務執行啟動一個新執行緒(固定延遲任務除外,它們都在單個排程器執行緒上執行,因此對於此與虛擬執行緒對齊的選項,建議使用固定速率和 cron 觸發器)。

排程和非同步執行的註解支援

Spring 為任務排程和非同步方法執行提供了註解支援。

啟用排程註解

要啟用對 @Scheduled@Async 註解的支援,您可以將 @EnableScheduling@EnableAsync 新增到您的 @Configuration 類之一,或者新增 <task:annotation-driven> 元素,如下例所示

  • Java

  • Kotlin

  • Xml

@Configuration
@EnableAsync
@EnableScheduling
public class SchedulingConfiguration {
}
@Configuration
@EnableAsync
@EnableScheduling
class SchedulingConfiguration
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:task="http://www.springframework.org/schema/task"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans
	   https://www.springframework.org/schema/beans/spring-beans.xsd
	   http://www.springframework.org/schema/task
	   https://www.springframework.org/schema/task/spring-task.xsd">

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

您可以為您的應用程式選擇相關的註解。例如,如果您只需要 @Scheduled 支援,您可以省略 @EnableAsync。為了進行更精細的控制,您還可以實現 SchedulingConfigurer 介面、AsyncConfigurer 介面,或兩者都實現。有關詳細資訊,請參閱 SchedulingConfigurerAsyncConfigurer 的 javadoc。

請注意,對於前面的 XML,為處理帶有 @Async 註解的方法對應的任務提供了 executor 引用,併為管理帶有 @Scheduled 註解的方法提供了 scheduler 引用。

處理 @Async 註解的預設通知模式是 proxy,它只允許透過代理攔截呼叫。同一類中的本地呼叫無法透過這種方式攔截。對於更高階的攔截模式,請考慮結合編譯時或載入時織入切換到 aspectj 模式。

@Scheduled 註解

您可以將 @Scheduled 註解新增到方法中,並附帶觸發器元資料。例如,以下方法以固定延遲每五秒鐘(5000 毫秒)呼叫一次,這意味著週期是從前一次呼叫的完成時間開始計算的。

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

預設情況下,毫秒將用作固定延遲(fixed delay)、固定速率(fixed rate)和初始延遲(initial delay)值的時間單位。如果您想使用不同的時間單位,例如秒或分鐘,可以透過 @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
}

對於一次性任務,您只需透過指定在方法預期執行之前需要等待的時間量來設定初始延遲。

@Scheduled(initialDelay = 1000)
public void doSomething() {
	// something that should run only once
}

如果簡單的週期性排程表達能力不夠,您可以提供一個 cron 表示式。以下示例僅在工作日執行。

@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
	// something that should run on weekdays only
}
您還可以使用 zone 屬性指定解析 cron 表示式的時區。

請注意,要排程的方法必須返回 void 並且不能接受任何引數。如果方法需要與應用程式上下文中的其他物件互動,通常應透過依賴注入提供這些物件。

@Scheduled 可以用作可重複註解。如果在同一方法上找到多個排程宣告,它們將獨立處理,每個宣告都會觸發一個單獨的觸發器。因此,這些共存的排程可能會重疊並並行或立即連續執行多次。請確保您指定的 cron 表示式等不會意外重疊。

從 Spring Framework 4.3 開始,任何作用域的 bean 都支援 @Scheduled 方法。

請確保在執行時不會初始化同一 @Scheduled 註解類的多個例項,除非您確實想為每個此類例項排程回撥。與此相關,請確保不要在已標註 @Scheduled 並作為普通 Spring bean 註冊到容器中的 bean 類上使用 @Configurable。否則,您將獲得雙重初始化(一次透過容器,一次透過 @Configurable 切面),結果是每個 @Scheduled 方法都會被呼叫兩次。

Reactive 方法或 Kotlin suspending 函式上的 @Scheduled 註解

從 Spring Framework 6.1 開始,@Scheduled 方法也支援幾種型別的 reactive 方法:

  • 返回型別為 Publisher(或 Publisher 的任何具體實現)的方法,例如以下示例所示:

@Scheduled(fixedDelay = 500)
public Publisher<Void> reactiveSomething() {
	// return an instance of Publisher
}
  • 可以透過共享的 ReactiveAdapterRegistry 例項適配到 Publisher 的返回型別方法,前提是該型別支援延遲訂閱,例如以下示例所示:

@Scheduled(fixedDelay = 500)
public Single<String> rxjavaNonPublisher() {
	return Single.just("example");
}

CompletableFuture 類就是一個可以通常適配到 Publisher 但不支援延遲訂閱的型別示例。其在登錄檔中的 ReactiveAdapter 透過 getDescriptor().isDeferred() 方法返回 false 來表示這一點。

  • Kotlin suspending 函式,例如以下示例所示:

@Scheduled(fixedDelay = 500)
suspend fun something() {
	// do something asynchronous
}
  • 返回 Kotlin FlowDeferred 例項的方法,例如以下示例所示:

@Scheduled(fixedDelay = 500)
fun something(): Flow<Void> {
	flow {
		// do something asynchronous
	}
}

所有這些型別的方法都必須宣告時沒有引數。對於 Kotlin suspending 函式,還必須存在 kotlinx.coroutines.reactor 橋接器,以允許框架將 suspending 函式作為 Publisher 呼叫。

Spring Framework 將為帶有註解的方法獲取一個 Publisher,然後排程一個 Runnable,在該 Runnable 中它訂閱該 Publisher。這些內部的常規訂閱根據相應的 cron/fixedDelay/fixedRate 配置發生。

如果 Publisher 發出 onNext 訊號,這些訊號將被忽略和丟棄(就像同步 @Scheduled 方法的返回值被忽略一樣)。

在下面的示例中,Flux 每 5 秒發出 onNext("Hello"), onNext("World"),但這些值未使用。

@Scheduled(initialDelay = 5000, fixedRate = 5000)
public Flux<String> reactiveSomething() {
	return Flux.just("Hello", "World");
}

如果 Publisher 發出 onError 訊號,它將被記錄在 WARN 級別並恢復。由於 Publisher 例項的非同步和延遲特性,異常不會從 Runnable 任務中丟擲:這意味著 ErrorHandler 契約不適用於 reactive 方法。

因此,儘管發生錯誤,後續的排程訂閱仍然會發生。

在下面的示例中,Mono 訂閱在前五秒內失敗了兩次。然後訂閱開始成功,每五秒向標準輸出列印一條訊息。

@Scheduled(initialDelay = 0, fixedRate = 5000)
public Mono<Void> reactiveSomething() {
	AtomicInteger countdown = new AtomicInteger(2);

	return Mono.defer(() -> {
		if (countDown.get() == 0 || countDown.decrementAndGet() == 0) {
			return Mono.fromRunnable(() -> System.out.println("Message"));
		}
		return Mono.error(new IllegalStateException("Cannot deliver message"));
	})
}

銷燬帶註解的 bean 或關閉應用程式上下文時,Spring Framework 會取消計劃的任務,這包括下次對 Publisher 的計劃訂閱以及任何仍在活動中的過去訂閱(例如,對於長時間執行的或甚至無限的釋出者)。

@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 型別的返回值。這仍然提供了非同步執行的好處,以便呼叫者可以在呼叫該 Futureget() 之前執行其他任務。以下示例顯示瞭如何在返回值的方法上使用 @Async

@Async
Future<String> returnSomething(int i) {
	// this will be run asynchronously
}
@Async 方法不僅可以宣告常規的 java.util.concurrent.Future 返回型別,還可以宣告 Spring 的 org.springframework.util.concurrent.ListenableFuture,或者從 Spring 4.2 開始,JDK 8 的 java.util.concurrent.CompletableFuture,以便與非同步任務進行更豐富的互動,並與後續的處理步驟立即組合。

您不能將 @Async 與生命週期回撥一起使用,例如 @PostConstruct。目前,要非同步初始化 Spring bean,您必須使用一個單獨的初始化 Spring bean,該 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();
	}

}
@Async 沒有直接的 XML 等效項,因為此類方法首先應該設計為非同步執行,而不是外部重新宣告為非同步。但是,您可以手動透過 Spring AOP 結合自定義切入點設定 Spring 的 AsyncExecutionInterceptor

使用 @Async 進行執行器限定

預設情況下,在方法上指定 @Async 時,使用的執行器是 啟用非同步支援時配置的執行器,即如果使用 XML 則是“annotation-driven”元素,或者如果存在,則是您的 AsyncConfigurer 實現。但是,當您需要指定執行給定方法時應使用預設執行器以外的其他執行器時,您可以使用 @Async 註解的 value 屬性。以下示例顯示瞭如何執行此操作:

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

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

使用 @Async 進行異常管理

@Async 方法具有 Future 型別的返回值時,管理方法執行期間丟擲的異常很容易,因為在呼叫 Future 結果的 get 方法時會丟擲此異常。但是,對於 void 返回型別,異常未被捕獲且無法傳遞。您可以提供一個 AsyncUncaughtExceptionHandler 來處理此類異常。以下示例顯示瞭如何執行此操作:

public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {

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

預設情況下,異常僅被記錄。您可以透過使用 AsyncConfigurer<task:annotation-driven/> XML 元素定義自定義的 AsyncUncaughtExceptionHandler

task 名稱空間

從版本 3.0 開始,Spring 包含一個 XML 名稱空間,用於配置 TaskExecutorTaskScheduler 例項。它還提供了一種便捷的方式來配置需要使用觸發器進行排程的任務。

scheduler 元素

以下元素建立了一個具有指定執行緒池大小的 ThreadPoolTaskScheduler 例項:

<task:scheduler id="scheduler" pool-size="10"/>

id 屬性提供的值用作池中執行緒名稱的字首。scheduler 元素相對簡單。如果您不提供 pool-size 屬性,則預設執行緒池只有一個執行緒。排程器沒有其他配置選項。

executor 元素

以下建立了一個 ThreadPoolTaskExecutor 例項:

<task:executor id="executor" pool-size="10"/>

上一節 中所示的排程器一樣,為 id 屬性提供的值用作池中執行緒名稱的字首。關於池大小,executor 元素比 scheduler 元素支援更多的配置選項。首先,ThreadPoolTaskExecutor 的執行緒池本身更具可配置性。執行器的執行緒池可以為核心大小和最大大小設定不同的值,而不僅僅是單一大小。如果您提供一個單一值,則執行器具有固定大小的執行緒池(核心大小和最大大小相同)。但是,executor 元素的 pool-size 屬性也接受 min-max 形式的範圍。以下示例將最小值為 5,最大值為 25

<task:executor
		id="executorWithPoolSizeRange"
		pool-size="5-25"
		queue-capacity="100"/>

在前面的配置中,還提供了 queue-capacity 值。在考慮執行器的佇列容量時,也應考慮執行緒池的配置。有關池大小和佇列容量之間關係的完整說明,請參閱 ThreadPoolExecutor 的文件。主要思想是,當提交任務時,如果當前活動執行緒數小於核心大小,執行器首先嚐試使用空閒執行緒。如果已達到核心大小,只要佇列容量未滿,任務就會被新增到佇列中。只有當佇列容量已滿時,執行器才會建立超出核心大小的新執行緒。如果也已達到最大大小,則執行器將拒絕任務。

預設情況下,佇列是無界的,但這很少是期望的配置,因為它可能導致 OutOfMemoryError,如果所有池執行緒都忙時向佇列中新增足夠多的任務。此外,如果佇列是無界的,最大大小完全沒有效果。由於執行器在建立超出核心大小的新執行緒之前總是先嚐試佇列,因此佇列必須具有有限容量,執行緒池才能增長到超出核心大小(這就是為什麼使用無界佇列時,固定大小池是唯一合理的情況)。

考慮上面提到的任務被拒絕的情況。預設情況下,當任務被拒絕時,執行緒池執行器會丟擲 TaskRejectedException。但是,拒絕策略實際上是可配置的。當使用預設拒絕策略(即 AbortPolicy 實現)時會丟擲異常。對於在高負載下可以跳過某些任務的應用程式,您可以配置 DiscardPolicyDiscardOldestPolicy。另一個在高負載下需要限制提交任務數量的應用程式的良好選項是 CallerRunsPolicy。該策略不是丟擲異常或丟棄任務,而是強制呼叫提交方法的執行緒自己執行任務。其思想是,此類呼叫者在執行該任務時很忙,無法立即提交其他任務。因此,它提供了一種簡單的方式來限制傳入負載,同時保持執行緒池和佇列的限制。通常,這使得執行器能夠“趕上”正在處理的任務,從而釋放佇列、池或兩者的部分容量。您可以從 executor 元素的 rejection-policy 屬性可用的列舉值中選擇任何一個選項。

以下示例顯示了一個 executor 元素,其中包含許多屬性來指定各種行為:

<task:executor
		id="executorWithCallerRunsPolicy"
		pool-size="5-25"
		queue-capacity="100"
		rejection-policy="CALLER_RUNS"/>

最後,keep-alive 設定確定執行緒在停止之前可以保持空閒的時間限制(以秒為單位)。如果池中當前執行緒數大於核心數,在等待此時間量而沒有處理任務後,多餘的執行緒將被停止。時間值為零會導致多餘的執行緒在執行任務後立即停止,而佇列中沒有後續工作。以下示例將 keep-alive 值設定為兩分鐘:

<task:executor
		id="executorWithKeepAlive"
		pool-size="5-25"
		keep-alive="120"/>

scheduled-tasks 元素

Spring 任務名稱空間最強大的特性是支援在 Spring Application Context 中配置計劃任務。這遵循了 Spring 中其他“方法呼叫者”類似的方法,例如 JMS 名稱空間為配置訊息驅動 POJO 提供的方法。基本上,ref 屬性可以指向任何 Spring 管理的物件,而 method 屬性提供在該物件上呼叫的方法的名稱。以下列表顯示了一個簡單示例:

<task:scheduled-tasks scheduler="myScheduler">
	<task:scheduled ref="beanA" method="methodA" fixed-delay="5000"/>
</task:scheduled-tasks>

<task:scheduler id="myScheduler" pool-size="10"/>

排程器由外部元素引用,每個單獨的任務包含其觸發器元資料的配置。在前面的示例中,該元資料定義了一個週期性觸發器,其固定延遲指示每次任務執行完成後的等待毫秒數。另一個選項是 fixed-rate,指示無論任何先前執行需要多長時間,該方法應多久執行一次。此外,對於 fixed-delayfixed-rate 任務,您可以指定 'initial-delay' 引數,指示在方法首次執行之前需要等待的毫秒數。為了獲得更多控制,您可以提供一個 cron 屬性來提供一個 cron 表示式。以下示例顯示了這些其他選項:

<task:scheduled-tasks scheduler="myScheduler">
	<task:scheduled ref="beanA" method="methodA" fixed-delay="5000" initial-delay="1000"/>
	<task:scheduled ref="beanB" method="methodB" fixed-rate="5000"/>
	<task:scheduled ref="beanC" method="methodC" cron="*/5 * * * * MON-FRI"/>
</task:scheduled-tasks>

<task:scheduler id="myScheduler" pool-size="10"/>

Cron 表示式

所有 Spring cron 表示式都必須符合相同的格式,無論您是在 @Scheduled 註解task:scheduled-tasks 元素 還是其他地方使用它們。一個格式良好的 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)
 │ │ │ │ │ │
 * * * * * *

有一些規則適用:

  • 欄位可以是星號(*),它始終代表“從頭到尾”。對於月中某天或週中某天欄位,可以使用問號(?)代替星號。

  • 逗號(,)用於分隔列表項。

  • 由連字元(-)分隔的兩個數字表示一個數字範圍。指定的範圍包含邊界值。

  • 範圍(或 *)後面跟著 / 指定該數字值在範圍內的間隔。

  • 也可以使用英文名稱表示月份和週中某天欄位。使用特定天或月份的前三個字母(不區分大小寫)。

  • 月中某天和週中某天欄位可以包含 L 字元,它具有不同的含義。

    • 在月中某天欄位中,L 表示該月的最後一天。如果後跟負偏移量(即 L-n),則表示該月倒數第 n

    • 在週中某天欄位中,L 表示該月的最後一天。如果字首為數字或三個字母的名稱(dLDDDL),則表示該月第 d 天(或 DDD)的最後一天

  • 月中某天欄位可以是 nW,表示離該月第 n 天最近的工作日。如果 n 是星期六,則表示前一個星期五。如果 n 是星期日,則表示後一個星期一;如果 n1 且是星期六(即 1W 表示該月的第一個工作日),也同樣如此。

  • 如果月中某天欄位是 LW,則表示該月的最後一個工作日

  • 週中某天欄位可以是 d#n(或 DDD#n),表示該月第 d 天(或 DDD)的第 n 次出現

以下是一些示例:

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

每月第一個星期一午夜

宏(Macros)

0 0 * * * * 這樣的表示式對人類來說很難解析,因此也難以修復 bug。為了提高可讀性,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 * * * *)

使用 Quartz 排程器

Quartz 使用 TriggerJobJobDetail 物件來實現各種作業的排程。有關 Quartz 的基本概念,請參閱 Quartz 網站。為了方便起見,Spring 提供了一些類,簡化了在基於 Spring 的應用程式中使用 Quartz 的過程。

使用 JobDetailFactoryBean

Quartz JobDetail 物件包含執行作業所需的所有資訊。Spring 提供了一個 JobDetailFactoryBean,它提供 bean 樣式屬性用於 XML 配置。考慮以下示例:

<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
	<property name="jobClass" value="example.ExampleJob"/>
	<property name="jobDataAsMap">
		<map>
			<entry key="timeout" value="5"/>
		</map>
	</property>
</bean>

作業詳情配置包含了執行作業(ExampleJob)所需的所有資訊。超時時間在作業資料對映中指定。作業資料對映可以透過 JobExecutionContext(在執行時傳遞給您)獲得,但 JobDetail 也從對映到作業例項屬性的作業資料中獲取其屬性。因此,在以下示例中,ExampleJob 包含一個名為 timeout 的 bean 屬性,並且 JobDetail 會自動應用它。

package example;

public class ExampleJob extends QuartzJobBean {

	private int timeout;

	/**
	 * Setter called after the ExampleJob is instantiated
	 * with the value from the JobDetailFactoryBean.
	 */
	public void setTimeout(int timeout) {
		this.timeout = timeout;
	}

	protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
		// do the actual work
	}
}

作業資料對映中的所有附加屬性也都可以供您使用。

透過使用 namegroup 屬性,您可以分別修改作業的名稱和組。預設情況下,作業的名稱與 JobDetailFactoryBean 的 bean 名稱(在上面的示例中為 exampleJob)匹配。

使用 MethodInvokingJobDetailFactoryBean

通常您只需要呼叫特定物件上的一個方法。透過使用 MethodInvokingJobDetailFactoryBean,您可以完全做到這一點,如下面的示例所示。

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
	<property name="targetObject" ref="exampleBusinessObject"/>
	<property name="targetMethod" value="doIt"/>
</bean>

前面的示例導致在 exampleBusinessObject 方法上呼叫 doIt 方法,如下面的示例所示。

public class ExampleBusinessObject {

	// properties and collaborators

	public void doIt() {
		// do the actual work
	}
}
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>

透過使用 MethodInvokingJobDetailFactoryBean,您無需建立僅呼叫方法的單行作業。您只需建立實際的業務物件並連線詳情物件即可。

預設情況下,Quartz 作業是無狀態的,這可能導致作業之間相互干擾。如果您為同一個 JobDetail 指定兩個觸發器,第二個觸發器可能在第一個作業完成之前啟動。如果 JobDetail 類實現 Stateful 介面,這種情況就不會發生:第二個作業在第一個作業完成之前不會啟動。

要使 MethodInvokingJobDetailFactoryBean 產生的作業成為非併發的,請將 concurrent 標誌設定為 false,如下面的示例所示。

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
	<property name="targetObject" ref="exampleBusinessObject"/>
	<property name="targetMethod" value="doIt"/>
	<property name="concurrent" value="false"/>
</bean>
預設情況下,作業將以併發方式執行。

使用觸發器和 SchedulerFactoryBean 連線作業

我們已經建立了作業詳情和作業。我們也回顧了允許您在特定物件上呼叫方法的便捷 bean。當然,我們仍然需要排程作業本身。這是透過使用觸發器和 SchedulerFactoryBean 來完成的。Quartz 中有幾種觸發器可用,Spring 提供了兩個具有便捷預設設定的 Quartz FactoryBean 實現:CronTriggerFactoryBeanSimpleTriggerFactoryBean

觸發器需要被排程。Spring 提供了一個 SchedulerFactoryBean,它將觸發器暴露為屬性進行設定。SchedulerFactoryBean 使用這些觸發器排程實際的作業。

以下列表同時使用了 SimpleTriggerFactoryBeanCronTriggerFactoryBean

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
	<!-- see the example of method invoking job above -->
	<property name="jobDetail" ref="jobDetail"/>
	<!-- 10 seconds -->
	<property name="startDelay" value="10000"/>
	<!-- repeat every 50 seconds -->
	<property name="repeatInterval" value="50000"/>
</bean>

<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
	<property name="jobDetail" ref="exampleJob"/>
	<!-- run every morning at 6 AM -->
	<property name="cronExpression" value="0 0 6 * * ?"/>
</bean>

前面的示例設定了兩個觸發器,一個每隔 50 秒執行一次,啟動延遲為 10 秒,另一個每天早上 6 點執行。最後,我們需要設定 SchedulerFactoryBean,如下面的示例所示。

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
	<property name="triggers">
		<list>
			<ref bean="cronTrigger"/>
			<ref bean="simpleTrigger"/>
		</list>
	</property>
</bean>

SchedulerFactoryBean 還有更多屬性可用,例如作業詳情使用的日曆、用於自定義 Quartz 的屬性以及 Spring 提供的 JDBC DataSource。有關更多資訊,請參閱 SchedulerFactoryBean 的 javadoc 文件。

SchedulerFactoryBean 也識別類路徑中的 quartz.properties 檔案,基於 Quartz 屬性鍵,與常規 Quartz 配置一樣。請注意,許多 SchedulerFactoryBean 設定與 properties 檔案中的常見 Quartz 設定互動;因此,不建議在兩個級別都指定值。例如,如果您打算依賴 Spring 提供的 DataSource,請不要設定 "org.quartz.jobStore.class" 屬性,或者指定一個 org.springframework.scheduling.quartz.LocalDataSourceJobStore 變體,它是標準 org.quartz.impl.jdbcjobstore.JobStoreTX 的完全替代品。