yoshikipom Tech Blog

twitter: https://twitter.com/yoshikipom

技術書を自費で購入して読む理由

はじめに

技術書を自費で買うのか?というようなコメントがあった。結論としては現状は自費で買って読んでいる。確かにそこそこコストがかかるのになぜなのか考えてみた。以下すべて、一般論ではなく自分の一意見。

なぜ読むようになったか

学生のころは技術書高すぎる、、、と思っていたのでたまに研究室の先輩に借りたり大学図書館で読む程度だった。 習慣的に読むようになったのは、運良く最初に入った会社がそういうカルチャーだったからだと思う。具体的には、入社前にオススメの技術書リストを提供、技術書を買うための補助、業務時間内で輪読会といった部分で自然と技術書に触れるようになっていた。

今でも技術書を購入して読む理由

転職してからは補助がなくなったが、今でも技術書には価値を感じていて購入し続けているので以下に理由を3つ書いてみる。

高クオリティかつ体系的な知識を得られるから

まず、技術書は無料のコンテンツと比べてクオリティが高いことが多い。それは、本になるものは作者が知識を詰め込んでいるのはもちろんのこと、出版前にはレビューされたコンテンツだからである。 また、有名な本はレビューもたくさんあるので、いい本や自分の目的にあった本は見つけやすく、必要なコンテンツを探す時間を短縮できる。

また、技術書はあるテーマのためにそれなりのページが費やされており、小さくなりがちな無料コンテンツと比較すると、トップダウンで体系的な知識を学ぶ時はとても理解しやすい。 体系的な知識があるとなにか調べたい時にすぐ検索ワードが思いついたり、検索結果をすんなり理解できたりするので、技術書で読む優先度が高い。

仕事が楽しくなるから

自分は子どものときからゲームの攻略本を読むのが好きなタイプだった。攻略本を読めば自分がプレイで通らなかったルートや使わなかったキャラクターを知ることができた。 仕事でも技術書を読んでおけば自分達が行なわなかった選択に気づくことができ、それらについて話し合ったりする中で連鎖的に知識を増やしていけるのが楽しい。

また、自分は「活躍した方が仕事は楽しい」と最初の会社で教えられたし、今はある程度そう思っている。なので、知識が増やして重要なタスクを任せてもらえる確率が上げることも仕事を楽しむためには必要だと考えている。

投資した以上に回収できるから

具体的に技術書がどれくらい自分のキャリアに貢献しているのかはもちろんわからないが、事実として身につけた知識はそれなりにアウトプットされているし、給料も増えている。 給料が増えるのは面接で体系的な知識が役に立ったからかもしれないし、普段の仕事がスピードアップするからかもしれない。 逆に技術書を全く読まなかったケースを考えてみると、今のようには働けていなかったと思う。

おわりに

言語化難しい。Web or 本とか、電子 or 紙とかを他のエンジニアはどう使い分けてるのか知りたい。

ソフトウェアアーキテクトに必要なシステム設計知識を学んだ17冊

はじめに

仕事では2022年までは主にバックエンドサービスの開発リードをしていたが、今後はソフトウェアアーキテクトとしてサービス全体の設計や横断的なシステムの設計をメインでやっていく。自分で悩んだ時の辞書としての役割と、おすすめの本を求められたときのリストとしての役割を兼ねて今まで読んだシステム設計系の本をまとめる。

チームの移り変わりはあったものの、基本的に以下のような環境で働いてきたので、そこで役立つ情報に偏ってる可能性有り。

  • マイクロサービス
  • バックエンド
  • 同期通信はREST APIでの連携が多め

アーキテクチャ・デザイン全般

ソフトウェアアーキテクチャの基礎

1部でアーキテクトは何を考える必要があるのか説明され、2部で様々なパターンを学ぶことができる。3部ではアーキテクトが効果的に働くための方法を知ることができる。図や具体例が多くて読みやすくかつ網羅的なのでソフトウェアアーキテクチャとは何か知りたい人にオススメ。

Clean Architecture 達人に学ぶソフトウェアの構造と設計

多数の設計原則を説明した上でメンテナブルでスケーラブルなクリーンアーキテクチャを設計する実践的な方法を学ぶことができる。開発チームで設計原則に基づいて意思決定することも多いのでその原則を学ぶだけでも価値がある。自分の場合、肝心のアーキテクチャ部分は社会人一年目だといまいち価値が理解できなかったので、あとから読み直した。

Design It!

デザイン思考に基づいて良いアーキテクチャをどう作り上げるかステップバイステップで説明される。”第Ⅲ部 アーキテクトの道具箱”ではどうやって問題を理解し、設計を可視化し、評価するかについて、様々なアクションで提案されるので、設計プロセスに困ったら目を通し直す価値あり。

ソフトウェアシステムアーキテクチャ構築の原理

とても分厚い上にあまりわかりやすい具体例はない。しかし、アーキテクチャのプロセス、考えなければならないことが網羅的にモレなく記述してある。自分の場合、深く設計が必要なときは一度は開く良書となっている。

データ指向アプリケーションデザイン

直接的にアーキテクチャ設計を取り扱ってはいないが、設計内に含まれるであろうテクノロジーの特性やトレードオフについて学ぶことができる。特に分散システムについてのトレードオフは深く記述されている。アプリケーション内だけでなく、システム全体を設計するのであれば是非読んでほしい。

マイクロサービス

マイクロサービスアーキテクチャ

マイクロサービスについて、利点欠点、何をするべきか、どう進めるべきかを幅広く説明している良書。 自分が読んだのは1st edition。 2nd editionはKubernetestとかマイクロフロントエンドとか流行りに合わせてかなり情報が追加されている。また、構成も整理されていて読みやすそう。

マイクロサービスパターン 実践的システムデザインのためのコード解説

具体的なエピソードに沿って、具体的な設計とコードを使ってパターンを解説してくれる。理想的なマイクロサービスを作る難しさを痛感した。

ソフトウェアアーキテクチャ・ハードパーツ

"ソフトウェアアーキテクチャの基礎"の作者らによる続編。マイクロサービスで出てくる様々なトレードオフに対してどう考えるかを教えてくれる。設計に関する引き出しを増やし、整理してくれる良書だった。

ドメイン駆動設計

エリック・エヴァンスのドメイン駆動設計

DDDの哲学が学べる。一回で理解しきれなかったが、実践や他の本を経て戻ってくるとよくまとまっている印象を受けた。実装の具体例は下の2冊で学んだ。

ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本

各パターンの意味や実装例がとてもわかりやすいし読みやすい。先にこの本で概要を掴んでから他のDDD本を読みたかった。

現場で役立つシステム設計の原則

特にタイトルにはDDDと入っていないが、DDDを用いた設計がメインの本。Java, Spring Frameworkを用いてオブジェクト指向を突き詰めたような本だった。保守しやすい理想的なアプリケーションの書き方の参考にはなるが、DDD初心者には上2冊のほうがオススメ。

要件定義

はじめよう!プロセス設計 ~要件定義のその前に

はじめよう! 要件定義 ~ビギナーからベテランまで

はじめよう!システム設計 ~要件定義のその後に

要件定義が何かもわかっていない最初の段階で読んだ。要件定義とシステム設計の流れが丁寧に説明されているので新卒にもオススメできる。要件定義から自分でしないとしても、どういう流れで要件が出来上がるか知ることで要件を適切に満たす設計ができるようになるので早めに学ぶと良い。

Web, Web API

Webを支える技術

Webまわりの基本的な技術が丁寧に説明されている。ちょっと古い情報、あまり仕事で出会わない技術も入っているが、REST周りの情報の網羅性が高いので今でもオススメできる。

プロになるためのWeb技術入門

「Webを支える技術」と近い範囲もカバーしているが、この本を読むとWebアプリケーションがどう動くかがより理解できる。

Web API: The Good Parts

Web APIに特化した本。実践的で網羅性が高く、API設計初心者のときは何度も参照した。仕事でAPI設計するならオススメ。

おわりに

新しいバージョンが出てるものや、読んでから時間が経っているものは読み直したい、、、。

GoReplayのDockerfileがAppleシリコン(M1) Macでうまく動かなかったので自分で書いた

経緯

GoReplayのリポジトリにあったDockerfileはIntel Macだとうまく動いたが、Appleシリコン(M1) Macではうまく動かなかった。 Dockerfile: https://github.com/buger/goreplay/blob/1.3.3/Dockerfile 実行時に以下のエラーが発生。

tunl0: SIOCETHTOOL(ETHTOOL_GLINK) ioctl failed: Function not implemented

--platform オプションでamd64用にビルドして実行してみたが、結果は同様だった。 たまにこういうことはありそう?

The emulation is not perfect https://github.com/docker/for-mac/issues/5328

結論

以下のDockerfileを書いた。これならIntel MacでもAppleシリコン(M1) Macでもうまく動く。

FROM golang:1.19 as builder

RUN apt-get update && apt-get install ruby vim-common -y

RUN apt-get install flex bison -y
RUN wget http://www.tcpdump.org/release/libpcap-1.10.0.tar.gz && tar xzf libpcap-1.10.0.tar.gz && cd libpcap-1.10.0 && ./configure && make install

WORKDIR /go/src/github.com/buger/goreplay/
RUN wget https://github.com/buger/goreplay/archive/refs/tags/v2.0.0-rc2.tar.gz -O gor.tar.gz && tar xzf gor.tar.gz -C . --strip=1
RUN go mod vendor
RUN go build -mod=vendor -tags netgo -o gor -ldflags "-extldflags \"-static\""


FROM alpine:3.17

RUN apk add bash
RUN apk add --no-cache ca-certificates openssl

COPY --from=builder /go/src/github.com/buger/goreplay/gor /
ENTRYPOINT ["./gor"]

build & run

# build
$ docker build --tag goreplay:2.0.0 .                 

# run
$ docker run --rm -it goreplay:2.0.0 -h
Gor is a simple http traffic replication tool written in Go. Its main goal is to replay traffic from production servers to staging and dev environments.

なぜ元のDockerfileだとダメか

以下で取得してきているGoReplayのプログラムがファイル名でもわかるようにx64のCPUアーキテクチャ用にビルドされたものなのが原因。

RUN wget https://github.com/buger/goreplay/releases/download/${RELEASE_VERSION}/gor_${RELEASE_VERSION}_x64.tar.gz -O gor.tar.gz

Dockerfile: https://github.com/buger/goreplay/blob/1.3.3/Dockerfile

--platform オプションをつけずにdocker buildコマンドを実行するとホストのCPUアーキテクチャ用のbuildする。 - Intel Mac -> amd64 (= x86 https://ja.wikipedia.org/wiki/X64) - Appleシリコン(M1) Mac -> arm64v8 dockerで使われるCPUアーキテクチャの種類: https://github.com/docker-library/official-images#architectures-other-than-amd64

AppleシリコンでBuildした場合、OSはarm64v8のCPUを想定するが、元のDockerfileだとx86用のgoプログラムを取得しているのでミスマッチが起こっている。

どう直したのか

go build コマンドもdocker buildと同様にdefaultは実行ホストのCPUアーキテクチャ用にbuildする。なので、Dockerfileにgo buildコマンドを入れた。 buildコマンドはMakefileから必要なオプションを持ってきた。 https://github.com/buger/goreplay/blob/1.3.3/Makefile ちなみにGOOS=linux GOARCH=amd64 go buildのようにすれば成果物のCPUアーキテクチャの指定もできるが、今回は指定しないほうが都合が良かったので特に指定していない。buildするCPUアーキテクチャとと実行するCPUアーキテクチャが違う時は指定が必要。

Makefileはbuild用のDockerコンテナ内でビルドする作りになっていたが、docker in dockerをしたくなかったのでマルチステージビルドを使って書いてみた。golangイメージでビルドして、alpineイメージで実行。goのプログラムは実行ファイルだけあれば実行できるのですごい。

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に対応できる。