サンプルコードで身につける: 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 ではなく、代理のであることを示しています。
singleton 1170358147 1170358147prototype 1047642418 1788600152request 1221071116 1221071116field 1690257920 RequestThing$$SpringCGLIB$$0---singleton 1170358147 1170358147prototype 1372117442 1594830219request 1529918130 1529918130field 1690257920 RequestThing$$SpringCGLIB$$0
3うまくいかない場合: singleton のフィールドに数を持たせる
コントローラは singleton なので、count というフィールドは全リクエストで共有されます。20000 件を同時 50 で送ることを5回繰り返すと、instance は5回とも同じ値で、count は5回とも 20000 に届かず、atomic は5回とも 20000 でした。HTTP の失敗は 0 件なので、呼び出す側からは何も起きていないように見えます。
--- run 1resetrequests=20000 concurrency=50 ok=20000 failed=0 wall_ms=1350 min_ms=0 p50_ms=2 max_ms=65instance=1353582626 count=19546 atomic=20000--- run 2resetrequests=20000 concurrency=50 ok=20000 failed=0 wall_ms=883 min_ms=0 p50_ms=1 max_ms=65instance=1353582626 count=19979 atomic=20000--- run 3resetrequests=20000 concurrency=50 ok=20000 failed=0 wall_ms=803 min_ms=0 p50_ms=1 max_ms=45instance=1353582626 count=19976 atomic=20000--- run 4resetrequests=20000 concurrency=50 ok=20000 failed=0 wall_ms=1099 min_ms=0 p50_ms=1 max_ms=67instance=1353582626 count=19972 atomic=20000--- run 5resetrequests=20000 concurrency=50 ok=20000 failed=0 wall_ms=869 min_ms=0 p50_ms=1 max_ms=54instance=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 に届きました。
5対比: Node で同じ書き方をすると
同じ count++ を Node で 20000 回呼ぶと 20000 になります。1つの Node プロセスの JavaScript は(worker_threads を使わない限り)1本のスレッドで順番に動き、count++ の途中で別の処理に切り替わる場所が無いためです。ただし、読んでから書き戻すまでの間に await を挟むと Node でも取りこぼすので(出力の2行目)、安全なのは「スレッドを分けない」という前提の範囲の話です。