- SpringFramework 6.0.11
- SpringBoot 3.1.2
@Async
と @EnableAsync
の使い方
SpringFrameworkで @Async
を使うとかんたんにメソッドを非同期で実行できます。
@Component class AsyncComponent { @Async void method() { // 時間のかかる処理 } }
使う側は単にメソッドを呼び出すだけです。戻り値を処理したいなら Future
で受ければいいけど、投げっばでいいなら void
で良い。投げっばでいい場合の方が多いはず。
@Async
を使うためにはアノテーションを処理する何かしらが必要で、この一式を用意するように指示するのが @EnableAsync
です。
以下のようなクラスをSpringがわかるところに置いてあげます。
@EnableAsync @Configuration(proxyBeanMethods = false) class AsyncConfiguration { }
かんたんですね。
⚠️以降はひたすらSpringの中身の話です。「知っているべき」とまでは言いませんが、知っておくとなんかの役には立つんじゃないかな。
SpringFrameworkの話
細かいことはコードやおまけでつけたクラス図と照らし合わせてもらうとして、大雑把に以下のようなことをやっている。
- 準備
@EnableAsync
によりAsyncAnnotationBeanPostProcessor
が登録される。AsyncAnnotationBeanPostProcessor
は@Async
なメソッド呼び出しをAnnotationAsyncExecutionInterceptor
でラップするようにする。
- 実行
@Async
なメソッドが呼び出されるとAnnotationAsyncExecutionInterceptor
がメソッドに応じたExecutor
に実行させする。
コードを見ての通り、 AnnotationAsyncExecutionInterceptor
では Executor
に Callable
を submit
しているだけです。( Executor
や Callable
も java.util.concurrent
です。わからない場合は別途勉強してください。)
さてここで重要になるのが Executor
の決まり方です。
Executor
は AnnotationAsyncExecutionInterceptor
(の親である AsyncExecutionAspectSupport
)により以下の順で決定されます。
- メソッドで指定されている
Executor
のBean AnnotationAsyncExecutionInterceptor
に設定されているデフォルトのExecutor
TaskExecutor
のBean"taskExecutor"
という名前のExecutor
のBeanSimpleAsyncTaskExecutor
を生成して使用
色々ありますが、SpringFrameworkの歴史的経緯などもあるのでしょう。とりあえずこうなっています。
SpringFrameworkの文脈で「 @Async
を動かす Executor
に手を加えたい」と思ったならば、この1から4のいずれかを使えばいいわけです。(5は全部ダメだった時にSpringがなんとか動かすための実装なので手は出せません。)
1番目はたとえば @Async("myExecutor")
と書いていれば myExecutor
という名前の Executor
Beanがあればそれが使われるということです。アノテーションを使わない場合も考慮されてるのでこんな表現になっています。
2番目の「 AnnotationAsyncExecutionInterceptor
に設定されているデフォルトの Executor
」はわかりづらいですが、 @EnableAsync
を使用する場合は AsyncConfigurer
の getAsyncExecutor
メソッドが返すインスタンス になります。なぜかよく紹介されているので使ったことのある人も多いかもしれません。
SpringBootの話
SpringBootを使用する場合、3番目の「 TaskExecutor
のBean 」が使用されます。
このBeanは TaskExecutionAutoConfiguration
が作成 します。
自動構成される TaskExecutor
は他の Executor
のBeanを作成すれば作られなくできますが、嬉しいことはないのでやめておきましょう。
TaskExecutionAutoConfiguration
を見ると、 TaskExecutorBuilder
を通して TaskExecutionProperties
に設定された値などを使って TaskExecutor
を作っているのがわかると思います。
SpringBootを使用する場合、SpringBootのプロパティ(ここでは spring.task.execution.*
)が効くことが期待されます。
そのため、 @Async
を実行するデフォルトの Executor
を差し替える場合、以下の対応は好ましくありません。動くけど。動くけど、好ましくない。
AsyncConfigurer
を作成してExecutor
のインスタンスを生成する(前述2番)TaskExecutorBuilder
(もしくはTaskExecutionProperties
)を使用しないTaskExecutor
のBeanを作成する(前述3番)taskExecutor
という名前のExecutor
Beanを作成する(前述4番)
……「全滅じゃん?」と言ってはいけない。まだだいじょうぶ。
下2つを採用する場合は spring.task.execution.*
が効かないことを明確にドキュメントに記述し、保守では引き継ぎ続けなければなりません。また「自分が作った通りの Executor
」となるため、SpringBootが「だいたいのアプリはこの設定でいけるっしょ」とやってくれているものに乗っかれません。ちゃんと自分たちで決めてぜんぶ実装しなければならないのです。もちろんSpringBootの設定値も妥当性を確認するのが望ましいですが、全部の値は見てられないし、そんな関心のないものはSpringBootに任せとくというのも一つの選択です。「SpringBootではないデフォルトでもいいじゃないか」というのもそれはそうかもしれませんが、おそらくSpringBootよりも広いコンテキストのデフォルトであり、SpringBootのデフォルトよりも微妙なものが多いなぁという感覚。
一番上の「 AsyncConfigurer
で Executor
を作成」はプロパティの問題に加えてさらに別の問題があります。
Executor
をBean登録しないので、SpringBootの管理外となってしまいます。SpringBootが管理してくれないことによる問題としては、シャットダウン時に後片付けがされないことや、メトリクスが自動では見えないことが挙げられます。
SpringBootActuatorで metrics
エンドポイントを公開すると /actuator/metrics
で簡易的なメトリクスが参照できます。
SpringBootActuatorは Executor
なBeanを TaskExecutorMetricsAutoConfiguration
が拾って自動的に見れるようにしてくれます。以下ではアクティブなタスク数とキューに入っている数を取得しています。
% curl http://localhost:8080/actuator/metrics/executor.active -s | jq { "name": "executor.active", "description": "The approximate number of threads that are actively executing tasks", "baseUnit": "threads", "measurements": [ { "statistic": "VALUE", "value": 8 } ], "availableTags": [ { "tag": "name", "values": [ "applicationTaskExecutor" ] } ] } % curl http://localhost:8080/actuator/metrics/executor.queued -s | jq { "name": "executor.queued", "description": "The approximate number of tasks that are queued for execution", "baseUnit": "tasks", "measurements": [ { "statistic": "VALUE", "value": 52 } ], "availableTags": [ { "tag": "name", "values": [ "applicationTaskExecutor" ] } ] }
実運用では /actuator/metrics
ではなく /actuator/prometheus
などを使うと思います。
% curl http://localhost:8080/actuator/prometheus -s | grep "^executor_" executor_pool_max_threads{name="applicationTaskExecutor",} 2.147483647E9 executor_active_threads{name="applicationTaskExecutor",} 8.0 executor_pool_size_threads{name="applicationTaskExecutor",} 8.0 executor_completed_tasks_total{name="applicationTaskExecutor",} 168.0 executor_queued_tasks{name="applicationTaskExecutor",} 14.0 executor_queue_remaining_tasks{name="applicationTaskExecutor",} 2.147483633E9 executor_pool_core_threads{name="applicationTaskExecutor",} 8.0
これが見えないのは運用を考えると厳しいため、AsyncConfigurer
を使う場合も別途Bean登録した Executor
を使うようにしてあげる必要があります。で、そうするなら TaskExecutor
なBean登録すりゃいいんじゃね?などになり、であれば AsyncConfigurer
をSpringBootで使うのは微妙に思えます。
そもそもやりたいこと
Executor
を変えたいというのはそうそうありません。
もちろん ThreadPoolTaskExecutor
だと要件を満たさなくて、別のライブラリや自分たちで制御する Executor
にしたいなどもあるでしょう。その場合はやるといいと思います。
ですがここに手を伸ばしたい多くの理由は「スレッドを跨いでMDCなど ThreadLocal
に頼っているコンテキストを引き渡したい」などでしょう。
であるならば Executor
の置き換えではなく、 TaskDecorator
を使うのが妥当でしょう。 TaskExecutionAutoConfiguration
の taskExecutorBuilder
メソッド を見てください。
引数で ObjectProvider<TaskDecorator>
を受けて
public TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties properties,
ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers,
ObjectProvider<TaskDecorator> taskDecorator) {
getIfUnique
で取得したのをTaskExecutionBuilder
に渡しています。
builder = builder.taskDecorator(taskDecorator.getIfUnique());
このため TaskDecorator
をBean登録すれば自動的に取り込まれますが、 ObjectProvider#getIfUnique
の動きも知っておきたいところです。
- 対象Beanがなければ
null
- 対象Beanが1つだけならそれ
- 対象Beanが複数ある場合
- 何かしらで1つに特定できればそれ
- 特定できなければ
null
注意したいのは「複数登録されていると null
になり得る」です。特定にはBeanの名前や @Primary
が使用されますが、特定できなくてもエラーも出ず単に設定されていないって動きをします。Bean定義が散在すると気づかないうちに未設定になっているなどがあるかもしれません。残念ながら特にエラーログも出てくれません。組織体制によっては固く定義した方がいいかもしれません。
ということで「 Executor
とかいじってないで TaskDecorator
だけBean登録する」で多くの場合は事足りるはずです。
@Bean TaskDecorator taskDecorator() { return runnable -> { // ここは呼び出し元のスレッドで実行される。必要なものを取り出す。 return () -> { // ここからExecutorのスレッドで実行される。外から受け取ったものを設定する。 try { runnable.run(); } finally { // 後片付けを忘れない } }; }; }
もし「TaskDecorator
じゃ足りないんだよぅ」とかでも、先のシグネチャの通りまだ TaskExecutorCustomizer
があります。 TaskExecutor
をごそっと差し替えるのは、以下を検討してからって感じですね。
- プロパティでなんとかならないか
TaskDecorator
でどうにかならないかTaskExecutorCustomizer
でもできないだろうかTaskExecutorBuilder
は使えるんじゃないか
どれでも満たせないなら Executor
のBean登録で対応。その時は Executor
をちゃんと正しく取り扱いましょう。でもそこまで行くと @Async
とか使わず実装する方が制御しやすいとかもある気がしないでもないです。
あとがき
SpringBootはSpringFrameworkの拡張ポイントも踏襲しているため、一つのことでもたくさんのやり方があります。 検索するとやり方はたくさん出てくるし、確かにそれでも動きはします。確かに動く、動くけど、けどさー……と思いつつ。
「それでも動くけどでもそれはそれのために使うものじゃなくて、いやそれでも動くんだけどさ、動くけど……まぁいいか……」
— irof (@irof) 2023年8月21日
あの時「まぁいいか」って言ったやつ誰だ(過去の自分
「知っているべき」って言わないのは、私も知らずにやってるものたくさんあるからです。知ろうと思った時に知ることができる筋力はつけておきたいお気持ち。
特にSpringFrameworkの拡張ポイントはSpringBootよりもドラスティックに変更できるものも多く、SpringBootはそちらに譲る形で実装されています。たとえば @ConditionalOnMissingBean
とか。
でもSpringBootにのっかるならSpringBootに乗っかったまま拡張したい感がある。SpringBootのバージョンアップとかでその辺の対応がいらなくなった時にすんなり乗れますし、互換性も崩れにくいし、マイグレーションガイドでフォローされる可能性も高い。先にあげたメトリクスのようにいい感じに面倒見てくれるところも多いですし。たまに予期しないところに影響でたりもするんだけど、それ系は自分が微妙なことしたりするせいで、メリットの方が多いと思ってます。
あ、今回は @Async
のを書きましたが、 @Scheduled
の Executor
も大体同じです。 ThreadPoolTaskExecutor
と ThreadPoolTaskScheduler
の違いは型階層だと TaskScheduler
を実装してるかどうかだけで、TaskSchedulingAutoConfiguration
が @AutoConfiguration(after = TaskExecutionAutoConfiguration.class)
なところとか趣深い。
おまけ
この辺のことを整理するときは物理紙やMiroとかに図を起こしながらやってます。こういう崩したり強弱つけたり配置に意味を持たせたりは機械的なクラス図だと厳しいのよね。
参考/参照
- Spring Boot Reference Documentation / Core Features / 7. Task Execution and Scheduling
- Tipにそれっぽいこと書いてるけど、これわかってから読むとわかる気がしないでもな……いややっぱわからん。わからず読んでわかる気はしない。
- Springのドキュメントの探し方 - 日々常々
まぁ「コードに書いてある」も「どこかにある」に含めちゃってたりする
- 今回はほぼコードですね。
- 誤解しがちなThreadPoolTaskExecutorの設定 - IK.AM
- 設定値の話。メトリクス眺めながら設定いじって動かすと「なるほどー」となると思います。
-
- この辺の話は書いてなさそう(目次、索引、Asyncの検索とかに引っかかってこない)
@Async
はそんな頻出でもないですし、守備範囲としては入ってなくていいと思います。プロになった後に理解しましょう。
-
- SpringFrameworkなところは大体書いてる。
TaskDecorator
はSpring4.3導入なのでないっぽい。 - SpringBootは1時代なので、まぁ。
- SpringFrameworkなところは大体書いてる。