任務執行與排程
Spring Framework 分別透過 TaskExecutor
和 TaskScheduler
介面提供了任務非同步執行和排程的抽象。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
介面,或兩者都實現。有關詳細資訊,請參閱 SchedulingConfigurer
和 AsyncConfigurer
的 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)值的時間單位。如果您想使用不同的時間單位,例如秒或分鐘,可以透過 例如,前面的例子也可以寫成如下形式。
|
如果您需要固定速率的執行,可以使用註解中的 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 都支援 請確保在執行時不會初始化同一 |
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");
}
|
-
Kotlin suspending 函式,例如以下示例所示:
@Scheduled(fixedDelay = 500)
suspend fun something() {
// do something asynchronous
}
-
返回 Kotlin
Flow
或Deferred
例項的方法,例如以下示例所示:
@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 會取消計劃的任務,這包括下次對 |
@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,該 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 名稱空間,用於配置 TaskExecutor
和 TaskScheduler
例項。它還提供了一種便捷的方式來配置需要使用觸發器進行排程的任務。
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
實現)時會丟擲異常。對於在高負載下可以跳過某些任務的應用程式,您可以配置 DiscardPolicy
或 DiscardOldestPolicy
。另一個在高負載下需要限制提交任務數量的應用程式的良好選項是 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-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 表示式
所有 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
表示該月的最後一天。如果字首為數字或三個字母的名稱(dL
或DDDL
),則表示該月第d
天(或DDD
)的最後一天。
-
-
月中某天欄位可以是
nW
,表示離該月第n
天最近的工作日。如果n
是星期六,則表示前一個星期五。如果n
是星期日,則表示後一個星期一;如果n
是1
且是星期六(即1W
表示該月的第一個工作日),也同樣如此。 -
如果月中某天欄位是
LW
,則表示該月的最後一個工作日。 -
週中某天欄位可以是
d#n
(或DDD#n
),表示該月第d
天(或DDD
)的第n
次出現。
以下是一些示例:
Cron 表示式 | 含義 |
---|---|
|
每天每小時的頂部 |
|
每十秒鐘 |
|
每天的 8、9 和 10 點 |
|
每天的上午 6:00 和下午 7:00 |
|
每天的 8:00、8:30、9:00、9:30、10:00 和 10:30 |
|
工作日九點到五點每小時的頂部 |
|
每年聖誕節午夜 |
|
每月最後一天午夜 |
|
每月倒數第三天午夜 |
|
每月最後一個星期五午夜 |
|
每月最後一個星期四午夜 |
|
每月第一個工作日午夜 |
|
每月最後一個工作日午夜 |
|
每月第二個星期五午夜 |
|
每月第一個星期一午夜 |
宏(Macros)
像 0 0 * * * *
這樣的表示式對人類來說很難解析,因此也難以修復 bug。為了提高可讀性,Spring 支援以下宏,它們代表常用序列。您可以使用這些宏代替六位數字值,例如:@Scheduled(cron = "@hourly")
。
宏 | 含義 |
---|---|
|
每年一次 ( |
|
每月一次 ( |
|
每週一次 ( |
|
每天一次 ( |
|
每小時一次, ( |
使用 Quartz 排程器
Quartz 使用 Trigger
、Job
和 JobDetail
物件來實現各種作業的排程。有關 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
}
}
作業資料對映中的所有附加屬性也都可以供您使用。
透過使用 name 和 group 屬性,您可以分別修改作業的名稱和組。預設情況下,作業的名稱與 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
實現:CronTriggerFactoryBean
和 SimpleTriggerFactoryBean
。
觸發器需要被排程。Spring 提供了一個 SchedulerFactoryBean
,它將觸發器暴露為屬性進行設定。SchedulerFactoryBean
使用這些觸發器排程實際的作業。
以下列表同時使用了 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
還有更多屬性可用,例如作業詳情使用的日曆、用於自定義 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 的完全替代品。 |