Dev Study
← 解説「Bean のライフサイクルとスコープ」に戻る

サンプルコードで身につける: Bean のライフサイクルとスコープ

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

13つのスコープを付け分ける

何も付けなければ です。 で を指定すると取り出すたびに新しいが作られ、 では1つのリクエストの間だけ同じインスタンスが使われます(Web アプリのでだけ有効)。次のサンプルが、この3つを同じリクエストの中で2回ずつ取り出した結果です。

// l12/scope/ScopeApp.java(抜粋)
@Component
class SingletonThing {
    SingletonThing() { System.out.println("new SingletonThing"); }
}

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
class PrototypeThing {
    PrototypeThing() { System.out.println("new PrototypeThing"); }
}

@Component
@RequestScope
class RequestThing {
    RequestThing() { System.out.println("new RequestThing on " + Thread.currentThread().getName()); }
    int id() { return System.identityHashCode(this); }
}

@RestController
class ScopeController {
    private final ObjectProvider<SingletonThing> singletons;
    private final ObjectProvider<PrototypeThing> prototypes;
    private final RequestThing requestThing;   // singleton のコントローラに、1回だけ注入される

    ScopeController(ObjectProvider<SingletonThing> singletons, ObjectProvider<PrototypeThing> prototypes, RequestThing requestThing) {
        this.singletons = singletons;
        this.prototypes = prototypes;
        this.requestThing = requestThing;
        System.out.println("ScopeController constructor: requestThing field class = " + requestThing.getClass().getName());
    }

    // /scope は、それぞれを2回ずつ取り出して System.identityHashCode を返す
}

2スコープごとに、同じものが返るかどうか

/scope を2回呼んだ応答です(数値はインスタンスを見分ける番号で、起動のたびに変わります)。singleton は2回のリクエストを通して同じ、prototype は取り出すたびに違い、request は1つのリクエストの中では同じで次のリクエストでは別です。field の行は、singleton のコントローラのに入っているのが本物の RequestThing ではなく、代理のであることを示しています。

$ curl http://localhost:8080/scope を2回(Spring Boot 4.1.1)コマンド出力
singleton 1170358147 1170358147
prototype 1047642418 1788600152
request 1221071116 1221071116
field 1690257920 RequestThing$$SpringCGLIB$$0
---
singleton 1170358147 1170358147
prototype 1372117442 1594830219
request 1529918130 1529918130
field 1690257920 RequestThing$$SpringCGLIB$$0

3うまくいかない場合: singleton のフィールドに数を持たせる

コントローラは singleton なので、count というフィールドは全リクエストで共有されます。20000 件を同時 50 で送ることを5回繰り返すと、instance は5回とも同じ値で、count は5回とも 20000 に届かず、atomic は5回とも 20000 でした。HTTP の失敗は 0 件なので、呼び出す側からは何も起きていないように見えます。

20000 件・同時 50 を5回(Spring Boot 4.1.1 / JDK 25 / 10 CPU)コマンド出力
--- run 1
reset
requests=20000 concurrency=50 ok=20000 failed=0 wall_ms=1350 min_ms=0 p50_ms=2 max_ms=65
instance=1353582626 count=19546 atomic=20000
--- run 2
reset
requests=20000 concurrency=50 ok=20000 failed=0 wall_ms=883 min_ms=0 p50_ms=1 max_ms=65
instance=1353582626 count=19979 atomic=20000
--- run 3
reset
requests=20000 concurrency=50 ok=20000 failed=0 wall_ms=803 min_ms=0 p50_ms=1 max_ms=45
instance=1353582626 count=19976 atomic=20000
--- run 4
reset
requests=20000 concurrency=50 ok=20000 failed=0 wall_ms=1099 min_ms=0 p50_ms=1 max_ms=67
instance=1353582626 count=19972 atomic=20000
--- run 5
reset
requests=20000 concurrency=50 ok=20000 failed=0 wall_ms=869 min_ms=0 p50_ms=1 max_ms=54
instance=1353582626 count=19968 atomic=20000
// l12/race/RaceApp.java(抜粋)
@RestController
class HitController {
    private int count = 0;                                   // singleton の Bean に持たせた状態
    private final AtomicInteger safe = new AtomicInteger();  // 比較用

    @GetMapping("/hit")
    String hit() { count++; safe.incrementAndGet(); return "ok"; }

    @GetMapping("/count")
    String count() { return "instance=" + System.identityHashCode(this) + " count=" + count + " atomic=" + safe.get(); }

    @GetMapping("/reset")
    String reset() { count = 0; safe.set(0); return "reset"; }
}

4同じ取りこぼしを素の Java で再現する

を使わず、1つだけ作ったオブジェクトを4本のから呼ぶ形にしたものです。count++ は「読む・足す・書き戻す」の3手なので、間に他のスレッドが入ると足した分が消えます(plain の値は実行のたびに変わります)。複数のスレッドが本当に同時に走る環境では、Spring が無くても取りこぼしが起きるということです。コアが1つしか見えない環境で試すと、5回中5回とも 2000000 に届きました。

Java

5対比: Node で同じ書き方をすると

同じ count++ を Node で 20000 回呼ぶと 20000 になります。1つの Node プロセスの JavaScript は(worker_threads を使わない限り)1本のスレッドで順番に動き、count++ の途中で別の処理に切り替わる場所が無いためです。ただし、読んでから書き戻すまでの間に await を挟むと Node でも取りこぼすので(出力の2行目)、安全なのは「スレッドを分けない」という前提の範囲の話です。

JavaScript (Node.js)

図やコードに出てくる用語(タップで説明)