Beanのスコープとライフサイクル
このレッスンでは、Bean のスコープによってインスタンスがいくつ作られるのかを、実装で確かめられるようになります。
既定は singleton です
Spring の Bean は、何も指定しなければ singleton スコープになります。コンテナの中にインスタンスが1つだけ作られ、必要とするすべての場所へ同じインスタンスが渡されます。
Java
@Service
class OrderService { } // コンテナ内に 1 つだけOrderService を注入しているクラスが10個あっても、渡される OrderService は同じものです。生成の回数が減るので効率がよく、状態を持たないクラスであれば何の問題もありません。
ここで言う singleton は、GoF のシングルトンパターンとは別物です。パターンのほうは JVM 全体で1つですが、Spring の singleton はコンテナごとに1つという意味です。同じアプリケーションの中に複数のコンテナを立てれば、その数だけインスタンスができます。
prototype は取得のたびに作られます
@Scope("prototype") を付けると、コンテナから取得するたびに新しいインスタンスが作られます。
Java
@Component
@Scope("prototype")
class ShoppingCart { } // getBean のたびに新品利用者ごとに状態を持つオブジェクトや、使い捨てのオブジェクトに向いています。
| スコープ | 生成のタイミング | インスタンス数 |
|---|---|---|
| singleton (既定) | コンテナの起動時 | コンテナに1つ |
| prototype | 取得のたび | 取得回数と同じ |
| request | HTTP リクエストごと | リクエスト数と同じ |
| session | HTTP セッションごと | セッション数と同じ |
request と session は Web アプリケーションでのみ使えるスコープです。
singleton が共有されるという事実
ここが実務でいちばん事故を生む点です。singleton の Bean は複数のスレッドから同時に呼ばれます。もしフィールドに状態を持たせていると、あるリクエストの処理中に別のリクエストがその値を書き換えてしまいます。
Java
@Service
class CounterService {
private int count; // 全リクエストで共有される
void add() { count++; } // 競合する
}このため、Service や Repository は状態を持たないように書くのが原則です。リクエストごとに変わる値はフィールドではなくメソッドの引数とローカル変数で持ち回ります。前のレッスンでコンストラクタインジェクションと final を勧めたのも、この原則と地続きです。
prototype を singleton に注入するときの注意
singleton の Bean が prototype の Bean を注入して保持すると、注入は最初の1回しか起きないので、結局そのインスタンスがずっと使い回されます。毎回新しいものが欲しければ、ObjectProvider を注入して必要なときに取得するなど、取得の側を工夫する必要があります。スコープは「注入された相手が自動で更新される」仕組みではない、と覚えておいてください。
演習で書くこと
スコープ名と取得回数を受け取り、実際に生成されたインスタンスが何個になるかを返します。singleton なら常に1、prototype なら取得回数と同じになるはずです。コンテナが裏でやっている「作るか、取っておいたものを返すか」の分岐を、自分の手で書きます。
要件
- Cart は生成のたびに静的なカウンタを 1 増やすこと。run の先頭でカウンタを 0 に戻すこと
- ScopedContainer の getBean は singleton なら保持したインスタンスを返し、prototype なら毎回 new すること
- run は singleton:1 / prototype:3 のような文字列を返し、未知のスコープでは unknown scope を返すこと
入出力例
run("singleton", 3) → "singleton:1"
run("prototype", 3) → "prototype:3"
run("singleton", 1) → "singleton:1"
run("prototype", 5) → "prototype:5"
run("singleton", 0) → "singleton:0"
run("request", 3) → "unknown scope"