Old-School Backend / 2026 ・ No.05

いにしえのバックエンド入門

第5回 Spring Boot に戻る ── DI、3層、そして「Bean は1つ」

公開

端末の画面。./race.sh bad を2回実行すると、200 件中、申請者が入れ替わったもの 91 件と 99 件(例:EXP-2026-2219 は arai が出したのに furui)。./race.sh good では 0 件
担当者(Service)が申請者をフィールドに置く版に、arai と furui が同時に 200 件申請すると、91 件と 99 件で申請者が入れ替わりました。引数で受け取る版は 0 件です(筆者がクラウド上の Linux で、2026年10月31日に撮影。ウインドウの上の帯は説明のために描き足したもの)

第3回で HTTP を、第4回で DB を、素手で扱いました。今回は Spring Boot に戻ります。素手で間違えたことは、本当に Spring Boot が引き受けてくれるのか。そして Spring Boot を使うときに、新しく気をつけることは何か。
第1回のアプリに実験用の窓口と担当者を足し、DI(依存性の注入)、窓口・担当者・倉庫番の3層、Bean は1つ、代理人と @Transactional を実測します。

一

素手から戻る

新井やっと Spring Boot です。これで全部おまかせですね。

古井半分はな。残りの半分は、Spring Boot を使うからこそ起きる事故だ。

新井便利になったのに、事故が増えるんですか。

古井見えないところで動くものが増えるからだ。今日は、その見えないものを数えるところから始める。

全15回の路線図(いまここ:第5回)

1 全体図 2 Java の準備 3 素の HTTP 4 素の JDBC 5 Spring Boot 6 入力チェック 7 例外と返事 8 DB アクセス 9 トランザクション 10 状態遷移 11 認証・認可 12 ファイル 13 ログと監視 14 テスト 15 動かす いまここ 基礎 素手で 受け口 データ 業務と守り 運用
  • 名簿 ── 起動すると Bean が 196 個。自分で書いたのは 8 個(実測)
  • DI ── 部品は自分で作らず、コンストラクターで渡してもらう。Spring なしでも 0.23 秒で動かせる(実測)
  • Bean は1つ ── 担当者のフィールドに申請者を置くと、200 件中 87〜102 件で申請者が入れ替わった(実測)
  • Jackson と Tomcat ── 第3・4回の失敗は、どれも起きなかった(実測)
  • 代理人 ── 同じクラスの中から呼ぶと @Transactional が効かず、半分だけの申請が残った(実測)

舞台は第1回と同じ3台です。アプリは第1回の Spring Boot 4.1.2(Tomcat 11、Jackson 3、JDK 25)に、実験用の窓口(DemoController)と担当者を3つ足しました。DB は第4回で申請者の列を足した PostgreSQL 16 です。

二

Spring Boot の中の名簿

Spring Boot のアプリが起動すると、Spring は必要な部品を作り、つなぎ合わせて、1つの入れ物(ApplicationContext)にしまいます。この部品を Bean と呼びます。起動のあとで、アプリ自身に Bean を数えさせました。

端末の画面。java -jar app.jar --demo.inspect=true の出力。Started ExpensesApplication in 3.787 seconds、Bean の数 196(うち自分で書いたもの 8)、expenseService = ExpenseService$$SpringCGLIB$$0 ← [expenseRepository] などの名簿と、DataSource は HikariDataSource、JsonMapper は tools.jackson.databind.json.JsonMapper
起動したアプリに、自分の中にある Bean を数えさせたところ。196 個のうち自分で書いたのは 8 個。担当者と倉庫番の名前には $$SpringCGLIB$$0 が付いていて、本物ではなく代理人が渡されています(筆者がクラウド上の Linux で、2026年10月31日に撮影。ウインドウの上の帯は説明のために描き足したもの)
# 起動したあと、Spring が作ったもの(Bean)の名簿(← は、そのBean に渡したもの)
Started ExpensesApplication in 4.12 seconds (process running for 4.83)
Bean の数 196(うち自分で書いたもの 8)
expenseController = ExpenseController ← [expenseService]
demoController = DemoController ← [expenseService, badExpenseService, tripService]
expenseService = ExpenseService$$SpringCGLIB$$0 ← [expenseRepository]
badExpenseService = BadExpenseService ← [expenseRepository]
tripService = TripService$$SpringCGLIB$$0 ← [expenseRepository]
expenseRepository = ExpenseRepository$$SpringCGLIB$$0 ← [jdbcClient]
DataSource : dataSource = com.zaxxer.hikari.HikariDataSource
JdbcClient : jdbcClient = org.springframework.jdbc.core.simple.DefaultJdbcClient
PlatformTransactionManager : transactionManager = org.springframework.jdbc.support.JdbcTransactionManager
JsonMapper : jacksonJsonMapper = tools.jackson.databind.json.JsonMapper
DispatcherServlet : dispatcherServlet = org.springframework.web.servlet.DispatcherServlet
ServletWebServerFactory : tomcatServletWebServerFactory = org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory

申請1件が通る Bean と、脇役たち

Tomcat HTTP を受ける DispatcherServlet 窓口を選ぶ ExpenseController 窓口(自分で書いた) ExpenseService 担当者(自分で書いた) ExpenseRepository 倉庫番(自分で書いた) JdbcClient SQL を送る JsonMapper Jackson TransactionManager @Transactional DataSource HikariCP Bean は 196 個。自分で書いたのは 8 個で、残りは Spring Boot が用意した 赤い矢印=Spring がコンストラクターに渡したもの。点線は脇役

Bean は 196 個。自分で書いたのは 8 個(アプリ本体、窓口2つ、担当者3つ、倉庫番、名簿係)で、残りの 188 個は Spring Boot が用意しました。第3回で素手でやった HTTP の受け取りは Tomcat、窓口選びは DispatcherServlet、JSON は Jackson 3 の JsonMapper。第4回の接続の池は HikariCP、トランザクションは JdbcTransactionManager です。そして、担当者と倉庫番の名前の後ろには $$SpringCGLIB$$0 という見慣れない印が付いています。これは第八章で種明かしします。

三

DI:自分で作らず、渡してもらう

担当者のコードをもう一度見ます。倉庫番(ExpenseRepository)を new で作らず、コンストラクターの引数で受け取っています。

@Service
public class ExpenseService {
    private static final Logger log = LoggerFactory.getLogger(ExpenseService.class);
    private final ExpenseRepository repo;

    public ExpenseService(ExpenseRepository repo) {      // 倉庫番は、自分で作らずに渡してもらう
        this.repo = repo;
    }

    @Transactional
    public String submit(ExpenseRequest req) {
        return submitAs("新井", req);
    }

    @Transactional
    public String submitAs(String applicant, ExpenseRequest req) {   // 申請者は引数で受け取る
        String id = "EXP-2026-%04d".formatted(repo.nextNumber());
        log.info("担当者:受付番号を決めた {} {}", id, applicant);
        repo.insert(id, applicant, req);
        return id;
    }

「使う部品を自分で作らず、外から渡してもらう」。これが DI(Dependency Injection、依存性の注入)です。名簿の矢印(←)は、Spring がだれに何を渡したかでした。Spring が起動のときにやっていることを手で書くと、こうなります。

# Spring が起動のときにやっていることを、手で書くと
JdbcClient jdbc = …;                                    // DataSource(HikariCP)から
ExpenseRepository repo = new ExpenseRepository(jdbc);
ExpenseService service = new ExpenseService(repo);
TripService trips = new TripService(repo);             // 倉庫番は1つを使い回す
ExpenseController controller = new ExpenseController(service);

拍子抜けするほど普通の Java です。渡してもらう形にしておくと、渡すものを替えられます。Spring も DB も使わず、偽の倉庫番を渡して担当者だけを動かしました。

# Spring も DB も使わずに、担当者だけを動かす(FakeRun.java の要点)
List<String> written = new ArrayList<>();
ExpenseRepository fake = new ExpenseRepository(null) {          // 偽の倉庫番(DB には行かない)
    long n = 12;
    @Override long nextNumber() { return n++; }
    @Override void insert(String id, String applicant, ExpenseRequest r) { written.add(id + " " + applicant + " " + r.item()); }
};
ExpenseService service = new ExpenseService(fake);                 // DI を手でやる=コンストラクターに渡すだけ
String id = service.submitAs("古井", new ExpenseRequest(LocalDate.of(2026, 10, 31), "交通費", new BigDecimal("1280")));
# 実行(3回)
返ってきた受付番号 EXP-2026-0012、偽の倉庫番に書かれたもの [EXP-2026-0012 古井 交通費](163.5 ms)  JVM の起動から終わりまで 0.23 s
返ってきた受付番号 EXP-2026-0012、偽の倉庫番に書かれたもの [EXP-2026-0012 古井 交通費](201.7 ms)  JVM の起動から終わりまで 0.25 s
返ってきた受付番号 EXP-2026-0012、偽の倉庫番に書かれたもの [EXP-2026-0012 古井 交通費](184.4 ms)  JVM の起動から終わりまで 0.23 s

JVM の起動から終わりまで 0.23〜0.25 秒。Spring Boot を起動すると、それだけで 3.6〜4.1 秒かかりました。担当者が倉庫番を自分で new していたら、DB なしでは担当者を試せません。DI は「テストしやすい形」でもあり、第14回のテストはこの形を前提にします。

POINT

部品は コンストラクターで受け取り、final のフィールドに入れる。コンストラクターが1つなら @Autowired も要りません。いにしえのコードによくある、フィールドに直接 @Autowired を付ける書き方は、Spring なしでは部品を渡す口がなく、テストで困ります。

四

窓口・担当者・倉庫番

第1回から使っている3つの役は、Spring の世界では「3層」と呼ばれる分け方です。

役Spring の印やることやらないこと
窓口@RestControllerHTTP の形(URL、メソッド、JSON、状態コード)を、Java の値と行き来させる業務のきまり、SQL
担当者@Service業務のきまり(受付番号、金額の計算、承認の順番)。トランザクションの範囲HTTP のこと、SQL の文
倉庫番@RepositorySQL を書き、DB と話す。DB の例外を Spring の例外に訳す業務の判断

分ける理由は、変わる理由が違うからです。画面の都合で JSON の形が変わっても担当者は変わらず、DB の表を分けても窓口は変わりません。それぞれを1人で試せるのも、DI と3層の組み合わせのおかげです。窓口に SQL を書きたくなったら、それは担当者か倉庫番の仕事が迷子になっている合図です。

五

Bean は1つ:机の上に置かない

Spring の Bean は、特に指定しなければアプリ全体で1つ(シングルトン)です。名簿の expenseRepository は、3人の担当者に同じ1つが渡されていました。担当者も1つで、同時に来た頼みは、別々のスレッドが同じ担当者を同時に使います。そこで、申請者を担当者のフィールド(机の上)に置く担当者を作りました。ログインの代わりに、申請者を X-User ヘッダーで送ります(本物のログインは第11回)。

# だめな担当者:申請者をフィールドに置く
@Service
public class BadExpenseService {
    private final ExpenseRepository repo;
    private String applicant;                             // ← Bean は1つ。全員が同じ机を使う

    public BadExpenseService(ExpenseRepository repo) {
        this.repo = repo;
    }

    public String submit(String who, ExpenseRequest req) {
        this.applicant = who;
        String id = "EXP-2026-%04d".formatted(repo.nextNumber());   // DB と1往復するあいだに……
        repo.insert(id, this.applicant, req);
        return id;
    }
}
# 200 件を同時に(50 本ずつ)、arai と furui が交互に申請。DB の申請者と比べる(3回)
フィールドに置く版:200 件中、申請者が入れ替わったもの 87 件(例:EXP-2026-1002 は furui が出したのに arai)
フィールドに置く版:200 件中、申請者が入れ替わったもの 102 件(例:EXP-2026-1209 は arai が出したのに furui)
フィールドに置く版:200 件中、申請者が入れ替わったもの 94 件(例:EXP-2026-1402 は furui が出したのに arai)
引数で受け取る版:200 件中、申請者が入れ替わったもの 0 件
引数で受け取る版:200 件中、申請者が入れ替わったもの 0 件
引数で受け取る版:200 件中、申請者が入れ替わったもの 0 件

Bean は1つ。同時に来た2人が、同じ机を使う(実測)

Bean は1つ。同時に来た2人が、同じ机(フィールド)を使う arai の頼み 机(applicant) furui の頼み 机に arai と書く DB と1往復 机を読んで保存 arai furui furui 机に furui と書く 時間 → arai が DB の返事を待つあいだに、furui が同じ机を書きかえた arai の申請が、furui の名前で保存される 実測:200 件中 87〜102 件が入れ替わった。引数で受け取る版は 0 件

200 件中 87〜102 件、ほぼ半分で申請者が入れ替わりました。arai が机に名前を書き、DB に受付番号を聞きに行っているあいだに、furui が同じ机を書きかえるからです。テストで1件ずつ試しているうちは、決して起きません。申請者を引数で受け取る担当者は、3回とも 0 件でした。

新井私の申請が、古井さんの名前になってる……。

古井逆でなくてよかったな。寿司代が君の名前になるところだった。

POINT

Bean のフィールドに置いてよいのは、渡された部品(final)と、変わらない設定だけ。頼みごとごとに違う値(だれが、何を、いくら)は、引数とローカル変数で運びます。第2回の SimpleDateFormat と同じく、「1つを大勢で使う」ものに状態を持たせない。

六

JSON は Jackson に

第3回で正規表現が化けさせた申請を、そのまま Spring Boot に送りました。

# 第3回と同じ、ちょっと意地悪な申請を Spring Boot に
## a ふつう
{"status":"submitted","id":"EXP-2026-2201"}
  -> 201
## b 品目に引用符
{"status":"submitted","id":"EXP-2026-2202"}
  -> 201
## c json.dumps(\u)
{"status":"submitted","id":"EXP-2026-2203"}
  -> 201
## d 内訳つき(入れ子の amount)
{"status":"submitted","id":"EXP-2026-2204"}
  -> 201
## e 閉じかっこがない
{"timestamp":"2026-10-30T15:25:47.433Z","status":400,"error":"Bad Request","path":"/api/expenses"}
  -> 400
## f 金額が文字
{"timestamp":"2026-10-30T15:25:47.462Z","status":400,"error":"Bad Request","path":"/api/expenses"}
  -> 400
# DB に入ったもの
EXP-2026-2201|交通費|1280.00
EXP-2026-2202|会議費 "A社"|3300.00
EXP-2026-2203|交通費|1280.00
EXP-2026-2204|交通費|1280.00
# 一覧の返事(GET)で、引用符入りの品目はこう書かれた
{"id":"EXP-2026-2202","date":"2026-10-31","item":"会議費 \"A社\"","amount":3300.00,"status":"submitted"}

引用符入りの品目は 会議費 "A社" のまま入り、返事では \" と正しく書かれました。交通費 は「交通費」に戻り、内訳の amount に惑わされず金額は 1280 円。閉じかっこのない JSON と、金額が「千二百八十円」の申請は 400 で断られました。

ただし、2つ気になる点があります。1つは、記録(ExpenseRequest)にない receipts を、Jackson が黙って捨てたこと。画面の人は内訳を送ったつもりでも、どこにも残りません。もう1つは、400 の返事が「Bad Request」だけで、どこが悪いのか分からないこと。前者は第6回の入力チェックで、後者は第7回の「例外と返事」で扱います。

七

Tomcat が引き受けたこと

# 壊れた頼みごと、ないメソッド、ない場所
## HELLO
HTTP/1.1 400 
Content-Type: text/html;charset=utf-8
Content-Language: en
Content-Length: 435
Date: Fri, 30 Oct 2026 15:25:47 GMT
Connection: close

<!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</title><style>…</style></head><body><h1>HTTP Status 400 – Bad Request</h1></body></html>
## PUT
{"timestamp":"2026-10-30T15:25:47.802Z","status":405,"error":"Method Not Allowed","path":"/api/expenses"}
  -> 405
## ない場所
{"timestamp":"2026-10-30T15:25:47.828Z","status":404,"error":"Not Found","path":"/api/nothing"}
  -> 404
# 一覧の返事のヘッダー(長さは先に数えず、区切って送る)
HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 30 Oct 2026 15:25:47 GMT

意味のない HELLO は Tomcat が 400(HTML のページ)で断り、PUT には Spring MVC が 405、ない場所には 404 を JSON で返しました。一覧の返事には Content-Length がなく、代わりに Transfer-Encoding: chunked。長さを先に数えず、区切りごとに「ここから何バイト」と書いて送る方法で、第3回の「文字の数で長さを書く」バグは、そもそも起きようがありません。

# 黙っている客
黙っている客:60.1 秒後にサーバーが切った(受け取ったバイト 0)
黙っている客がいる間の申請 -> 201 (0.033746 s)

黙っている客は、60.1 秒でサーバーから切られました。Tomcat の既定の待ち時間(connectionTimeout)は 60 秒で、Spring Boot では server.tomcat.connection-timeout で変えられます。その間も、ほかの申請は 0.034 秒で 201。Tomcat は作業員(スレッド)を最大 200 人まで用意して、来た客を割り当てています。

八

代理人と @Transactional

名簿の ExpenseService$$SpringCGLIB$$0 の種明かしです。Spring は、@Transactional が付いたメソッドを持つ Bean の代わりに、本物を包んだ代理人(プロキシ)を作って渡します。代理人は、呼ばれたらトランザクションを始め、本物を呼び、成功なら commit、例外なら rollback します。第4回で手で書いた setAutoCommit(false)、commit、rollback を、代理人が肩代わりしているのです。倉庫番の代理人は、DB の例外を Spring の例外(第4回の SQLState ではなく DataIntegrityViolationException など)に訳す係です。

第4回の出張の申請(ホテル代 1,000,000 円が明細の列に入らない)を、3つの呼び方で送りました。

# 出張の担当者(TripService の要点)
@Transactional
public String submit(TripRequest t) {
    log.info("出張:トランザクションの中? {}", TransactionSynchronizationManager.isActualTransactionActive());
    String id = "EXP-2026-%04d".formatted(repo.nextNumber());
    repo.insert(id, "新井", new ExpenseRequest(t.date(), t.item(), t.total()));
    int no = 1;
    for (TripRequest.Line l : t.lines()) repo.insertLine(id, no++, l.item(), l.amount());
    return id;
}

public String submitFromInside(TripRequest t) {     // 同じクラスの中から submit を呼ぶ
    return submit(t);
}

@Transactional
public String submitChecked(TripRequest t) throws ReceiptMissingException {
    String id = submit(t);
    throw new ReceiptMissingException("領収書の画像がない " + id);   // チェック例外
}
端末の画面。上はアプリのログで、Creating new transaction、出張:トランザクションの中? true、Rolling back JDBC transaction、PSQLException: numeric field overflow、そのあと 出張:トランザクションの中? false。下は ./trips.sh tx で残った見出し 0 行・明細 0 行、./trips.sh inside で見出し 1 行・明細 1 行
同じ submit を、代理人を通して呼ぶと(tx)トランザクションの中で動いて全部巻き戻り、同じクラスの中から呼ぶと(inside)トランザクションなしで動いて半分だけの申請が残りました(筆者がクラウド上の Linux で、2026年10月31日に撮影。ウインドウの上の帯は説明のために描き足したもの)
# 新幹線 12,860 円+ホテル 1,000,000 円(明細の列に入らない)。checked は明細が正しく、最後にチェック例外
## mode=tx
  -> 500
  残ったもの:expense 0 / expense_line 0
## mode=inside
  -> 500
  残ったもの:expense 1 / expense_line 1
## mode=checked(明細は正しい。最後にチェック例外)
  -> 500
  残ったもの:expense 1 / expense_line 2
# アプリのログ(抜き出し)
15:26:47.982 Creating new transaction with name [example.expenses.TripService.submit]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
15:26:47.983 出張:トランザクションの中? true
15:26:47.987 倉庫番:1行書いた EXP-2026-2206
15:26:47.992 倉庫番:明細を書いた EXP-2026-2206 1
15:26:48.004 Rolling back JDBC transaction on Connection [HikariProxyConnection@1350144235 wrapping org.postgresql.jdbc.PgConnection@59b65dce]
15:26:48.005 Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springf
org.postgresql.util.PSQLException: ERROR: numeric field overflow
15:26:48.170 出張:トランザクションの中? false
15:26:48.173 倉庫番:1行書いた EXP-2026-2207
15:26:48.174 倉庫番:明細を書いた EXP-2026-2207 1
15:26:48.177 Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springf
org.postgresql.util.PSQLException: ERROR: numeric field overflow
15:26:48.308 Creating new transaction with name [example.expenses.TripService.submitChecked]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
15:26:48.309 出張:トランザクションの中? true
15:26:48.310 倉庫番:1行書いた EXP-2026-2208
15:26:48.311 倉庫番:明細を書いた EXP-2026-2208 1
15:26:48.312 倉庫番:明細を書いた EXP-2026-2208 2
15:26:48.312 Committing JDBC transaction on Connection [HikariProxyConnection@1851115318 wrapping org.postgresql.jdbc.PgConnection@59b65dce]
15:26:48.315 Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: example.exp
example.expenses.ReceiptMissingException: 領収書の画像がない EXP-2026-2208

代理人を通れば巻き戻る。通らなければ、ただのメソッド(実測)

Controller 代理人 TripService$$SpringCGLIB$$0 ① トランザクション開始 ③ commit/rollback 本物の TripService submit() submitFromInside() this.submit() 同じクラスの中から呼ぶと、代理人を通らない mode=tx 代理人を通る → rollback、残り 0 行 mode=inside 代理人を通らない → 見出し1・明細1が残る mode=checked チェック例外 → commit、3 行とも残る DI で渡されるのは、本物を包んだ代理人のことがある this から呼ぶと代理人を通らない。チェック例外は既定で巻き戻さない
  • tx ── 窓口から代理人を通って submit。トランザクションの中で動き、失敗で全部巻き戻って 0 行
  • inside ── 窓口から submitFromInside を呼び、その中から this.submit()。this は本物の自分なので、代理人を通らない。ログは「トランザクションの中? false」で、第4回と同じ半分だけの申請が残った
  • checked ── 全部書けたあと、領収書がないというチェック例外(Exception の子)を投げた。Spring は既定で、チェック例外では巻き戻さず commit する。3 行とも残った

@Transactional は、付ければ効く魔法ではなく、代理人を通って呼ばれたときだけ効く約束です。トランザクションの範囲にしたい処理は別の Bean に分けて、外から呼ぶ。チェック例外でも巻き戻したいなら rollbackFor を書くか、実行時例外にする。同時に2人が同じ申請を書きかえる話は、第9回でもう一度トランザクションに戻ってきます。

九

素手の仕事は、どこへ行ったか

第3・4回で素手でやって間違えたことを、Spring Boot で(実測)

試したこと 素手(第3・4回) Spring Boot(実測) だれが 品目に引用符 × 会議費 \ 会議費 "A社" Jackson \u エスケープ × そのまま保存 交通費 Jackson 入れ子の amount × 780 円 1280 円・内訳は捨てる Jackson 壊れた JSON・金額が文字 (試していない) 400 Spring MVC HELLO の1行 × 404 400 Tomcat PUT ○ 405(自作) 405 Spring MVC 黙っている客 × 全員止まる 60.1 秒で切る Tomcat 明細の途中で失敗 × 半分残る 0 行(巻き戻し) @Transactional 第3回・第4回の失敗は、Spring Boot ではどれも起きなかった ただし「知らない項目(receipts)を黙って捨てる」のは、Jackson の既定。第6回で考える

第3回と第4回の失敗は、Spring Boot ではどれも起きませんでした。HTTP は Tomcat、窓口選びは Spring MVC、JSON は Jackson、接続は HikariCP、巻き戻しは代理人。素手でやったからこそ、それぞれの名前を聞いたときに「あの仕事だ」と分かります。その代わりに、Bean は1つと代理人という、Spring ならではの約束が増えました。

十

自分の PC で確かめる

試したいことコマンド・書き方
雛形を作る(ブラウザでも可)https://start.spring.io
ビルドせずに動かす./mvnw spring-boot:run
自動設定が何を作り、何を作らなかったかjava -jar app.jar --debug
渡されたものが代理人かlog.info("{}", service.getClass());
いまトランザクションの中かTransactionSynchronizationManager.isActualTransactionActive()
黙っている客を切るまでの時間server.tomcat.connection-timeout=20s

--debug を付けると、起動時に「CONDITIONS EVALUATION REPORT」が出ます。DataSource が作られたのは JDBC ドライバーがあったから、といった Spring Boot の判断が一覧になり、「なぜこの Bean があるのか」を調べる入口になります。

結 — 一枚まとめと、次回

  1. Spring Boot は起動時に Bean を作ってつなぐ。この回のアプリで 196 個、自分で書いたのは 8 個。
  2. DI:部品はコンストラクターで受け取り、final に入れる。偽物を渡せば Spring なしでも試せる。
  3. 窓口(HTTP の形)・担当者(業務のきまりとトランザクション)・倉庫番(SQL)。変わる理由で分ける。
  4. Bean は1つ。頼みごとごとの値をフィールドに置くと、申請者が入れ替わった(200 件中 87〜102 件)。
  5. 第3・4回の失敗は、Tomcat・Spring MVC・Jackson・HikariCP が引き受けた。ただし知らない項目は黙って捨てられる。
  6. @Transactional は代理人を通ったときだけ効く。this から呼ぶと効かず、チェック例外では commit される。

新井内訳を黙って捨てられたのが、地味に気になります。

古井いい目だ。次回は入力チェック。空の品目、マイナスの金額、全角の数字、知らない項目。窓口の手前で、何をどこまで確かめるかを決めよう。

参考

記録は、筆者がクラウド上の Linux(Ubuntu 24.04、カーネル 6.18)で、Spring Boot 4.1.2(Tomcat 11、Jackson 3)、OpenJDK 25.0.4、PostgreSQL 16.15 を使って 2026年10月30日〜31日に取ったものです。PC・申請サーバー・DB サーバーは、1台の中に作った別々のネットワーク空間(network namespace)です。申請者の入れ替わりの件数と起動時間は、動かすたびに変わります。表の数字や出力はすべて、そのときのコマンドの出力から読んだ実際の値です。登場人物は架空の人物です。

いにしえのバックエンド入門 第5回 — Spring Boot に戻る / 2026.10

← 記事一覧へ