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 を非同期処理の戻り値に使えば非同期処理を待って戻り値を得ることも可能。

JSONから特定のキーを削除するスクリプト

Overview

同僚が大量のJSONテストデータを手作業で編集するのが辛いと言っていたときに書いた。

Usage

yoshiki@yoshiki-mbp:python/script ‹main*›$ cat ./test_data/data_remove.json                                 
{
    "key": "value",
    "remove_target1": "value",
    "object": {
        "key": "value",
        "remove_target2": "value"
    }
}
yoshiki@yoshiki-mbp:python/script ‹main*›$ python remove_keys_from_json_file.py ./test_data/data_remove.json
{
  "key": "value",
  "object": {
    "key": "value"
  }
}

Script

  • REMOVE_ITEM_LIST に消したいキーのリストをハードコードしている。引数にすることもできるが、特定のタスクでしか使わないのと連続実行することが多い(しないなら手作業で十分)なので、このままにしている。
  • 指定したキーがネストしたオブジェクトの中にあっても削除される。(上の例の remove_target2
  • 簡単なロジック説明
    • 対象がリストだったら中のオブジェクトを探索する
    • 対象がオブジェクトだったら削除対象チェック+中のオブジェクトとリストを探索する

remove_keys_from_json_file.py

import json
from collections import OrderedDict
import sys

REMOVE_ITEM_LIST = ["remove_target1", "remove_target2"]

def remove_fields(arg):
    if isinstance(arg, list):
        for item in arg:
            remove_fields(item)
    elif isinstance(arg, dict):
        for remove_item in REMOVE_ITEM_LIST:
            if remove_item in arg:
                del(arg[remove_item])
        for value in arg.values():
            remove_fields(value)

if __name__ == '__main__':
    args = sys.argv
    path = args[1]

    with open(path) as f:
        od = json.load(f, object_pairs_hook=OrderedDict)
        remove_fields(od)
        print(json.dumps(od, indent=2, ensure_ascii=False))

Enhance

対象のキー名が十分にユニークなものだったのでこれで上手くいった。 間違ったものを削除しないようにするためにはJSON path (e.g. $.object.remove_target2) で指定できるようにする方法が考えられる。 jsonpath-python というライブラリが便利そう。

pypi.org

探索するロジック自体は他の要件にも使える。 (特定のオブジェクトに特定のkey, valueを追加したい等)

初転職時で入ってから気づいた大変だったこと (新卒4年目に転職、ソフトウェアエンジニア)

Overview

転職したときはそれなりにギャップがあって大変だったが、現状は楽しく働けているので、転職していろいろ辛いと思っている人の助けになればと思う。

Condition

  • 前の会社
    • 新卒で入社
    • 大きいWeb企業のソフトウェア開発をする部署
    • バックエンドエンジニアとして3年と少し
    • エンジニアが1000人以上
    • 英語使わない
  • 今の会社
    • 小売業界のIT部署
    • バックエンドエンジニアを継続して現在一年弱
    • IT部署は会社の一部
    • 英語使う

入ってから気づいた大変だった点

期待値

新卒時の初期の期待値は例年の新卒の平均になるので、周りを見れば自分ができている点・できていない点がわかった。 しかし、中途採用は面接時は自己経歴が期待値になるので、それを超えないと期待を満たせないというプレッシャーがあった。 周りからの評価を気にして漠然と不安になっていたが、自己評価すべき。

同期がいない

軽い相談をできる先がない、他部署に知り合いがいない、一緒にランチに行く友達がいない、など入ってから同期の偉大さを知った。 新卒で同期がたくさんいるのはメンタル的にはかなり良い。

英語

ある程度はコミュニケーションが取れるようになってきたが、聞き返すことも聞き返されることも多く、想像の数倍苦労している。 ポジティブな気持ちのときは、英語を使って仕事をするのは楽しいが、特に仕事がうまくいかない時、技術的に難しいことをしている時に言語の壁もプラスであるのが精神的にキツかった。 コミュニケーションを諦めるのが一番良くないので、言語以外のところで価値を発揮できるように積極的にコミュニケーションをがんばった。

書類・手続き・研修

新卒なら同期と同じ状況なのでなにも思わなかったが、入社時の書類、手続き、フォローアップの研修などを1人で進める必要があり大変だった。 また、前の会社は情報が上手であまりドキュメントを探すのに困らなかった、今の会社はわからないことがたくさんあって辛かった。 わからないものはわからないので、積極的に質問して、次の人が同じ罠にかからないようにドキュメントを残すしかない。

(おまけ) 想定外に良かった点

  • グローバル: ビジネスも働いている人もグローバル。仕事を通して世界を知れるのは楽しい。
  • 多様性: 転職組が多いのでいろいろな考えを知ることができる。また、前よりビジネスサイドとの距離が近い。
  • 複雑なシステム: 課題が多い反面、学ぶことも多いので楽しい。

読書記録 2022/04-06

総評

12冊(月4冊)が目標だったが結果は10冊。 内訳としては以下。

  • 本屋で見つけた面白そうな本: 4
  • 会社で薦めてもらった本: 1
  • 読み直した技術書: 2
  • 新しく読んだ技術書: 3

Fundamentals of Software Architectureは英語で読んでみたが、感覚としては日本語で読む場合の3倍程度は時間がかかっていそう。普通に日本語で読んで残った時間を英語の勉強に当てた方が効率がいいかもしれない。 読み直した技術書は、今のタイミングで読むことでかなり理解が深まったように思う。技術書を読むのには適切なタイミング、レベルがあることを実感。

読んだ本

本屋で見つけた面白そうな本

ライフハック大全 プリンシプルズ

どうやって時間を作るか、効率的に進めるか、習慣の作り方 など小さいテクニックがたくさん載っている本。各テーマについて原則を定義した上で多様なアクションが紹介されているのが良い。特に原則部分は何年経っても役に立つはず。アクションの部分は問題をシステム的に解決しようと試みる点が良い。 一番刺さったのは習慣のトリガーについて。習慣をセットする際はなにをいつトリガーとして実行するかを検討したい。 他のテーマに関しても自己管理に課題があったら再度確認する。

ネイティブなら12歳までに覚える 80パターンで英語が止まらない!

簡単な英会話のフレーズが80個載っている。理解はできても使っていないフレーズが多かったので、自然と口から出せるようになりたかったのが購入動機。音声もwebから入手できるので何周かシャドーイングした。

ファスト&スロー(上) あなたの意思はどのように決まるか? / ファスト&スロー(下) あなたの意思はどのように決まるか?

「速くていくつかの欠陥がある思考」と「遅くて丁寧に考えられる思考」の話。速い思考に頼ると知識があっても判断を誤る。重要な判断のときは直感だけに頼らないのが大事。

会社で薦めてもらった本:

基礎から学べる! 世界標準のSCM教本

SCMのタスクがどう分割されて連携されているかと、それぞれのタスクを抽象的に理解できた。どうPDCAを回すかといった点はどんな業務でも応用できそう。

読み直した技術書

増補改訂版Java言語で学ぶデザインパターン入門

読み直したが、学び直せたパターンはあまり使わないパターンのみだった。今後は辞書的に使うので十分そう。

データ指向アプリケーションデザイン ―信頼性、拡張性、保守性の高い分散システム設計の原理

大規模バックエンドシステムを開発する上で分散システムは欠かせないのでとても参考になる本。しかし、以前は十分な経験がなく、正直パッと理解できな部分が多かった。仕事でRedisやKafkaを使うことがあったためか、以前特に理解が難しかった分散トランザクション周りをある程度理解できた。

新しく読んだ技術書

レガシーコードからの脱却 ―ソフトウェアの寿命を延ばし価値を高める9つのプラクティス

リファクタリングの本かと想定していたが、アジャイルの本だった。よくまとまってはいるが、みたことがあるプラクティスがほとんどだった。エンジニアになって速い段階で読むのが良さそう。

Fundamentals of Software Architecture: An Engineering Approach

ソフトウェアアーキテクトとアーキテクチャの説明 + パターン + テクニックがまとまった本。アーキテクチャ設計の前には再度確認しておきたい。

WEB+DB PRESS Vol.127

リファクタリングPhoenixフレームワーク、入社先の適用が特集。今時のWebフレームワークは小さい努力でWebアプリケーションを構築できることがわかったが、仕事でElixirを使う予定がないので深く学ぶのは見送る、、、。