任務執行與排程
Spring Framework 分別透過 TaskExecutor 和 TaskScheduler 介面為任務的非同步執行和排程提供了抽象。Spring 還提供了這些介面的實現,它們支援執行緒池或在應用伺服器環境中委託給 CommonJ。最終,在通用介面後面使用這些實現抽象了 Java SE 和 Jakarta EE 環境之間的差異。
Spring 還提供了整合類以支援使用 Quartz 排程器進行排程。
Spring TaskExecutor 抽象
執行器是 JDK 對執行緒池概念的命名。“執行器”的命名是因為不能保證底層實現實際上是一個池。執行器可以是單執行緒的,甚至是同步的。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,它使用 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 進行包裝。裝飾器應該委託給它正在包裝的任務,可能會在任務執行之前/之後實現自定義行為。
讓我們考慮一個簡單的實現,它將在任務執行之前和之後記錄訊息。
-
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。這會導致任務在指定時間後執行一次。所有其他方法都能夠排程任務重複執行。固定速率和固定延遲方法用於簡單的週期性執行,但接受 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,執行器引用用於處理與帶有 @Async 註解的方法對應的任務,而排程器引用用於管理帶有 @Scheduled 註解的方法。
處理 @Async 註解的預設建議模式是 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 返回值,並且不能接受任何引數。如果方法需要與應用程式上下文中的其他物件進行互動,這些物件通常會透過依賴注入提供。
@Scheduled 可以作為可重複註解使用。如果在同一個方法上找到多個排程宣告,每個宣告都將獨立處理,每個宣告都將觸發一個單獨的觸發器。因此,這些共同位置的排程可能會重疊並多次並行或緊密相繼執行。請確保您指定的 cron 表示式等不會意外重疊。
|
從 Spring Framework 4.3 開始,任何作用域的 bean 都支援 請確保在執行時不要初始化同一 |
@Scheduled 註解在響應式方法或 Kotlin 掛起函式上
從 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,然後該 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 的 AsyncExecutionInterceptor 與 Spring AOP 結合使用,並結合自定義切入點。 |
使用 @Async 進行執行器限定
預設情況下,當在方法上指定 @Async 時,使用的執行器是 啟用非同步支援時配置的執行器,即如果您使用 XML 或您的 AsyncConfigurer 實現(如果有)的“annotation-driven”元素。但是,當您需要指示在執行給定方法時應使用除預設執行器之外的其他執行器時,可以使用 @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 應用程式上下文中配置要排程的任務。這遵循了 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 |
|
工作日朝九晚五的整點 |
|
每年聖誕節午夜 |
|
每月最後一天午夜 |
|
每月倒數第三天午夜 |
|
每月最後一個星期五午夜 |
|
每月最後一個星期四午夜 |
|
每月第一個工作日午夜 |
|
每月最後一個工作日午夜 |
|
每月第二個星期五午夜 |
|
每月第一個星期一午夜 |
使用 Quartz 排程器
Quartz 使用 Trigger、Job 和 JobDetail 物件來實現各種作業的排程。有關 Quartz 背後的基本概念,請參閱 Quartz 網站。為了方便起見,Spring 提供了一些類來簡化在基於 Spring 的應用程式中使用 Quartz。
使用 JobDetailFactoryBean
Quartz JobDetail 物件包含執行作業所需的所有資訊。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>
作業詳情配置包含了執行作業 (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>
前面的示例設定了兩個觸發器,一個以 10 秒的起始延遲每 50 秒執行一次,另一個每天早上 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 屬性鍵識別類路徑中的 quartz.properties 檔案,就像常規 Quartz 配置一樣。請注意,許多 SchedulerFactoryBean 設定與屬性檔案中的常見 Quartz 設定相互作用;因此,不建議在兩個級別指定值。例如,如果您打算依賴 Spring 提供的 DataSource,或者指定 org.springframework.scheduling.quartz.LocalDataSourceJobStore 變體(它是標準 org.quartz.impl.jdbcjobstore.JobStoreTX 的完整替代品),請不要設定“org.quartz.jobStore.class”屬性。 |