任務執行與排程

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

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

Spring TaskExecutor 抽象化

Executor 是 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:此實作是最常用的。它公開 Bean 屬性,用於配置 java.util.concurrent.ThreadPoolExecutor 並將其包裝在 TaskExecutor 中。如果您需要適應不同種類的 java.util.concurrent.Executor,我們建議您改用 ConcurrentTaskExecutor。它還透過 Spring 的生命週期管理提供暫停/恢復功能和優雅關閉。

  • DefaultManagedTaskExecutor:此實作在 JSR-236 相容的運行時環境(例如 Jakarta EE 應用程式伺服器)中使用 JNDI 取得的 ManagedExecutorService,以取代 CommonJ WorkManager 的用途。

使用 TaskExecutor

Spring 的 TaskExecutor 實作通常與依賴注入一起使用。在以下範例中,我們定義了一個 Bean,該 Bean 使用 ThreadPoolTaskExecutor 非同步印出一組訊息:

  • 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 提交的任務的方法。Decorator 應該委派給它正在包裝的任務,可能會在任務執行之前/之後實作自訂行為。

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

  • 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 實例上配置我們的 decorator:

  • 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>

如果需要多個 decorator,則可以使用 org.springframework.core.task.support.CompositeTaskDecorator 來依序執行多個 decorator。

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 的方法,它僅採用 RunnableInstant。這會導致任務在指定時間後運行一次。所有其他方法都能够排程任務重複運行。固定速率和固定延遲方法用於簡單的定期執行,但是接受 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 點到 5 點「工作時間」內運行:

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

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

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,提供了 executor 參考,用於處理那些與具有 @Async 註解的方法相對應的任務,並且提供了 scheduler 參考,用於管理那些使用 @Scheduled 註解的方法。

處理 @Async 註解的預設建議模式是 proxy,它僅允許透過 Proxy 攔截調用。同一類別中的本機調用無法透過這種方式攔截。對於更進階的攔截模式,請考慮切換到 aspectj 模式,並結合編譯時或載入時編織。

@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
}

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

@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 回傳值,並且不得接受任何引數。如果該方法需要與應用程式 Context 中的其他物件互動,則通常會透過依賴注入提供這些物件。

@Scheduled 可以用作可重複的註解。如果在同一方法上找到多個排程宣告,則將獨立處理每個宣告,並且每個宣告都有單獨的觸發器觸發。因此,此類共置排程可能會重疊並行或緊接著連續執行多次。請確保您指定的 cron 運算式等不會意外重疊。

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

請確保您在運行時沒有初始化同一個 @Scheduled 註解類別的多個實例,除非您確實想要排程對每個此類實例的回呼。與此相關的是,請確保您不要在使用 @Scheduled 註解並註冊為容器的常規 Spring Bean 的 Bean 類別上使用 @Configurable。否則,您將獲得雙重初始化(一次透過容器,一次透過 @Configurable Aspect),後果是每個 @Scheduled 方法都會被調用兩次。

反應式方法或 Kotlin 暫停函數上的 @Scheduled 註解

從 Spring Framework 6.1 開始,反應式方法的幾種類型也支援 @Scheduled 方法:

  • 具有 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 的類型範例,但不支援延遲訂閱。其在 registry 中的 ReactiveAdapter 通過讓 getDescriptor().isDeferred() 方法回傳 false 來表示這一點。

  • Kotlin 暫停函式,如下列範例所示

@Scheduled(fixedDelay = 500)
suspend fun something() {
	// do something asynchronous
}
  • 回傳 Kotlin FlowDeferred 實例的方法,如下列範例所示

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

所有這些類型的方法都必須宣告為不帶任何引數。在 Kotlin 暫停函式的情況下,也必須存在 kotlinx.coroutines.reactor 橋接器,以允許框架將暫停函式作為 Publisher 呼叫。

Spring Framework 將為帶註解的方法取得一個 Publisher,並排程一個 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 契約不適用於反應式方法。

因此,儘管發生錯誤,仍會進行進一步的排程訂閱。

在以下範例中,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 訂閱以及任何仍在活動中的過去訂閱(例如,對於長時間執行的 publisher 甚至無限 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 類型的回傳值。這仍然提供了非同步執行的好處,以便呼叫者可以在對該 Future 呼叫 get() 之前執行其他任務。以下範例顯示如何在回傳值的方法上使用 @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,然後在目標上調用 @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 進行 Executor 限定

預設情況下,當在方法上指定 @Async 時,使用的 executor 是啟用非同步支援時配置的 executor,也就是說,如果您使用 XML,則為「annotation-driven」元素;如果您有任何 AsyncConfigurer 實作,則為該實作。但是,當您需要指示在執行給定方法時應使用預設以外的 executor 時,可以使用 @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 屬性,則預設執行緒池只有一個執行緒。scheduler 沒有其他配置選項。

executor 元素

以下建立一個 ThreadPoolTaskExecutor 實例

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

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

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

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

預設情況下,佇列是無界的,但這很少是所需的配置,因為如果將足夠多的任務新增到該佇列中,而所有池執行緒都處於忙碌狀態,則可能導致 OutOfMemoryError。此外,如果佇列是無界的,則最大大小根本沒有效果。由於 executor 始終在建立超出核心大小的新執行緒之前嘗試佇列,因此佇列必須具有有限的容量,執行緒池才能超出核心大小增長(這就是為什麼在使用無界佇列時,固定大小的池是唯一明智的情況)。

考慮上述情況,即任務被拒絕。預設情況下,當任務被拒絕時,執行緒池 executor 會拋出 TaskRejectedException。但是,拒絕策略實際上是可配置的。使用預設拒絕策略(即 AbortPolicy 實作)時,會拋出異常。對於在重負載下可以跳過某些任務的應用程式,您可以改為配置 DiscardPolicyDiscardOldestPolicy。對於需要限制重負載下提交任務的應用程式,另一個效果良好的選項是 CallerRunsPolicy。該策略不是拋出異常或丟棄任務,而是強制呼叫提交方法的執行緒本身執行該任務。其想法是,此類呼叫者在執行該任務時很忙,並且無法立即提交其他任務。因此,它提供了一種簡單的方法來限制傳入的負載,同時保持執行緒池和佇列的限制。通常,這允許 executor「趕上」它正在處理的任務,從而釋放佇列、池或兩者上的一些容量。您可以從 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 的 task 命名空間最強大的功能是支援配置要在 Spring 應用程式上下文中排程的任務。這遵循類似於 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"/>

scheduler 由外部元素引用,每個單獨的任務都包含其觸發器元資料的配置。在前面的範例中,該元資料定義了一個週期性觸發器,該觸發器具有固定的延遲,指示在每次任務執行完成後要等待的毫秒數。另一個選項是 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 表達式

無論您是在 @Scheduled 註解task:scheduled-tasks 元素還是在其他地方使用 Spring cron 表達式,所有 Spring cron 表達式都必須符合相同的格式。一個格式正確的 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 代表該週的最後一天。如果以數字或三個字母的名稱 (dLDDDL) 作為前綴,則表示該月中的最後一個星期 (dDDD)

  • 月份中的日期欄位可以是 nW,它代表最接近月份中日期 n 的工作日。如果 n 是星期六,則這會產生之前的星期五。如果 n 是星期日,則這會產生之後的星期一,如果 n1 且是星期六,也會發生這種情況(也就是說: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

每月第一個星期一午夜

巨集

諸如 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 * * * *)

使用 Quartz Scheduler

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

使用 JobDetailFactoryBean

Quartz JobDetail 物件包含執行 job 所需的所有資訊。Spring 提供了 JobDetailFactoryBean,它為 XML 配置目的提供了 bean 樣式的屬性。考慮以下範例

<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>

job 詳細配置具有執行 job (ExampleJob) 所需的所有資訊。逾時在 job 資料映射中指定。job 資料映射可透過 JobExecutionContext(在執行時傳遞給您)取得,但 JobDetail 也會從映射到 job 實例屬性的 job 資料中取得其屬性。因此,在以下範例中,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
	}
}

job 資料映射中的所有其他屬性也可用於您。

通過使用 namegroup 屬性,您可以分別修改 job 的名稱和群組。預設情況下,job 的名稱與 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,您無需建立僅調用方法的單行 job。您只需要建立實際的業務物件並連接詳細物件即可。

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

要使從 MethodInvokingJobDetailFactoryBean 產生的 job 非同步執行,請將 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>
預設情況下,job 將以同步方式執行。

通過使用觸發器和 SchedulerFactoryBean 連接 Job

我們已經建立了 job 詳細資訊和 job。我們也回顧了方便的 bean,它讓您可以在特定物件上調用方法。當然,我們仍然需要排程 job 本身。這是通過使用觸發器和 SchedulerFactoryBean 來完成的。Quartz 內有多個觸發器可用,Spring 提供了兩個 Quartz FactoryBean 實作,並具有方便的預設值:CronTriggerFactoryBeanSimpleTriggerFactoryBean

觸發器需要排程。Spring 提供了 SchedulerFactoryBean,它公開了要設定為屬性的觸發器。SchedulerFactoryBean 使用這些觸發器排程實際的 job。

以下清單同時使用 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 還有更多屬性可用,例如 job 詳細資訊使用的日曆、用於自訂 Quartz 的屬性以及 Spring 提供的 JDBC DataSource。有關更多資訊,請參閱 SchedulerFactoryBean javadoc。

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