yoshikipom Tech Blog

GoReplayで検証環境へ本番環境のリクエストを復元する方法と懸念

やりたいこと

  • (1) 検証環境で本番環境へのリクエストを再現したい
  • (2) トラフィックミラーリングではなく、任意のタイミングで任意の流量で再現したい
  • (3) 本番環境の機密情報を検証環境に持ち込みたいくない
  • (4) 本番環境への影響・変更は最低限に留めたい
  • (5) 対象アプリケーションはECSで動作しているとする

GoReplayで実現できるか?

「やりたいこと」の(1)、(2)に対応できるか。 GoReplay(https://github.com/buger/goreplay) の gor コマンドには 入力元と出力先を指定する必要がある。 参考: https://github.com/buger/goreplay/wiki/The-Basics

ミラーリングするだけならば入力: HTTPトラフィック、出力 HTTPトラフィック とすることですぐさまリクエストをコピーして転送できる。

ローカルでテストした例。(左: リクエスト対象サーバ、中央: コピー先サーバ、右:実行したgorコマンド)
HOSTヘッダ以外は同様のものをコピー先サーバで受信できていることがわかる。

今回検討したいことは任意のタイミングでのリプレイなので、入力・出力は以下のようになる。 (本番環境) 入力: HTTPトラフィック、出力: ファイル (検証環境) 入力: ファイル、出力: HTTPトラフィック アーキテクチャは以下のようなイメージ。

GoReplayをも用いて検証環境でリプレイするイメージ

GoReplayのPRO版ならばS3に保存する機能がついている。これを使わない場合、他の手段でアプリケーションのコンテナからファイルをどこかに保存する必要がある(この記事では省略)。 参考: https://github.com/buger/goreplay/wiki/%5BPRO%5D-Using-S3-for-storing-and-replaying-traffic

リプレイ時の流量調整もコマンドで対応可能 (|200%部分)。

# Replay from file on 2x speed 
gor --input-file "requests.gor|200%" --output-http "staging.com"

参考: https://github.com/buger/goreplay/wiki/Saving-and-Replaying-from-file#:~:text=change%20line%20endings.-,Performance%20testing,-Currently%2C%20this%20functionality

基本的な機能はこれでカバーできそうである。

データの変換、フィルタリング

「やりたいこと」の(3)に対応できるか。 ミドルウェアビジネスロジックの注入が可能。 参考: https://github.com/buger/goreplay/wiki/Middleware ミドルウェアの実装はアプリケーション次第だが、変換ロジックに外部との通信などコストが高い処理があるとアプリケーションコンテナに負荷がかかってしまう。 ミラーリングの場合でも無視できないレベルの負荷がありそう。 GoReplay利用時のベンチマークの参考 -> https://blog.glidenote.com/blog/2016/12/16/goreplay/

この部分は機能面では実現はできるはずだが、ロジック次第ではパフォーマンスの考慮が必要。

本番環境への影響

「やりたいこと」の(4)に対応できるか。 GoReplayはコンテナ内ではアプリケーションへのリクエストをプロキシしているわけではなく、コンテナに来たHTTPリクエストを取得している。GoReplayに問題があってもアプリケーションは問題なく動く。 懸念点の一つ目は、前節でも挙げたパフォーマンスである。負荷試験を行い、必要があればインフラを増強する必要がある。 また、別の問題点として、アプリケーションのDocker Imageへの改修が必要なことである。結果として、アプリケーションが複雑化してしまったり、切り戻り処理にそのアプリケーションのデプロイが必要になる可能性がある。

この問題を完全に回避するなら AWSVPC トラフィックミラーリングを使って、GoReplayのキャプチャ用コンテナにリクエストを流すという方法が考えられそう。 https://aws.amazon.com/jp/blogs/news/new-vpc-traffic-mirroring/

そもそも別の手段でミラーリングするならGoReplayが不要とも考えられるが、以下の理由でこの構成もありかもしれない。

  • リプレイ側で使える機能がとても便利そう(流量制限、複数ファイルの結合、同じファイルをループして利用、ミドルウェアなど)
  • リプレイで使うファイルが独自形式

感想

多機能でドキュメントもしっかりしていて、よく考えられたツールという印象。大規模システムで使う場合はパフォーマンスやデータの要件など細かい点の検討が必要になるとは思う。

対象データリストをファイルで受け取ってPostgreSQLのデータを更新する

やりたいこと

IDのリストを外部からもらい、自分達のサービスのDBにデータパッチ(データの更新 or 削除など)を行いたい。 このとき、IDのリストはかなり大きいもの (>100)とする。 このとき、SQLのレビューや再利用性の観点から IN句を使うのは避けたい。 避けたい例 id in (id1, id2, id3, ... , id1000)

どういう状況でそういうことが起こるか

IDのリストを自サービスで抽出できる場合、ファイルから渡さなくてもサブクエリ等で対処可能な場合が多い。 しかし、以下のようなケースではこのようなワークアラウンドが有効。 - SELECTクエリが複雑すぎて負荷が心配 - 今回のやり方なら抽出用のReader instanceからIDのリストを持ってきて、Writer instanceで更新ということができる - マイクロサービス化していてDBが分かれており、IDのリストの検索が外部サービスによって行われている - 対象データをマニュアルでレビューして確認が取れたものだけに対して更新を実行したい

今回用いるシンプルな例

外部からもらったファイルid_list

1
3
5

自サービスのtableの状況

person=# select * from person;
 id | name | deleted 
----+------+---------
  1 | a    | f
  2 | b    | f
  3 | c    | f
  4 | d    | f
  5 | e    | f
(5 rows)

できたもの

id_list に含まれているIDを持つテーブル上のレコードを論理削除する。 対象テーブル: person 変更カラム: person.deleted (false -> true)

実行

$ cat id_list 
1
3
5

$ export PGPASSWORD=${pass} && sh run.sh
1
UPDATE 1
3
UPDATE 1
5
UPDATE 1

結果

$ psql -h localhost -p 5432 -U root person 
psql (14.6 (Homebrew), server 12.13 (Debian 12.13-1.pgdg110+1))
Type "help" for help.

person=# select * from person;
 id | name | deleted 
----+------+---------
  2 | b    | f
  4 | d    | f
  1 | a    | t
  3 | c    | t
  5 | e    | t
(5 rows)

対象レコード(id:1,3,5)だけが deleted: t (true) になっていることを確認。

つかうもの

server side - postgres:12.13

client side - shell script - SQL file - psql command

スクリプト/SQL

コード: https://github.com/yoshikipom/sandbox/tree/main/postgres/task/update-target

3ファイルを同じフォルダに置く。

$ ls
id_list    run.sh     update.sql

id_list

1
3
5

今回はheaderなしの改行区切りのファイルとする。フォーマット変更や複数変数に対応する場合、 run.sh でハンドリング。

update.sql

\echo :target_id
UPDATE person SET deleted = true WHERE id = :target_id;
  • :{変数名} で変数を展開できる。SQL上で文字列に展開したい場合は :'{変数名}' (例 :'target_id') とする。
  • 今回はIDがintegerなので :target_id
  • ログに削除したIDを表示するために \echo で変数の値を表示。

run.sh

#!/bin/bash
while read id
do
if [ -n "${id}" ]; then
    psql -h localhost -p 5432 -U root person -v target_id=${id} -f update.sql
fi
done < id_list
  • -v key=valueSQLに変数の値を渡せる。
  • < id_listread id${id} にファイルの各行にあるIDを代入。
  • 通常はpsqlでパスワードを求められるため、今回は環境変数を通して先に渡した。
    • $ export PGPASSWORD=${pass} && sh run.sh
  • if文は id_list に空行があった場合にスキップするため。

感想

エンジニア的には1つのSQLでなんとかしようとしがちだが、読みやすさ (+ レビューしやすさ)、拡張性、再利用性、パフォーマンスの予想しやすさなどを考えるとシェルスクリプトを通してデータパッチするのもありだと学んだ。

参考

Spring Boot外部設定(application.yml)の保守性を上げる

Overview

spring bootの設定ファイルの保守性を上げる

  • spring boot 2.7.0
  • 設定ファイルはyml形式を使用

保守性をあげるために設定ファイルに求めること

  • 環境に応じて切替可能
  • ファイル分割(コンフリクトを避ける、設定がみつけやすい)
  • 外部からの値の注入(シークレット情報を扱う場合は重要)
  • 起動の簡単さ(必須設定が多すぎると新規開発者が苦労する)

Spring Bootの機能

基本

設定ファイルとそれを読み込むConfigクラスを準備。(@Valueでも同様に動くが今回は省略。)

src/main/resources/application.yml

myapp:
  configTest:
    key: value
    env_test: ${ENV_TEST}
    noenv: ${NO_ENV:default_value}
    flag: true
    env: no_env

ConfigTest.java

@Data
@ConfigurationProperties(prefix = "myapp.config-test")
@Component
public class ConfigTest {

  private String key;
  private String key2 = null;
  private String envTest;
  private String noenv;
  private boolean flag;
  private String env;
}

読み込みはAutowiredして呼びたい設定のgetterを呼べばOK。 (ここではテスト結果が見やすいようにtoString()を読んでます。)

@SpringBootApplication
@Slf4j
public class ConfigApplication implements CommandLineRunner {

  @Autowired
  private ConfigTest configTest;

  public static void main(String[] args) {
    SpringApplication.run(ConfigApplication.class, args);
    System.exit(0);
  }

  @Override
  public void run(String... args) throws Exception {
    log.info(configTest.toString());
  }
}

実行ログ

2022-08-21 20:39:03.100  INFO 62961 --- [           main] c.y.dev.config.ConfigApplication         : ConfigTest(key=value, key2=null, envTest=injected_value, noenv=default_value, flag=true, env=no_env)

profile

  • application.yml, application-dev.yml, application-prod.yml を準備
  • dev環境: export spring_profiles_active=dev してからアプリ実行 -> application-dev.yml が読まれる
  • prod環境: export spring_profiles_active=prod してからアプリ実行 -> application-prod.yml が読まれる

src/main/resources/application-dev.yml

myapp:
  configTest:
    env: dev

環境変数spring_profiles_active=devを設定して実行ログ確認

2022-08-21 16:59:05.057  INFO 61874 --- [           main] c.y.dev.config.ConfigApplication         : The following 1 profile is active: "dev"
...
2022-08-21 16:59:07.101  INFO 61874 --- [           main] c.y.dev.config.ConfigApplication         : ConfigTest(key=overwritten_by_external, key2=null, envTest=injected_value, noenv=default_value, flag=true, env=dev)

env=devになっているのでうまく動作している。

参考 https://www.baeldung.com/spring-profiles https://spring.pleiades.io/spring-boot/docs/current/reference/html/features.html#features.external-config.files.profile-specific

外部ファイル読み込み

src/main/resources/application.yml (追記)

spring:
  config:
    import:
      - "another.yml"
      - "optional:file:./spring-mvc/dev/external-config.yml"
  • importで複数ファイル指定可能。
  • 絶対パスor相対パスでJARファイル外のファイルも指定可能
  • 同じ値が設定されている場合は後に設定したものが優先。

src/main/resources/another.yml

myapp:
  configTest:
    key: overwritten_by_another

spring-mvc/dev/external-config.yml

myapp:
  configTest:
    key: overwritten_by_external

実行ログ

2022-08-21 16:59:07.101  INFO 61874 --- [           main] c.y.dev.config.ConfigApplication         : ConfigTest(key=overwritten_by_external, key2=null, envTest=injected_value, noenv=default_value, flag=true, env=dev)

external-config.ymlが優先されるので値はkey=overwritten_by_externalとなっている。

https://spring.pleiades.io/spring-boot/docs/current/reference/html/features.html#features.external-config.files

環境変数読み込み

「基本」の env_test: ${ENV_TEST} を参照。${}内で指定した環境変数が自動で展開される。このプレースホルダは設定ファイル内の他の値を展開したり文字列の結合したりも可能。 https://spring.pleiades.io/spring-boot/docs/current/reference/html/features.html#features.external-config.files.property-placeholders

default値の設定

環境変数が設定されない場合 env_test: ${ENV_TEST:default-value} のように書ける。

他の値はJava class側のコンストラクタで設定しておけばデフォルトを持てるが、設定ファイル側に入れておいたほうが見やすい。

ConfigTest.java

@Data
@ConfigurationProperties(prefix = "myapp.config-test")
@Component
public class ConfigTest {
  private String key = "default-value";
  ...

環境変数 vs profile

環境変数でもprofileでも環境ごとの値の設定は可能。 - 環境変数 - シークレットを扱いやすい - 値の変更が容易 - yamlのような構造がないのでリスト型や辞書型の設定は持ちづらい - profile - 構造があるので多数の設定やリスト型、辞書型の設定も扱いやすい - シークレットを使うなら結局プレースホルダーで環境変数を入れることになりそう - JARに設定ファイルを入れているなら値の変更に再度ビルドが必要 設定値が少ないうちは環境変数で環境差異を吸収するのが良さそう。

感想

Spring Bootの外部設定管理まわりは機能が多くて、同じことを複数のやり方でできることが多い。 何を使って何を使わないか決めておくのがよさそう。

参考

Spring BootでREST API, MQ consumer, CLI, 定期実行タスクを実行

Overview

Spring BootはWebアプリケーションを実行する以外にもいろいろできるのでいくつか紹介する。

REST API

application

Spring Bootで一番よくあるアプリケーション。 HTTPリクエストを送ったらJSONレスポンスを得られる。

Spring Boot Application + @RestControllerを持つクラスを実装すれば実装できる。

application class

@SpringBootApplication
public class ApiApplication {

  public static void main(String[] args) {
    SpringApplication.run(ApiApplication.class, args);
  }

}

controller example

@RestController
@RequestMapping("items")
public class ItemController {

  @GetMapping("")
  public List<Item> get() {
    return List.of(new Item("value"));
  }
}

https://github.com/yoshikipom/java/blob/main/spring-mvc/src/main/java/com/yoshikipom/dev/api/rest/ItemController.java

実行

$ curl --request GET \
  --url http://localhost:8080/items \
[{"key":"value"}]%

MQ consumer (worker)

application

Message Queueのconsumer. この例はkafkaの consumer (org.springframework.kafka:spring-kafka を利用)。 @KafkaListenerでトピックを指定。シリアライザーなど細かい設定はapplication.yamlに書ける。

application class

@SpringBootApplication
@EnableKafka
@Slf4j
public class ConsumerApplication {

  public static void main(String[] args) {
    SpringApplication.run(ConsumerApplication.class, args);
  }

  @KafkaListener(topics = "my_topic")
  public void receive(String value) {
    log.info("Received value: {}", value);
  }
}

application.yaml

spring:
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      client-id: ${spring.application.name}
      group-id: ${spring.application.name}-group
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      auto-offset-reset: earliest

https://github.com/yoshikipom/java/blob/main/spring-mvc/src/main/java/com/yoshikipom/dev/consumer/ConsumerApplication.java

実行

kafkaに対してメッセージをproduce.

$ cat test.json
{"name":"myname"}
$ kcat -b localhost:9092 -t my_topic -P -J -l test.json

アプリケーションのログ

2022-08-01 00:25:38.553  INFO 54511 --- [ntainer#0-0-C-1] o.s.k.l.KafkaMessageListenerContainer    : ${spring.application.name}-group: partitions assigned: [my_topic-0]
2022-08-01 00:30:04.773  INFO 54511 --- [ntainer#0-0-C-1] c.y.dev.consumer.ConsumerApplication     : Received value: {"name":"myname"}

コマンドラインアプリケーション

application

単発の処理を実行してすぐ終了するアプリケーション。 CommandLineRunnerの実装に@SpringBootApplicationをつける。 runメソッドの引数でコマンドライン引数を受け取れる。

@SpringBootApplication
@Slf4j
public class CliApplication implements CommandLineRunner {

  public static void main(String[] args) {
    SpringApplication.run(CliApplication.class, args);
    System.exit(0);
  }

  @Override
  public void run(String... args) throws Exception {
    log.info("arg size: {}", args.length);
    for (int i = 0; i < args.length; ++i) {
      log.info("args[{}]: {}", i, args[i]);
    }
  }
}

実行

jarに固めて実行してみる

$ java -jar spring-mvc-0.0.1-SNAPSHOT.jar arg1 arg2

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.0)

2022-08-01 00:33:47.676  INFO 54895 --- [           main] com.yoshikipom.dev.cli.CliApplication    : Starting CliApplication using Java 18 on yoshiki-mbp.local with PID 54895 (/Users/yoshiki/Documents/work/java/spring-mvc/build/libs/spring-mvc-0.0.1-SNAPSHOT.jar started by yoshiki in /Users/yoshiki/Documents/work/java/spring-mvc/build/libs)
2022-08-01 00:33:47.679  INFO 54895 --- [           main] com.yoshikipom.dev.cli.CliApplication    : No active profile set, falling back to 1 default profile: "default"
2022-08-01 00:33:48.732  INFO 54895 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-08-01 00:33:48.744  INFO 54895 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-08-01 00:33:48.744  INFO 54895 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.63]
2022-08-01 00:33:48.834  INFO 54895 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-08-01 00:33:48.834  INFO 54895 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1073 ms
2022-08-01 00:33:49.420  INFO 54895 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-08-01 00:33:49.436  INFO 54895 --- [           main] com.yoshikipom.dev.cli.CliApplication    : Started CliApplication in 2.275 seconds (JVM running for 2.708)
2022-08-01 00:33:49.441  INFO 54895 --- [           main] com.yoshikipom.dev.cli.CliApplication    : arg size: 2
2022-08-01 00:33:49.446  INFO 54895 --- [           main] com.yoshikipom.dev.cli.CliApplication    : args[0]: arg1
2022-08-01 00:33:49.446  INFO 54895 --- [           main] com.yoshikipom.dev.cli.CliApplication    : args[1]: arg2

定期実行アプリケーション

application

定期実行する処理。 interval(fixedRate)を設定する方法やcron形式で設定する方法がある。 @EnableSchedulingをapplicationクラスにつけて、@Scheduledをつけたメソッドを実装する。 上記のコマンドラインアプリケーションを定期的に実装しても同じことができるが、その場合アプリケーションの起動処理の後にやりたい処理が実行される。 こちらの場合は定期処理の前のアプリケーション実行はないが、常にSpring Bootアプリケーションは起動しておく必要がある。

@SpringBootApplication
@EnableScheduling
@Slf4j
public class ScheduledApplication {

  public static void main(String[] args) {
    SpringApplication.run(ScheduledApplication.class, args);
  }

  //  @Scheduled(cron = "0 * 9 * * ?")
  @Scheduled(fixedRate = 1000)
  public void receive() {
    log.info("run scheduled task: {}", LocalDateTime.now());
  }
}

実行

アプリのログ

...
2022-08-01 00:36:32.601  INFO 54945 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-08-01 00:36:32.612  INFO 54945 --- [   scheduling-1] c.y.dev.scheduled.ScheduledApplication   : run scheduled task: 2022-08-01T00:36:32.612477
2022-08-01 00:36:32.615  INFO 54945 --- [           main] c.y.dev.scheduled.ScheduledApplication   : Started ScheduledApplication in 4.381 seconds (JVM running for 4.891)
2022-08-01 00:36:33.613  INFO 54945 --- [   scheduling-1] c.y.dev.scheduled.ScheduledApplication   : run scheduled task: 2022-08-01T00:36:33.613535
2022-08-01 00:36:34.616  INFO 54945 --- [   scheduling-1] c.y.dev.scheduled.ScheduledApplication   : run scheduled task: 2022-08-01T00:36:34.616480

まとめ

Spring Bootのさまざまな機能をWeb API実装以外にも使える。

追記

Web APIはGraghQLやgRPCにも対応できる。 Batch処理に関しては大規模な処理ではSpring Batch (https://spring.io/projects/spring-batch) なども使える。 consumerはspring cloud streamを使った方がいろいろなMQに対応できる。

Mac Setup メモ (2022)

install

command line

mac app (productivity)

  • alfred (luncher)
  • vanilla (tool bar management)
  • rectangle (window management)
  • dropover (file management)
  • karabiner (key setting)
  • Logi Options (mouse setting)

mac app (development)

Mac Setting

  • Keyboard > Shortcuts > Mission Control で Switch to Desktop # を on
  • Accessibility > Display > Reduce motion を on
  • Keyboard で modifier key、repeatの設定、function keyの設定を変更

VSCode Setting

github accountで同期できる

開発ツール for http (wiremock, REST Client plugin for VSCode)

Overview

  • wiremockのdocker-composeでの起動
  • REST Client plugin for VSCode

Environment

  • MacOS
  • docker, docker-compose

wiremock

一番シンプルなmockの設定

{
  "request": {
    "method": "GET",
    "url": "/items"
  },
  "response": {
    "status": 200,
    "jsonBody": {
      "id": 1,
      "name": "item-name"
    },
    "headers": {
      "Content-Type": "application/json"
    }
  }
}

ディレイをつける、request bodyの一致の確認、外部ファイルの利用などは以下を参照。 https://github.com/yoshikipom/sandbox/tree/main/wiremock/dev/wiremock/mappings

docker-composeでの起動

---
version: '3'
services:
  mockServer:
    image: wiremock/wiremock:latest-alpine
    volumes:
      - "./dev/wiremock/:/home/wiremock/"
    ports:
      - 9000:8080
    restart: on-failure
    command:
      - --verbose
      - --disable-banner

run

docker-compose up

REST Client plugin for VSCode

インストール

https://marketplace.visualstudio.com/items?itemName=humao.rest-client

リクエス

ファイルを準備

###
GET http://localhost:9000/items

###
POST http://localhost:9000/items
Content-Type: application/json

{
    "name": "name"
}

Send Requestを押す

vscode rest client plugin

Refference

My Repository

https://github.com/yoshikipom/sandbox/tree/main/wiremock

Spring Bootの@Asyncで非同期処理(thread poolの設定 + 動作確認)

Overview

メインスレッドの処理に大きな影響を与えずにに何かの処理をしたいときに実装した(APIでレスポンスタイムに影響を与えずになにかDBに保存したい等)。 その処理を確実に達成したい場合はMessage Queueをを使った方が無難。

以下のような要件を達成するような実装を紹介する。 - 非同期で実行し、メインスレッドはその完了を待たない - 何らかの理由でThreadが増えすぎてメイン処理を遅くすることは避けたい

Code: https://github.com/yoshikipom/java/tree/main/spring-mvc/src/main/java/com/yoshikipom/dev/api/async Ref - https://www.baeldung.com/spring-async - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html

Code

非同期処理を実行するThread

@Configuration
public class MyTaskExecutorBean {

  @Bean(name = "myTask")
  public TaskExecutor myTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(1);
    executor.setQueueCapacity(1);
    executor.setMaxPoolSize(2);
    return executor;
  }
}

上記の非同期スレッドで実装したい処理

  • @Async("${bean名}") をメソッドにつける (publicメソッド限定)
@Service
@Slf4j
public class MyAsyncService {

  @Async("myTask")
  public void executeAsyncProcess() {
    log.info("start async process");
    log.info("sleep 1s");
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    log.info("finish async process");
  }
}

呼び出し側

  • myAsyncService.executeAsyncProcess() が非同期処理
  • @EnableAsync を 実行クラスにつける
  • thread poolの設定のため4回連続で実行
  • maxPoolSize + queueCapacityは受け付けられない。そのときはTaskRejectedException が起こる
@SpringBootApplication
@EnableAsync
@Slf4j
public class MyApplication implements CommandLineRunner {

  @Autowired
  private MyAsyncService myAsyncService;

  public static void main(String[] args) {

    SpringApplication.run(MyApplication.class, args);
  }

  @Override
  public void run(String... args) throws Exception {
    log.info("start app");
    try {
      // max pool size:2, queue size:1
      myAsyncService.executeAsyncProcess(); // will be executed
      myAsyncService.executeAsyncProcess(); // will be executed on another thread
      myAsyncService.executeAsyncProcess(); // will be queued
      myAsyncService.executeAsyncProcess(); // will not be executed due to shortage of thread pool
    } catch (TaskRejectedException e) {
      log.warn(e.getMessage());
    }
    log.info("finish app");
  }
}

実行結果

  • メインの処理が終わってから非同期のメソッドが動いている
  • myTask-1myTask-2の二つは並列で動いている
  • 3つ目の処理はmaxPoolSizeを超えるので上記の2つのどちらかが終わるまで待ってから実行 (今回は myTask-1が先に空いた)
  • 4つ目の処理は受け付けられないため、Errorが発生 (catchに書いたWARN logが出ている)。
2022-07-07 21:44:23.914  INFO 66165 --- [           main] c.y.dev.api.async.MyApplication          : Started MyApplication in 2.479 seconds (JVM running for 2.874)
2022-07-07 21:44:23.916  INFO 66165 --- [           main] c.y.dev.api.async.MyApplication          : start app
2022-07-07 21:44:23.918  WARN 66165 --- [           main] c.y.dev.api.async.MyApplication          : Executor [java.util.concurrent.ThreadPoolExecutor@32eae6f2[Running, pool size = 2, active threads = 2, queued tasks = 1, completed tasks = 0]] did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$785/0x0000000800fe2a40@550e9be6
2022-07-07 21:44:23.918  INFO 66165 --- [           main] c.y.dev.api.async.MyApplication          : finish app
2022-07-07 21:44:23.924  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : start async process
2022-07-07 21:44:23.924  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : sleep 1s
2022-07-07 21:44:23.924  INFO 66165 --- [       myTask-2] c.y.dev.api.async.MyAsyncService         : start async process
2022-07-07 21:44:23.924  INFO 66165 --- [       myTask-2] c.y.dev.api.async.MyAsyncService         : sleep 1s
2022-07-07 21:44:24.928  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : finish async process
2022-07-07 21:44:24.928  INFO 66165 --- [       myTask-2] c.y.dev.api.async.MyAsyncService         : finish async process
2022-07-07 21:44:24.929  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : start async process
2022-07-07 21:44:24.929  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : sleep 1s
2022-07-07 21:44:25.933  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : finish async process

Conclusion

Thread数(とキューイングする数)を制限した非同期処理が実装できた。 CompletableFutureFuture を非同期処理の戻り値に使えば非同期処理を待って戻り値を得ることも可能。