任務執行與排程
Spring Framework 分別透過 TaskExecutor
和 TaskScheduler
介面,為任務的非同步執行和排程提供抽象化。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
的方法,它僅採用 Runnable
和 Instant
。這會導致任務在指定時間後運行一次。所有其他方法都能够排程任務重複運行。固定速率和固定延遲方法用於簡單的定期執行,但是接受 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
介面或兩者都實作。請參閱 SchedulingConfigurer
和 AsyncConfigurer
javadoc 以取得完整詳細資訊。
請注意,對於先前的 XML,提供了 executor 參考,用於處理那些與具有 @Async
註解的方法相對應的任務,並且提供了 scheduler 參考,用於管理那些使用 @Scheduled
註解的方法。
處理 @Async 註解的預設建議模式是 proxy ,它僅允許透過 Proxy 攔截調用。同一類別中的本機調用無法透過這種方式攔截。對於更進階的攔截模式,請考慮切換到 aspectj 模式,並結合編譯時或載入時編織。 |
@Scheduled
註解
您可以將 @Scheduled
註解與觸發器中繼資料一起新增到方法中。例如,以下方法每五秒 (5000 毫秒) 調用一次,並具有固定延遲,這表示該週期是從每個先前調用的完成時間開始測量的。
@Scheduled(fixedDelay = 5000)
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 都支援 請確保您在運行時沒有初始化同一個 |
反應式方法或 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");
}
|
-
Kotlin 暫停函式,如下列範例所示
@Scheduled(fixedDelay = 500)
suspend fun something() {
// do something asynchronous
}
-
回傳 Kotlin
Flow
或Deferred
實例的方法,如下列範例所示
@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 會取消排程的任務,其中包括下一個排程的 |
@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 命名空間,用於配置 TaskExecutor
和 TaskScheduler
實例。它還提供了一種方便的方式來配置要使用觸發器排程的任務。
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
實作)時,會拋出異常。對於在重負載下可以跳過某些任務的應用程式,您可以改為配置 DiscardPolicy
或 DiscardOldestPolicy
。對於需要限制重負載下提交任務的應用程式,另一個效果良好的選項是 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-delay
和 fixed-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
代表該週的最後一天。如果以數字或三個字母的名稱 (dL
或DDDL
) 作為前綴,則表示該月中的最後一個星期 (d
或DDD
)。
-
-
月份中的日期欄位可以是
nW
,它代表最接近月份中日期n
的工作日。如果n
是星期六,則這會產生之前的星期五。如果n
是星期日,則這會產生之後的星期一,如果n
是1
且是星期六,也會發生這種情況(也就是說:1W
代表該月的第一個工作日)。 -
如果月份中的日期欄位是
LW
,則表示該月的最後一個工作日。 -
星期中的日期欄位可以是
d#n
(或DDD#n
),它代表該月中的第n
個星期d
(或DDD
)。
以下是一些範例
Cron 表達式 | 含義 |
---|---|
|
每天每小時的頂端 |
|
每十秒 |
|
每天 8 點、9 點和 10 點 |
|
每天早上 6:00 和晚上 7:00 |
|
每天 8:00、8:30、9:00、9:30、10:00 和 10:30 |
|
工作日朝九晚五的整點 |
|
每年聖誕節午夜 |
|
每月最後一天午夜 |
|
每月倒數第三天午夜 |
|
每月最後一個星期五午夜 |
|
每月最後一個星期四午夜 |
|
每月第一個工作日午夜 |
|
每月最後一個工作日午夜 |
|
每月第二個星期五午夜 |
|
每月第一個星期一午夜 |
巨集
諸如 0 0 * * * *
之類的表達式對於人類來說很難解析,因此,如果出現錯誤,也很難修復。為了提高可讀性,Spring 支援以下巨集,這些巨集表示常用的序列。您可以改用這些巨集而不是六位數的值,如下所示:@Scheduled(cron = "@hourly")
。
巨集 | 含義 |
---|---|
|
每年一次 ( |
|
每月一次 ( |
|
每週一次 ( |
|
每天一次 ( |
|
每小時一次,( |
使用 Quartz Scheduler
Quartz 使用 Trigger
、Job
和 JobDetail
物件來實現各種 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 資料映射中的所有其他屬性也可用於您。
通過使用 name 和 group 屬性,您可以分別修改 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
實作,並具有方便的預設值:CronTriggerFactoryBean
和 SimpleTriggerFactoryBean
。
觸發器需要排程。Spring 提供了 SchedulerFactoryBean
,它公開了要設定為屬性的觸發器。SchedulerFactoryBean
使用這些觸發器排程實際的 job。
以下清單同時使用 SimpleTriggerFactoryBean
和 CronTriggerFactoryBean
<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 的完整替代品。 |