メインコンテンツまでスキップ

Spring Boot の骨格: 起動とリクエストの流れ

第 2 部では Spring Boot を使って注文 API を作ります。この章では、まだエンドポイントを 1 つも持たないアプリを起動し、起動のときとリクエストが届いたときに Spring Boot の中で何が起きているかを追います。あわせて、Gradle の設定ファイルの読み方、Kotlin で Spring を使うための設定、設定ファイルと profile を扱います。読み終えると、サンプルリポジトリの app/ にある設定ファイルの各行が何のためにあるかを説明でき、手元で PostgreSQL につないだアプリを起動できるようになります。

この章のコードを取得する​

第 1 部では tag api-06 を取得しました。この章のコードは tag api-07 にあります。はじめに で取得したリポジトリのディレクトリで、次のコマンドを打ってください。

git fetch --tags
git switch --detach api-07

git fetch --tags は、手元のリポジトリがまだ知らない tag を取得します。git switch --detach api-07 は、その tag の時点のコードを作業ツリーに取り出します。api-06 のときと同じく、ブランチではなく tag の時点を取り出すので、detached HEAD の状態になります。手元でコードを書き換えていたら、先に git stash で変更を退避してください。

この章からは Docker を使います。Docker Desktop などを起動しておいてください。

start.spring.io の生成物と app の違い​

Spring Boot のプロジェクトは、ふつう start.spring.io (Spring Initializr) で作ります。言語、ビルドツール、Spring Boot の版、使う機能を選ぶと、そのままビルドできるプロジェクトの一式が手に入ります。サンプルリポジトリの app/ も、この生成物を出発点にしています。ただし、生成物のままの形ではありません。違いは次の表のとおりです。

項目start.spring.io の生成物サンプルリポジトリ
プロジェクトの数1 つルートの下に app と kotlin-basics の 2 つ
ライブラリの版を書く場所build.gradle.ktsgradle/libs.versions.toml
ライブラリの取得先を決める場所build.gradle.kts の repositoriessettings.gradle.kts
Flyway のテスト用の starter足す足さない
compose.yaml の PostgreSQL の imagepostgres:latestpostgres:18
設定ファイルの形式application.propertiesapplication.yaml

starter は、ある機能に要るライブラリをひとまとめにした依存です。たとえば spring-boot-starter-webmvc を 1 つ足すと、Spring MVC と組み込みの Tomcat と Jackson がそろいます。入力の検証の starter は、検証を扱う章で足します。

2 つのサブプロジェクト​

サンプルリポジトリは、第 1 部のコード例 (kotlin-basics) とアプリ (app) を 1 つのリポジトリに置いています。ルートの settings.gradle.kts が、2 つを同じビルドのサブプロジェクトとして登録します。

settings.gradle.kts
rootProject.name = "kotlin-spring-boot-order"

dependencyResolutionManagement {
// 依存の取得先はここだけで決める。サブプロジェクトが別の取得先を足すとビルドを失敗させる。
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenCentral()
}
}

include("app", "kotlin-basics")

include に並べた名前が、サブプロジェクトのディレクトリの名前です。Gradle のタスクは :app:bootRun のように、サブプロジェクトの名前を前に付けて指定します。

ライブラリの取得先も、ここで 1 回だけ決めています。start.spring.io の生成物は build.gradle.kts に repositories { mavenCentral() } を書きますが、サブプロジェクトが 2 つあると、それぞれに同じことを書くことになります。FAIL_ON_PROJECT_REPOS を指定すると、サブプロジェクトの側で取得先を足したときにビルドが失敗するので、取得先が知らないうちに増えることがありません。

ライブラリの版を 1 か所にまとめる​

ライブラリとプラグインの版は、version catalog と呼ぶ gradle/libs.versions.toml にまとめています。

gradle/libs.versions.toml (抜粋)
[versions]
kotlin = "2.3.21"
spring-boot = "4.1.2"
spring-dependency-management = "1.1.7"
# ...
[libraries]
# app が使うもの。版は Spring Boot の依存管理が決める。
spring-boot-starter-webmvc = { module = "org.springframework.boot:spring-boot-starter-webmvc" }
spring-boot-starter-data-jdbc = { module = "org.springframework.boot:spring-boot-starter-data-jdbc" }
spring-boot-starter-flyway = { module = "org.springframework.boot:spring-boot-starter-flyway" }
# ...
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management" }

[libraries] の行の多くは版を持ちません。Spring Boot が動作を確かめたライブラリの版の組み合わせを持っていて、io.spring.dependency-management のプラグインがその組み合わせから版を補うためです。版を書くのは、Spring Boot が管理しないライブラリ (テストで使う MockK など) と、プラグインだけです。

各サブプロジェクトの build.gradle.kts は、このファイルの名前を libs. から始まる名前で参照します。spring-boot-starter-webmvc は libs.spring.boot.starter.webmvc になり、- が . に変わります。

Flyway のテスト用の starter を足さない理由​

start.spring.io は、本体の starter ごとに、テスト用の starter (-test の付いたもの) を足します。app/ は、そのうち Flyway のテスト用の starter (spring-boot-starter-flyway-test) だけを足していません。

app/build.gradle.kts (抜粋)
testImplementation(libs.spring.boot.starter.data.jdbc.test)
/*
* Flyway の test 用の starter (spring-boot-starter-flyway-test) は、start.spring.io の生成物と違って足さない。
* 本体の Flyway の starter を依存に含むので、足すと本体の starter が抜けても、テストの classpath に
* Flyway の自動設定 (spring-boot-flyway) が残り、起動確認のテスト (ShopApplicationTests) が通ってしまう。
* flyway-test が持つほかの依存 (starter-test と starter-jdbc-test) は、data-jdbc-test も持つ。
*/
testImplementation(libs.spring.boot.testcontainers)

Flyway は、アプリの起動時にデータベースのテーブルを作る仕組みです (次の章で扱います)。app/ には、起動したときに Flyway が動いたことを確かめるテストが 1 つあります。Flyway のテスト用の starter は本体の Flyway の starter を依存に含んでいるので、これを足すと、誰かが本体の側の Flyway を依存から消しても、テストの実行時には Flyway が残ります。テストは通るのに、本番で起動したアプリはテーブルを作りません。このテストが本体の依存の抜けを見つけられるのは、テストの依存が本体の依存を持ち込まないあいだだけです。

後の章でテスト用の starter を足すときも、それが本体の starter を持ち込むかを確かめます。

Gradle の Kotlin DSL​

build.gradle.kts は Gradle の設定を Kotlin で書くファイルで、Kotlin DSL と呼びます。中身は Kotlin のコードなので、第 1 部で扱った書き方 (ラムダを最後の引数に渡す形など) がそのまま出てきます。app/build.gradle.kts の冒頭は、使うプラグインの宣言です。

app/build.gradle.kts (抜粋)
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependency.management)
}

kotlin.jvm は Kotlin を JVM 向けにコンパイルするプラグインです。spring.boot は、実行できる jar を作るタスクや、アプリを起動する bootRun タスクを足します。kotlin.spring は次の節で扱います。

依存は dependencies に、どの場面で使うかを表す名前と一緒に書きます。

app/build.gradle.kts (抜粋)
dependencies {
implementation(libs.spring.boot.starter.webmvc)
implementation(libs.spring.boot.starter.data.jdbc)
implementation(libs.spring.boot.starter.flyway)
// ...
runtimeOnly(libs.postgresql)
// bootRun のときだけ classpath に入り、app/compose.yaml の PostgreSQL を立てて接続先にする。jar には入らない。
developmentOnly(libs.spring.boot.docker.compose)

testImplementation(libs.spring.boot.starter.webmvc.test)
// ...
}
名前使う場面
implementationコンパイルと実行
runtimeOnly実行だけ。コードから直接は参照しない (PostgreSQL の JDBC ドライバなど)
developmentOnlybootRun で起動したときだけ。作る jar には入らない
testImplementationテストのコンパイルと実行
testRuntimeOnlyテストの実行だけ

developmentOnly は Spring Boot のプラグインが足す名前です。本番で動かす jar に、開発のための仕組みが入りません。

kotlin-spring プラグイン​

Kotlin のクラスとメンバーは、何も付けなければ final で、継承もオーバーライドもできません (第 1 部の クラスとオブジェクト)。一方、Spring のいくつかの機能は、実行時にクラスを継承した代理のクラス (プロキシ) を作って動きます。メソッドの呼び出しをプロキシが受け、前後に処理を挟んでから元のメソッドを呼ぶ仕組みです。後の章で使うトランザクションの @Transactional がその例です。final のクラスは継承できないので、プロキシを作れません。

すべてのクラスに open を書く代わりに、kotlin-spring プラグインを使います。このプラグインは、@Component・@Async・@Transactional・@Cacheable・@SpringBootTest のどれかを付けたクラスと、そのメンバーを open にします1。これらをメタ注釈に持つ注釈 (@Configuration・@Service・@RestController など) を付けたクラスも対象です。注釈の無いクラスは final のまま残るので、Kotlin の既定の安全さは保たれます。

プラグインが無いと、@Bean メソッドを持つ @Configuration のクラスで、Spring の起動が止まります2。そのときのメッセージは @Configuration class '...' may not be final. Remove the final modifier to continue. です。

Java との相互運用​

Spring は Java で書かれています。Kotlin から Java のコードを呼ぶときに気をつける点が 3 つあります。

プラットフォーム型​

Java の型には、null を許すかどうかの区別がありません。Kotlin は、Java のメソッドが返す値が null になりうるかを判断できないとき、その型を プラットフォーム型 として扱います。コンパイラのメッセージには String! のように ! を付けて表示されます。

プラットフォーム型の値は、null を許す型と許さない型の、どちらとしても使えます。コンパイラは null の確かめを求めないので、実際に null が返ってきたときは、使った場所で実行時に NullPointerException になります。第 1 部で見た null 安全の検査が、Java との境目では働かないということです。

JSpecify の注釈​

Spring Framework 7 と Spring Boot 4 は、API に JSpecify の注釈を付けています。パッケージに @NullMarked を付けて「注釈の無い型は null を許さない」と宣言し、null を返しうる箇所にだけ @Nullable を付ける形です。Kotlin 2.1 以降は、この注釈を厳密に読みます3。

たとえば、設定の値を名前で読む PropertyResolver.getProperty(String key) は @Nullable String を返すと宣言されています。Kotlin からは String? を返す関数に見えるので、null の確かめを書かずに値を使おうとすると、コンパイルが通りません。Spring の API の多くは、プラットフォーム型でなく、Kotlin の null 許容の型として見えます。

古いライブラリには、JSR-305 という別の注釈で null の許容を表しているものがあります。app/build.gradle.kts のコンパイラの設定にある -Xjsr305=strict は、その注釈も厳密に読む指定です。

app/build.gradle.kts (抜粋)
kotlin {
compilerOptions {
// ...
freeCompilerArgs.addAll("-Xjsr305=strict", "-Xannotation-default-target=param-property")
}
}

注釈の付く場所​

もう 1 つの -Xannotation-default-target=param-property は、注釈がどこに付くかを決める指定です。

Kotlin のコンストラクタに val で書いたプロパティは、Java から見ると、コンストラクタの引数、フィールド、getter の 3 つになります。そこに注釈を書くと、どれに付けるかを決める必要があります。この指定のもとでは、注釈は引数と、プロパティ (プロパティに付けられない Java の注釈ではフィールド) の両方に付きます。入力の検証の注釈は、検証する側がフィールドか getter を見るので、引数にしか付かないと検証が行われません。この指定は Kotlin 2.2 で入った新しい規則を先に使うもので、Spring Boot の文書が推奨しています4。注釈の付け方は、入力の検証の章でもう一度扱います。

起動の流れ​

アプリの入り口は ShopApplication.kt です。

app/src/main/kotlin/com/example/shop/ShopApplication.kt
package com.example.shop

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication

@SpringBootApplication
@ConfigurationPropertiesScan
class ShopApplication

fun main(args: Array<String>) {
runApplication<ShopApplication>(*args)
}

main の runApplication が Spring Boot を起動します。@SpringBootApplication は 3 つの注釈をまとめたものです5。

  • @SpringBootConfiguration: このクラスが設定のクラスであることを表します
  • @EnableAutoConfiguration: 次の節の自動設定を有効にします
  • @ComponentScan: このクラスの package (com.example.shop) とその下を探し、@Component などを付けたクラスを読み込みます

@ConfigurationPropertiesScan は、この章の後半で扱う設定のクラスを探す注釈です。

自動設定​

starter を依存に足すと、その機能の 自動設定 のクラスが classpath に入ります。自動設定のクラスは、アプリが使う部品 (データベースへの接続、JSON の変換、Web サーバーなど) を、条件を満たしたときだけ作ります。条件には、あるクラスが classpath にあることや、ある設定の値があること、同じ種類の部品をアプリがまだ作っていないことがあります。

最後の条件が、自動設定の考え方の中心です。たとえば、エラーの応答の中身を作る部品 (ErrorAttributes) は、アプリがその種類の部品を定義していないときだけ、Spring Boot が既定のものを作ります6。アプリが自分で定義すれば、自動設定はその部品を作るのをやめます。既定のまま使い、要るところだけを自分で差し替える、という使い方ができます。

条件を満たさなければ、その部品は作られません。データベースの接続先の設定が何も無いまま app/ を起動すると、次のように止まります。

接続先が無いときの起動のログ (抜粋)
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

どの自動設定が使われ、どれが条件で外れたかは、起動のときに --debug を付けると一覧で出ます7。

DI コンテナ​

起動の途中で作られた部品は、Spring の DI コンテナ (ApplicationContext) が持ちます。コンテナが持つ部品を bean と呼びます。bean には、自動設定が作るもの、@ComponentScan が見つけたクラスから作るもの、設定のクラスの @Bean メソッドが返すものがあります。

ある bean が別の bean を使うときは、自分では作らずに、コンストラクタの引数で受け取ります。コンテナは引数の型を見て、合う bean を渡します。この形を DI (依存性の注入) と呼びます。Kotlin では、プライマリコンストラクタに引数を並べるだけで済みます。コンストラクタが 1 つなら、注釈は要りません。DI の使い方は、サービス層を扱う章で詳しく見ます。

Docker Compose で PostgreSQL を立てる​

このガイドのアプリは PostgreSQL を使います。手元で起動するときの PostgreSQL は、app/compose.yaml に書いてあります。

app/compose.yaml
services:
postgres:
image: 'postgres:18'
environment:
- 'POSTGRES_DB=shop'
- 'POSTGRES_USER=shop'
- 'POSTGRES_PASSWORD=shop'
ports:
- '5432'

依存に足した spring-boot-docker-compose (developmentOnly) は、起動のときに作業ディレクトリでこのファイルを探し、docker compose up でコンテナを立てます8。コンテナの image の名前 (postgres) から PostgreSQL だと判断し、コンテナの環境変数 (データベース名・ユーザー・パスワード) と、手元に割り当てられたポートを読み取って接続先にします。ports に '5432' だけを書くと、手元の側のポートは空いている番号が選ばれます。手元にほかの PostgreSQL があっても、ポートがぶつかりません。

image の版は、テストで使う PostgreSQL (Testcontainers) と同じ postgres:18 にしています。latest にすると、新しいメジャー版が出た時点で、手元とテストで違う版の PostgreSQL を使うことになります。

起動はリポジトリのルートで次のコマンドを打ちます。:app:bootRun の作業ディレクトリは app/ なので、app/compose.yaml が見つかります。

./gradlew :app:bootRun

起動のログの中に、次のような行が出ます (日時やプロセスの番号などは省いています)。

起動のログ (抜粋)
The following 1 profile is active: "dev"
Using Docker Compose file /path/to/kotlin-spring-boot-order/app/compose.yaml
Container app-postgres-1 Started
Tomcat started on port 8080 (http) with context path '/'
Started ShopApplicationKt in 5.035 seconds

Tomcat started on port 8080 の行が出れば、アプリはリクエストを受けられる状態です。止めるときは、ターミナルで Ctrl+C を押してください。Spring Boot がコンテナに docker compose stop を送るので、コンテナは止まりますが消えません。次に起動したときは、同じコンテナが動き出し、データベースの中身も残っています。

リクエストの流れ​

起動したアプリに、別のターミナルからリクエストを送ってみます。

curl -s -w '\n%{http_code}\n' http://localhost:8080/api/v1/orders
結果
{
"timestamp" : "2026-10-23T22:28:53.139Z",
"status" : 404,
"error" : "Not Found",
"path" : "/api/v1/orders"
}
404

-s は curl の進み具合の表示を消し、-w '\n%{http_code}\n' は応答の本文の後に HTTP のステータスコードを出します。このガイドの curl の例は、この形で応答を見ます。Windows では、Git Bash か WSL のシェルで打ってください (コマンドプロンプトは単一引用符を引用符として扱いません)。

/api/v1/orders を受け持つ処理はまだ無いので、結果の最後の行のステータスコードは Not Found を表しています。それでも、JSON の本文が返ってきています。この本文を誰が作ったかは、Spring MVC のログを詳しく出すと分かります。

./gradlew :app:bootRun --args='--logging.level.org.springframework.web=debug'

この状態で同じ curl を打つと、1 つのリクエストについて次のログが出ます (行の頭の日時などと、行の途中の一部を省いています)。

ログ (抜粋)
DispatcherServlet : GET "/api/v1/orders", parameters={}
SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], ...]
ResourceHttpRequestHandler : Resource not found for path [api/v1/orders]
DefaultHandlerExceptionResolver : Resolved [org.springframework.web.servlet.resource.NoResourceFoundException: No static resource api/v1/orders for request '/api/v1/orders'.]
DispatcherServlet : Completed 404 NOT_FOUND
DispatcherServlet : "ERROR" dispatch for GET "/error", parameters={}
RequestMappingHandlerMapping : Mapped to org.springframework.boot.webmvc.autoconfigure.error.BasicErrorController#error(HttpServletRequest)
HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json]
DispatcherServlet : Exiting from "ERROR" dispatch, status 404

ログを上から読むと、リクエストは次の順に処理されています。

  1. 組み込みの Tomcat がリクエストを受け、Spring MVC の入り口である DispatcherServlet に渡します
  2. DispatcherServlet は、URL と HTTP メソッドを受け持つ処理 (ハンドラ) を探します。コントローラ (10 章で作ります) が 1 つも無いので、最後に残った静的なファイルの配信 (ResourceHttpRequestHandler) に回ります
  3. 静的なファイルにも api/v1/orders は無いので、NoResourceFoundException が投げられます。例外を受けた DefaultHandlerExceptionResolver が、例外の種類からステータスコードを 404 に決めます
  4. Tomcat は、エラーのステータスコードを受けると、Spring Boot が登録したエラーページ /error にリクエストを転送します (ログの "ERROR" dispatch)
  5. /error は BasicErrorController が受け持ちます。エラーの中身を Map で返し、Jackson がそれを JSON に変えます

コントローラを作った後のリクエストは、2 のところでコントローラのメソッドに渡り、メソッドの戻り値が 5 と同じく JSON に変えられます。応答の本文の timestamp は 5 で作った時刻です。エラーの応答の形を変える方法は、エラーレスポンスの章で扱います。

仮想スレッド​

Tomcat は、リクエストを 1 件ずつスレッドで処理します。データベースへの問い合わせを待つあいだ、そのスレッドは何もせずに止まっています。従来のスレッド (プラットフォームスレッド) は OS のスレッドと 1 対 1 で、数を増やすほどメモリを使うので、Tomcat は決まった数のスレッドを使い回していました。同時に届いたリクエストがその数を超えると、空きを待つことになります。

仮想スレッド は、Java 21 から使える軽いスレッドです。仮想スレッドが待ちに入ると、その下で動いていた OS のスレッドは別の仮想スレッドの処理に回ります。リクエストごとに仮想スレッドを作っても、OS のスレッドは少ないままで済みます。

Spring Boot では、設定の値 1 つで、Tomcat がリクエストを仮想スレッドで処理するようになります9。

app/src/main/resources/application.yaml
spring:
application:
name: shop
threads:
virtual:
enabled: true

shop:
listing:
default-limit: 20
max-limit: 100

spring.threads.virtual.enabled が、その設定の値です。この設定のとき、Spring Boot は Tomcat に、名前が tomcat-handler- で始まる仮想スレッドでリクエストを処理させます10。前の節のログの 1 行を、省かずに載せます。

ログ (1 行)
2026-10-24T07:31:17.753+09:00 DEBUG 185150 --- [shop] [omcat-handler-0] o.s.web.servlet.DispatcherServlet : GET "/api/v1/orders", parameters={}

角括弧の 2 つ目の欄がスレッドの名前で、tomcat-handler- で始まる名前の頭の 1 文字が、欄の幅に収まらずに切れています。Spring Boot の文書は Java 24 以降で使うことを強くすすめていて、このガイドの Java 25 はそれを満たします。

コードの書き方は変わりません。リクエストを処理するメソッドは、従来どおり、待ちが入る処理をそのまま順に書きます。

設定ファイルと profile​

application.yaml は、アプリの設定の値を書くファイルです。前の節の spring.threads.virtual.enabled のような Spring Boot が読む値と、shop.listing のようなアプリが自分で決めた値を、同じファイルに書きます。

環境によって変えたい値は、profile ごとのファイルに分けます。application-<profile の名前>.yaml という名前のファイルは、その profile を有効にしたときだけ読まれ、application.yaml の同じ名前の値を上書きします。app/ には、手元で動かすための dev の profile があります。

app/src/main/resources/application-dev.yaml
spring:
jackson:
serialization:
indent-output: true

indent-output は、JSON を改行と字下げを入れて返す設定です。前の節の結果の JSON が字下げされていたのは、この設定のためです。本番では、通信量を減らすために字下げしない形で返します。

dev の profile は、bootRun で起動したときに有効になるようにしてあります。

app/build.gradle.kts (抜粋)
tasks.bootRun {
// 手元で動かすときの設定 (application-dev.yaml) を使う。jar を java -jar で動かすときは付かない。
systemProperty("spring.profiles.active", "dev")
}

設定の値は、ファイルのほかに、環境変数やコマンドラインの引数でも渡せます。同じ名前の値が複数の場所にあるときは、強いほうが勝ちます。主な場所を弱いほうから並べると、次の順です11。

  1. jar の中の application.yaml
  2. jar の中の profile ごとのファイル (application-dev.yaml など)
  3. OS の環境変数
  4. Java のシステムプロパティ (-D で渡すもの)
  5. コマンドラインの引数 (-- で始まるもの)

前の節で使った --args='--logging.level.org.springframework.web=debug' は 5 にあたり、ファイルの値より優先されます。bootRun が profile を渡す systemProperty は 4 です。そのため、環境変数 SPRING_PROFILES_ACTIVE で profile を変えようとしても、bootRun では dev が勝ちます。bootRun で別の profile を使いたいときは、--args='--spring.profiles.active=<名前>' と引数で渡してください。

設定をクラスで受け取る​

アプリが自分で決めた設定の値は、@ConfigurationProperties を付けたクラスで受け取ります。app/ は、一覧 API が 1 回に返す件数の設定を ListingProperties で受け取ります。

app/src/main/kotlin/com/example/shop/product/ListingProperties.kt (抜粋)
@ConfigurationProperties("shop.listing")
data class ListingProperties(
val defaultLimit: Int,
val maxLimit: Int,
) {
init {
require(maxLimit >= 1) { "shop.listing.max-limit は 1 以上にしてください: $maxLimit" }
require(defaultLimit in 1..maxLimit) {
"shop.listing.default-limit は 1 から max-limit ($maxLimit) にしてください: $defaultLimit"
}
}
}

@ConfigurationProperties("shop.listing") は、shop.listing の下の値をこのクラスに入れる指定です。YAML の default-limit は、名前の - を外したキャメルケースのプロパティ defaultLimit に入ります。Spring Boot はコンストラクタの引数に値を渡してインスタンスを作るので、プロパティは val のままで済みます12。作ったインスタンスは bean になり、ほかの bean がコンストラクタで受け取れます。ShopApplication の @ConfigurationPropertiesScan が、このクラスを見つけて bean にする役目を持ちます。

init の require は、第 1 部の 例外と失敗の表し方 で扱った引数の確かめです。値が矛盾していれば、インスタンスを作る時点、つまりアプリの起動の時点で止まります。既定の件数をわざと 0 にして起動すると、次のように止まります。

./gradlew :app:bootRun --args='--shop.listing.default-limit=0'
起動のログ (抜粋)
APPLICATION FAILED TO START

Description:

Failed to bind properties under 'shop.listing' to com.example.shop.product.ListingProperties:

Reason: java.lang.IllegalArgumentException: shop.listing.default-limit は 1 から max-limit (100) にしてください: 0

設定の誤りを、一覧 API が初めて呼ばれたときではなく起動のときに見つけられます。この設定は一覧 API の章で使います。

本番で効く注意点​

手元のための設定を本番に持ち込まない​

dev の profile は bootRun のときだけ有効になり、Docker Compose の対応は developmentOnly なので jar に入りません。本番で java -jar で起動したアプリは、どちらも使いません。この分け方を崩さないために、application.yaml に spring.profiles.active を書かないでください。書くと、本番でもその profile が有効になります。本番で profile を使うときは、本番の環境の側で環境変数などから渡します。

秘密の値をファイルに書かない​

compose.yaml のパスワードは、手元のコンテナのためのものです。本番のデータベースのパスワードは、application.yaml にもリポジトリのどのファイルにも書かず、環境変数か、クラウドの秘密の値の保管庫から渡します。環境変数の名前は、設定の名前の . を _ に変え、- を消して大文字にしたものです (spring.datasource.password なら SPRING_DATASOURCE_PASSWORD)13。

仮想スレッドにすると、接続の数が先に足りなくなる​

仮想スレッドでは、同時に処理するリクエストの数がスレッドの数で抑えられなくなります。代わりに、データベースの接続の数が上限になります。Spring Boot が使う接続プールの HikariCP は、既定で接続を 10 本までしか作りません14。接続がすべて使われているあいだ、次に接続を求めたリクエストは空きを待ち、待ちが長引けば失敗します。同時に届くリクエストが増える本番では、接続の数と、データベースが受けられる接続の数をあわせて決めてください。

まとめ​

  • サンプルリポジトリは、start.spring.io の生成物をもとに、2 つのサブプロジェクトと version catalog の形に組み替えています。Flyway のテスト用の starter は、依存の抜けを見逃すので足していません
  • build.gradle.kts は Kotlin で書く Gradle の設定です。依存は implementation や developmentOnly などの名前で、使う場面を分けて書きます
  • kotlin-spring プラグインは、Spring の注釈を付けたクラスとメンバーを open にし、Spring がプロキシを作れるようにします
  • Java の値は、注釈が無ければプラットフォーム型になります。JSpecify の注釈を付けた Spring の API は、Kotlin の null 許容の型として見えます
  • @SpringBootApplication が自動設定と部品の読み込みを有効にします。自動設定は、アプリが同じ種類の bean を定義していないときだけ部品を作ります
  • リクエストは Tomcat から DispatcherServlet に渡り、受け持つ処理が無ければ /error に転送されて JSON のエラーになります
  • spring.threads.virtual.enabled で、リクエストを仮想スレッドで処理します
  • 環境ごとの値は profile ごとのファイルに分けます。コマンドラインの引数が、ファイルの値より強く効きます
  • アプリの設定は @ConfigurationProperties のクラスで受け取り、矛盾した値は起動のときに止めます

次に読む​

前の章は、第 1 部の最後の 例外と失敗の表し方 です。

次章 テーブル設計とマイグレーション では、注文・注文明細・商品・在庫のテーブルを設計し、Flyway で起動のときに作ります。

練習問題​

一覧 API が 1 回に返す件数の上限 (shop.listing.max-limit) を、本番の環境だけ 50 にしたくなりました。application.yaml を書き換えずに設定するには、どうしますか

解答例

本番の環境で、環境変数 SHOP_LISTING_MAXLIMIT に 50 を設定してから、アプリを起動します。

SHOP_LISTING_MAXLIMIT=50 java -jar app.jar

環境変数の名前は、設定の名前 shop.listing.max-limit の . を _ に変え、- を消して大文字にしたものです。環境変数は jar の中の application.yaml より強いので、ファイルの 100 の代わりに 50 が使われます。

環境変数の名前を間違えると (SHOP_LISTINGS_MAXLIMIT のように)、その環境変数はどの設定にも対応せず、ファイルの値のまま動きます。何も知らされないので、設定した後に、起動したアプリの挙動で値を確かめてください。ListingProperties の require が止めるのは値の矛盾 (上限を既定の件数より小さくしたときなど) で、名前の誤りは止めません。

app/ の設定ファイルには、データベースの URL もユーザー名もパスワードも書いてありません。それでも bootRun で起動したアプリが PostgreSQL に接続できるのは、なぜですか

解答例

bootRun のときだけ classpath に入る spring-boot-docker-compose が、接続先を用意しているからです。

起動の途中で、この依存は作業ディレクトリ (app/) の compose.yaml を見つけ、docker compose up で PostgreSQL のコンテナを立てます。続いて、コンテナの image の名前から PostgreSQL だと判断し、コンテナの環境変数 (POSTGRES_DB・POSTGRES_USER・POSTGRES_PASSWORD) と、手元に割り当てられたポートから接続先を組み立てます。データベースへの接続の自動設定は、その接続先を使います。

developmentOnly の依存なので、jar には入りません。jar で起動するときは、接続先を設定の値 (環境変数など) で渡す必要があります。


Footnotes​

  1. 出典: kotlin-spring compiler plugin (Kotlin のドキュメント)。 ↩

  2. 出典: Spring Framework 7.0.10 の ConfigurationClass のソース。static でない @Bean メソッドを持つ @Configuration のクラスが final のときに、この問題として報告します。 ↩

  3. 出典: Kotlin Support: Null-safety (Spring Boot 4.1)。 ↩

  4. 出典: Kotlin Support: Requirements (Spring Boot 4.1)。 ↩

  5. 出典: Spring Boot 4.1.2 の SpringBootApplication のソース。 ↩

  6. 出典: Spring Boot 4.1.2 の ErrorMvcAutoConfiguration のソース。ErrorAttributes の bean が無いときだけ DefaultErrorAttributes を作ります。 ↩

  7. 出典: Auto-configuration (Spring Boot 4.1)。 ↩

  8. 出典: Docker Compose Support (Spring Boot 4.1)。 ↩

  9. 出典: Virtual threads (Spring Boot 4.1)。 ↩

  10. 出典: Spring Boot 4.1.2 の TomcatVirtualThreadsWebServerFactoryCustomizer のソース。 ↩

  11. 出典: Externalized Configuration (Spring Boot 4.1)。表の順は文書の一覧から、このガイドで使うものだけを抜き出しています。 ↩

  12. 出典: Kotlin Support: @ConfigurationProperties (Spring Boot 4.1)。 ↩

  13. 出典: Binding From Environment Variables (Spring Boot 4.1)。 ↩

  14. 出典: HikariCP の README の maximumPoolSize の項。 ↩