タスクの実行とスケジューリング

Spring Framework は、それぞれ TaskExecutor インターフェースと TaskScheduler インターフェースを使用したタスクの非同期実行とスケジューリングの抽象化を提供します。Spring は、アプリケーションサーバー環境内でスレッドプールや CommonJ への委譲をサポートするインターフェースの実装も備えています。最終的に、共通インターフェースの背後でこれらの実装を使用すると、Java SE 環境と Jakarta EE 環境の違いが抽象化されます。

Spring は、Quartz スケジューラー (英語) とのスケジューリングをサポートする統合クラスも備えています。

Spring TaskExecutor の抽象化

エグゼキュータは、スレッドプールの概念の JDK 名です。"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 を参照してください。

  • ConcurrentTaskExecutor: この実装は、java.util.concurrent.Executor インスタンス用のアダプターです。Executor 構成パラメーターを Bean プロパティとして公開する代替手段(ThreadPoolTaskExecutor)があります。ConcurrentTaskExecutor を直接使用する必要はほとんどありません。ただし、ThreadPoolTaskExecutor がニーズに対して十分な柔軟性がない場合は、ConcurrentTaskExecutor が代替です。

  • ThreadPoolTaskExecutor: この実装が最も一般的に使用されます。java.util.concurrent.ThreadPoolExecutor を構成するための Bean プロパティを公開し、TaskExecutor にラップします。別の種類の java.util.concurrent.Executor に適応する必要がある場合は、代わりに ConcurrentTaskExecutor を使用することをお勧めします。

  • DefaultManagedTaskExecutor: この実装では、JSR-236 互換のランタイム環境(Jakarta EE アプリケーションサーバーなど)で JNDI が取得した ManagedExecutorService を使用し、そのために CommonJ WorkManager を置き換えます。

6.1 以降、ThreadPoolTaskExecutor は、Spring のライフサイクル管理を通じて一時停止 / 再開機能と正常なシャットダウンを提供します。また、SimpleAsyncTaskExecutor には、JDK 21 の仮想スレッドと連携した新しい "virtualThreads" オプションがあり、SimpleAsyncTaskExecutor にも正常なシャットダウン機能があります。

TaskExecutor を使用する

Spring の TaskExecutor 実装は、依存関係の注入でよく使用されます。次の例では、ThreadPoolTaskExecutor を使用して一連のメッセージを非同期に出力する Bean を定義します。

import org.springframework.core.task.TaskExecutor;

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));
		}
	}
}

ご覧のように、プールからスレッドを取得して自分で実行するのではなく、Runnable をキューに追加します。次に、TaskExecutor はその内部ルールを使用して、タスクが実行されるタイミングを決定します。

TaskExecutor が使用するルールを構成するには、簡単な Bean プロパティを公開します。

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

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

最も単純な方法は、Runnable と Instant のみを取る schedule という名前の方法です。これにより、タスクは指定された時間の後に 1 回実行されます。他のすべての方法では、繰り返し実行するタスクをスケジュールできます。固定レートと固定遅延の方法は単純で定期的な実行用ですが、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 インターフェースの 2 つの実装を提供します。最も興味深いのは CronTrigger です。これにより、cron 式に基づいてタスクのスケジューリングが可能になります。例: 次のタスクは毎時 15 分後に実行されるようにスケジュールされていますが、平日の 9 時から 5 時までの「営業時間」にのみ実行されます。

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

もう 1 つの実装は、固定期間、オプションの初期遅延値、期間を固定レートまたは固定遅延として解釈する必要があるかどうかを示すブール値を受け入れる PeriodicTrigger です。TaskScheduler インターフェースは、固定レートまたは固定遅延でタスクをスケジュールするためのメソッドをすでに定義しているため、これらのメソッドは可能な限り直接使用する必要があります。PeriodicTrigger 実装の価値は、Trigger 抽象化に依存するコンポーネント内で使用できることです。例: 定期的なトリガー、cron ベースのトリガー、さらにはカスタムトリガーの実装を交換可能に使用できると便利な場合があります。このようなコンポーネントは依存性注入を利用できるため、そのような Triggers を外部で構成できるため、簡単に変更または拡張できます。

TaskScheduler の実装

Spring の TaskExecutor 抽象化と同様、TaskScheduler 構成の主な利点は、アプリケーションのスケジューリングのニーズが デプロイ環境から切り離されることです。この抽象化レベルは、アプリケーション自体によってスレッドが直接作成されるべきではないアプリケーションサーバー環境にデプロイする場合に特に関連します。このようなシナリオのために、Spring は、Jakarta EE 環境で JSR-236 ManagedScheduledExecutorService に委譲する DefaultManagedTaskScheduler を提供します。

外部スレッド管理が要件でない場合は常に、より簡単な代替手段はアプリケーション内のローカル ScheduledExecutorService セットアップです。これは Spring の ConcurrentTaskScheduler を介して調整できます。便宜上、Spring は ThreadPoolTaskScheduler も提供します。ThreadPoolTaskScheduler は、ScheduledExecutorService に内部的に委譲して、ThreadPoolTaskExecutor のラインに沿って共通の Bean スタイルの構成を提供します。これらのバリアントは、寛容なアプリケーションサーバー環境、特に Tomcat および Jetty でのローカルに埋め込まれたスレッドプールのセットアップでも完全に機能します。

6.1 以降、ThreadPoolTaskScheduler は、Spring のライフサイクル管理を通じて一時停止 / 再開機能と正常なシャットダウンを提供します。また、JDK 21 の仮想スレッドに合わせた SimpleAsyncTaskScheduler と呼ばれる新しいオプションもあり、単一のスケジューラースレッドを使用しますが、スケジュールされたタスクの実行ごとに新しいスレッドを起動します (すべて単一のスケジューラースレッドで動作する固定遅延タスクを除く)。この仮想スレッドに合わせたオプションでは、固定レートと cron トリガーが推奨されます)。

スケジューリングと非同期実行のアノテーションサポート

Spring は、タスクスケジューリングと非同期メソッド実行の両方にアノテーションサポートを提供します。

スケジューリングアノテーションを有効にする

@Scheduled および @Async アノテーションのサポートを有効にするには、次の例に示すように、@EnableScheduling および @EnableAsync を @Configuration クラスのいずれかに追加できます。

@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}

アプリケーションに関連するアノテーションを選択できます。例: @Scheduled のサポートのみが必要な場合は、@EnableAsync を省略できます。よりきめ細かな制御を行うには、SchedulingConfigurer インターフェース、AsyncConfigurer インターフェース、その両方を追加で実装できます。詳細については、SchedulingConfigurer (Javadoc) および AsyncConfigurer javadoc を参照してください。

XML 構成が必要な場合は、次の例に示すように、<task:annotation-driven> 要素を使用できます。

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

上記の XML では、@Async アノテーションを持つメソッドに対応するタスクを処理するためのエグゼキューターリファレンスが提供され、@Scheduled アノテーションが付けられたメソッドを管理するためのスケジューラリファレンスが提供されることに注意してください。

@Async アノテーションを処理するためのデフォルトのアドバイスモードは proxy であり、プロキシを介した呼び出しのみのインターセプトを許可します。同じクラス内のローカル呼び出しは、そのようにインターセプトすることはできません。より高度なインターセプトモードについては、コンパイル時またはロード時のウィービングと組み合わせて aspectj モードに切り替えることを検討してください。

@Scheduled アノテーション

トリガーメタデータとともに、@Scheduled アノテーションをメソッドに追加できます。例: 次のメソッドは、固定遅延で 5 秒(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 属性を使用できます。次のメソッドは 5 秒ごとに呼び出されます (各呼び出しの連続した開始時間の間で測定)。

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

1 回限りのタスクの場合は、メソッドの目的の実行前に待機する時間を示すことで、初期遅延を指定するだけです。

@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 以降、@Scheduled メソッドは任意のスコープの Bean でサポートされます。

実行時に同じ @Scheduled アノテーションクラスの複数のインスタンスを初期化していないことを確認してください。ただし、各インスタンスへのコールバックをスケジュールしたい場合を除きます。これに関連して、@Scheduled アノテーションが付けられ、コンテナーに通常の Spring Bean として登録されている Bean クラスで @Configurable を使用しないようにしてください。それ以外の場合は、各 @Scheduled メソッドが 2 回呼び出されるため、二重の初期化(コンテナーを介して 1 回、@Configurable を介して 1 回)が発生します。

Reactive メソッドまたは 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 に適合できる型の例ですが、遅延サブスクリプションはサポートしていません。レジストリ内の ReactiveAdapter は、getDescriptor().isDeferred() メソッドが false を返すことを示します。

  • 次の例のような、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 サスペンド関数の場合、フレームワークがサスペンド関数を Publisher として呼び出すことができるように、kotlinx.coroutines.reactor ブリッジも存在する必要があります。

Spring Framework は、アノテーション付きメソッドの Publisher を一度取得し、その Publisher をサブスクライブする Runnable をスケジュールします。これらの内部定期購読は、対応する cron/fixedDelay/fixedRate 構成に従って発生します。

Publisher が onNext シグナルを発行する場合、これらは無視され、破棄されます (同期 @Scheduled メソッドからの戻り値が無視されるのと同じ方法です)。

次の例では、Flux は onNext("Hello")onNext("World") を 5 秒ごとに発行しますが、これらの値は使用されません。

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

Publisher が onError シグナルを発信すると、WARN レベルで記録され、回復されます。Publisher インスタンスの非同期で遅延的な性質のため、例外は Runnable タスクからスローされません。これは、ErrorHandler 契約がリアクティブメソッドに関与しないことを意味します。

その結果、エラーにもかかわらず、さらにスケジュールされたサブスクリプションが発生します。

次の例では、Mono サブスクリプションが最初の 5 秒間に 2 回失敗します。その後、サブスクリプションが成功し始め、5 秒ごとにメッセージが標準出力に出力されます。

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

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

アノテーション付きの Bean を破棄するか、アプリケーションコンテキストを閉じると、Spring Framework はスケジュールされたタスクをキャンセルします。これには、Publisher への次にスケジュールされたサブスクリプションと、現在もアクティブな過去のサブスクリプション (たとえば、長期実行パブリッシャーや無限パブリッシャーの場合) が含まれます。

@Async アノテーション

メソッドに @Async アノテーションを付けて、そのメソッドの呼び出しが非同期に行われるようにすることができます。つまり、呼び出し元は呼び出し時にすぐに戻りますが、メソッドの実際の実行は Spring TaskExecutor に送信されたタスクで発生します。最も単純なケースでは、次の例に示すように、void を返すメソッドにアノテーションを適用できます。

@Async
void doSomething() {
	// this will be run asynchronously
}

@Scheduled アノテーションが付けられたメソッドとは異なり、これらのメソッドは、コンテナーによって管理されているスケジュールされたタスクからではなく、実行時に呼び出し元によって「通常」の方法で呼び出されるため、引数を期待できます。例: 次のコードは、@Async アノテーションの正当なアプリケーションです。

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

値を返すメソッドでさえ非同期に呼び出すことができます。ただし、そのようなメソッドには Future -typed 戻り値が必要です。これにより、非同期実行の利点が得られるため、呼び出し元は、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 によるエグゼキューター修飾

デフォルトでは、メソッドで @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 -typed 戻り値がある場合、メソッド実行中にスローされた例外を管理するのは簡単です。この例外は、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 には TaskExecutor および TaskScheduler インスタンスを構成するための XML 名前空間が含まれています。また、トリガーを使用してスケジュールされるタスクを構成する便利な方法も提供します。

'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 には固定サイズのスレッドプールがあります(コアサイズと最大サイズは同じです)。ただし、executor 要素の pool-size 属性は、min-max の形式の範囲も受け入れます。次の例では、5 の最小値と 25 の最大値を設定します。

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

上記の構成では、queue-capacity 値も提供されています。スレッドプールの構成も、エグゼキューターのキュー容量を考慮して考慮する必要があります。プールサイズとキュー容量の関連の詳細については、ThreadPoolExecutor (標準 Javadoc) のドキュメントを参照してください。主なアイデアは、タスクが送信されると、アクティブなスレッドの数が現在コアサイズより少ない場合、エグゼキューターは最初に空きスレッドを使用しようとすることです。コアサイズに達した場合、その容量にまだ達していない限り、タスクはキューに追加されます。そのときだけ、キューの容量に達した場合、エグゼキューターはコアサイズを超える新しいスレッドを作成します。最大サイズにも達している場合、エグゼキューターはタスクを拒否します。

デフォルトでは、キューは無制限ですが、プールスレッドがすべてビジーである間に十分なタスクがキューに追加されると OutOfMemoryError につながる可能性があるため、これはめったに望ましい構成ではありません。さらに、キューが制限されていない場合、最大サイズはまったく効果がありません。executor は常にコアサイズを超えて新しいスレッドを作成する前にキューを試行するため、スレッドプールがコアサイズを超えて大きくなるには、キューに有限の容量が必要です(これは、使用時に固定サイズのプールのみが実用的な場合です無制限のキュー)。

上記のように、タスクが拒否された場合を考えてください。デフォルトでは、タスクが拒否されると、スレッドプールエグゼキューターは TaskRejectedException をスローします。ただし、拒否ポリシーは実際に構成可能です。AbortPolicy 実装であるデフォルトの拒否ポリシーを使用すると、例外がスローされます。重い負荷でいくつかのタスクをスキップできるアプリケーションでは、代わりに DiscardPolicy または DiscardOldestPolicy を構成できます。重い負荷で送信されたタスクを調整する必要があるアプリケーションに適した別のオプションは、CallerRunsPolicy です。このポリシーは、例外をスローしたりタスクを破棄したりする代わりに、submit メソッドを呼び出しているスレッドにタスク自体を実行させます。そのような発呼者は、そのタスクの実行中はビジーであり、他のタスクをすぐに送信できないという考えです。スレッドプールとキューの制限を維持しながら、受信負荷を調整する簡単な方法を提供します。通常、これにより、executor は処理中のタスクを「追いつく」ことができ、キュー、プール、その両方の容量を解放できます。executor 要素の rejection-policy 属性で使用可能な値の列挙から、これらのオプションのいずれかを選択できます。

次の例は、さまざまな動作を指定する多数の属性を持つ executor 要素を示しています。

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

最後に、keep-alive 設定は、スレッドが停止する前にアイドル状態を維持できる時間制限(秒単位)を決定します。現在プールにあるスレッドのコア数を超える場合、タスクを処理せずにこの時間待機した後、余分なスレッドは停止します。時間値がゼロの場合、タスクキューにフォローアップ作業が残っていないまま、タスクの実行直後に余分なスレッドが停止します。次の例では、keep-alive 値を 2 分に設定します。

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

'scheduled-tasks' エレメント

Spring のタスク名前空間の最も強力な機能は、Spring アプリケーションコンテキスト内でスケジュールされるようにタスクを構成するためのサポートです。これは、メッセージ駆動型 POJO を構成するために JMS 名前空間によって提供されるものなど、Spring の他の「メソッド呼び出し側」と同様のアプローチに従います。基本的に、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"/>

スケジューラーは外部要素によって参照され、個々のタスクにはトリガーメタデータの構成が含まれます。前の例では、そのメタデータは、各タスクの実行が完了した後に待機するミリ秒数を示す固定遅延を伴う定期的なトリガーを定義します。もう 1 つのオプションは 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 式は、スペースで区切られた 6 つの時刻フィールドと日付フィールドで構成され、それぞれに有効な値の範囲があります。

 ┌───────────── 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)
 │ │ │ │ │ │
 * * * * * *

適用されるいくつかのルールがあります。

  • フィールドはアスタリスク(*)である場合があり、これは常に「最初から最後」を表します。曜日または曜日のフィールドには、アスタリスクの代わりに疑問符(?)を使用できます。

  • カンマ(,)は、リストの項目を区切るために使用されます。

  • ハイフン(-)で区切られた 2 つの数値は、数値の範囲を表します。指定された範囲は包括的です。

  • / を使用して範囲(または *)を追跡すると、範囲内の数値の値の間隔が指定されます。

  • 月と曜日のフィールドには、英語の名前も使用できます。特定の日または月の最初の 3 文字を使用します (大文字と小文字は区別されません)。

  • 月日および曜日フィールドには、異なる意味を持つ L 文字を含めることができます。

    • 日フィールドでは、L は月の最後の日を表します。負のオフセット(つまり、L-n)が後に続く場合、n の月の最後の日を意味します。

    • 曜日フィールドでは、L は週の最後の日を表します。接頭辞として数字または 3 文字の名前(dL または DDDL)が付いている場合は、その月の最後の曜日(d または DDD)を意味ます。

  • 日フィールドは nW にすることができます。これは、月の n に最も近い曜日を表します。n が土曜日に当たる場合、これはその前の金曜日になります。n が日曜日に該当する場合、これは翌月曜日になります。これは、n が 1 であり、土曜日に該当する場合にも発生します(つまり、1W は月の最初の平日を表します)。

  • 日フィールドが LW場合、その月の最後の平日を意味ます。

  • 曜日フィールドは、d#n (または DDD#n)にすることができます。これは、月の n 番目の曜日 d (または DDD)を表します。

ここではいくつかの例を示します。

cron 式 意味

0 0 * * * *

毎日 0 時 0 分

*/10 * * * * *

10 秒ごと

0 0 8-10 * * *

毎日 8, 9, 10 時

0 0 6,19 * * *

毎日 6:00 AM、7:00 PM

0 0/30 8-10 * * *

毎日 8:00, 8:30, 9:00, 9:30, 10:00, 10:30

0 0 9-17 * * MON-FRI

平日の 9 時から 5 時

0 0 0 25 DEC ?

毎年クリスマスの真夜中

0 0 0 L * *

月の最終日深夜

0 0 0 L-3 * *

月の最後から 3 番目の深夜

0 0 0 * * 5L

月の最終金曜日の深夜

0 0 0 * * THUL

月の最終木曜日の深夜

0 0 0 1W * *

月の最初の平日深夜

0 0 0 LW * *

月の最後の平日深夜

0 0 0 ? * 5#2

月の第 2 金曜日の深夜

0 0 0 ? * MON#1

月の最初の月曜日の深夜

マクロ

0 0 * * * * などの式は、人間が解析するのが難しいため、バグが発生した場合に修正するのが困難です。読みやすさを向上させるために、Spring は、一般的に使用されるシーケンスを表す次のマクロをサポートしています。6 桁の値の代わりにこれらのマクロを使用できます。つまり、@Scheduled(cron = "@hourly") です。

マクロ 意味

@yearly (or @annually)

once a year (0 0 0 1 1 *)

@monthly

1 ヶ月に 1 回 (0 0 0 1 * *)

@weekly

1 週間に 1 回 (0 0 0 * * 0)

@daily (or @midnight)

once a day (0 0 0 * * *), or

@hourly

1 時間に 1 回 (0 0 * * * *)

Quartz スケジューラーの使用

Quartz は TriggerJobJobDetail オブジェクトを使用して、あらゆる種類のジョブのスケジューリングを実現します。Quartz の背後にある基本概念については、Quartz Web サイト (英語) を参照してください。便宜上、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>

上記の例では、次の例に示すように、doIt メソッドが exampleBusinessObject メソッドで呼び出されます。

public class ExampleBusinessObject {

	// properties and collaborators

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

MethodInvokingJobDetailFactoryBean を使用することにより、メソッドを呼び出すだけの 1 行のジョブを作成する必要がなくなります。実際のビジネスオブジェクトを作成し、詳細オブジェクトを結び付けるだけです。

デフォルトでは、Quartz ジョブはステートレスであるため、ジョブが互いに干渉する機能があります。同じ JobDetail に 2 つのトリガーを指定すると、最初のジョブが完了する前に 2 番目のトリガーが開始される機能があります。JobDetail クラスが Stateful インターフェースを実装している場合、これは起こりません。最初のジョブが完了する前に 2 番目のジョブが開始されません。

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 は、CronTriggerFactoryBean と SimpleTriggerFactoryBean の便利なデフォルトを備えた 2 つの Quartz FactoryBean 実装を提供します。

トリガーをスケジュールする必要があります。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>

上記の例では、2 つのトリガーをセットアップします。1 つは 50 秒ごとに実行され、開始遅延は 10 秒で、もう 1 つは毎朝午前 6 時に実行されます。すべてをファイナライズするには、次の例に示すように、SchedulerFactoryBean をセットアップする必要があります。

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

ジョブの詳細で使用されるカレンダー、Quartz をカスタマイズするためのプロパティ、Spring が提供する JDBC DataSource など、SchedulerFactoryBean で使用できるプロパティは他にもあります。詳細については、SchedulerFactoryBean javadoc を参照してください。

SchedulerFactoryBean は、通常の Quartz 構成と同様に、Quartz プロパティキーに基づいて、クラスパス内の quartz.properties ファイルも認識します。多くの SchedulerFactoryBean 設定は、プロパティファイルの一般的な Quartz 設定と相互作用することに注意してください。両方のレベルで値を指定することはお勧めしません。例: Spring が提供する DataSource に依存する場合は、"org.quartz.jobStore.class" プロパティを設定しないでください。または、標準の org.quartz.impl.jdbcjobstore.JobStoreTX の本格的な代替である org.springframework.scheduling.quartz.LocalDataSourceJobStore バリアントを指定してください。