Spring

IoCコンテナとは?制御の反転の仕組みとSpring IoCコンテナ・Beanのスコープとライフサイクルを動くコードで解説

IoCコンテナとは?制御の反転の仕組みとSpring IoCコンテナ・Beanのスコープとライフサイクルを動くコードで解説

IoCコンテナとは、アプリケーションを構成するオブジェクトの生成・依存関係の解決・破棄を、開発者のコードの代わりに引き受ける仕組みです。クラスの中で new して部品を用意するのをやめ、「何が何を必要とするか」だけを宣言して、組み立てはコンテナに任せます。この「組み立ての主導権を手放す」考え方が制御の反転(Inversion of Control、IoC)です。

自作コンテナと、Springのスコープ・ライフサイクルの章に載せた出力は、JDK 25.0.4.1とspring-context 7.0.10で実行した結果です(それ以外のコードは説明用の断片です)。

まとめ:IoCコンテナの要点

  • IoCコンテナは、依存の連鎖をたどって下流から順にオブジェクトを生成し、注入し、使い終わったら破棄する仕組み。自作しても中核は40行ほどで書ける。
  • IoCは「フレームワークがこちらのコードを呼ぶ」という広い原則で、DIはその代表的な実現手段。名前を分けたのはMartin Fowlerの2004年の記事で、IoCという呼び名が汎用すぎたため。
  • SpringではApplicationContextを使う。素のBeanFactoryは@Autowiredも@PostConstructも処理しないことを実行で確認した。
  • Beanの既定スコープはsingleton。singletonにprototypeを注入すると1個に固定されるので、毎回新しく欲しいときはObjectProviderで取り出す。
  • 初期化は@PostConstruct→afterPropertiesSet()→init-methodの順、破棄も同じ並びで呼ばれる。prototypeの破棄コールバックは呼ばれない。

IoCコンテナとは:オブジェクトの生成と組み立てを引き受ける仕組み

制御の反転(IoC):フレームワークへの呼び出し主導権の移譲

制御の反転は、Martin Fowlerが2005年6月26日の解説「InversionOfControl」で「フレームワークの決定的な特徴」と書いた現象です。ライブラリはこちらが呼び出しますが、フレームワークはこちらが書いたメソッドを、フレームワーク側の都合で呼び出します。Fowlerはこれを「Hollywood Principle(こちらから電話するので、電話してこないで)」とも言い換えています。

オブジェクトの組み立てに当てはめると、次の違いになります。反転前は、OrderServiceが自分で実装クラスを選んで生成します。

public class OrderService {
    // どの実装を使うかをOrderService自身が決めている
    private final OrderRepository repository = new JdbcOrderRepository();
}

この形では、テストでモックに差し替えるにも、別のデータベース実装に替えるにも、OrderService本体を書き換える必要があります。反転後は、必要な部品をコンストラクタで受け取るだけにして、どの実装を渡すかは外側に任せます。

public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

この「外側」を自動化したものがIoCコンテナです。クラスが数百になると、生成順と受け渡しを手で書くコード自体が大きくなるため、それを肩代わりさせます。

40行で作る最小のIoCコンテナ:コンテナが内部でしていること

コンテナの中身はリフレクション(実行時にクラスの構造を読み取る仕組み)で書けます。次のコードは、コンストラクタの引数の型を読み、その型のオブジェクトを先に作ってから本体を生成し、作ったものを1個だけ保持して使い回す、最小のIoCコンテナです。リフレクションそのものの仕組みはリフレクションとは?実行時にクラス情報を操る仕組みを言語横断で解説で扱っています。

import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class MiniContainer {
    private final Map<Class<?>, Object> singletons = new HashMap<>();

    @SuppressWarnings("unchecked")
    public <T> T getBean(Class<T> type) {
        Object cached = singletons.get(type);
        if (cached != null) return (T) cached;
        try {
            Constructor<?> ctor = type.getDeclaredConstructors()[0];
            Class<?>[] paramTypes = ctor.getParameterTypes();
            Object[] args = new Object[paramTypes.length];
            for (int i = 0; i < paramTypes.length; i++) {
                // 依存を先に解決する
                args[i] = getBean(paramTypes[i]);
            }
            T bean = (T) ctor.newInstance(args);
            singletons.put(type, bean);
            System.out.println("生成: " + type.getSimpleName());
            return bean;
        } catch (ReflectiveOperationException e) {
            throw new IllegalStateException(e);
        }
    }

    static class OrderRepository {}
    static class MailSender {}
    static class OrderService {
        final OrderRepository repo; final MailSender mail;
        OrderService(OrderRepository repo, MailSender mail) { this.repo = repo; this.mail = mail; }
    }

    public static void main(String[] args) {
        MiniContainer c = new MiniContainer();
        OrderService s1 = c.getBean(OrderService.class);
        OrderService s2 = c.getBean(OrderService.class);
        System.out.println("同じインスタンスか: " + (s1 == s2));
        System.out.println("repoは共有か: " + (s1.repo == c.getBean(OrderRepository.class)));
    }
}

OrderServiceを1回要求しただけで、依存先の2つが先に生成され、2回目以降は同じインスタンスが返っています。

生成: OrderRepository
生成: MailSender
生成: OrderService
同じインスタンスか: true
repoは共有か: true

依存の連鎖をたどる生成、型による注入、生成済みインスタンスの使い回し(singleton)。IoCコンテナの骨格はこの3点です。実用のコンテナはここに、同じ型の実装が複数あるときの選び分け、寿命(スコープ)の種類、初期化と破棄のコールバック、循環依存の検出、AOPのためのプロキシ差し込みを積み上げています。この自作版は循環依存があると再帰が止まらず、インタフェース型の引数も解決できません。自作せず既存のコンテナを使うのは、この積み上げ分を保守しないためです。

IoCとDI(依存性注入)の違い:Fowlerが名前を分けた理由

「IoCコンテナ」と「DIコンテナ」は、いまはほぼ同じものを指します。呼び名が2つあるのは歴史の名残です。2000年代前半、Javaの軽量コンテナ(PicoContainerやSpring)は自らを「Inversion of Control」のコンテナと呼んでいました。Martin Fowlerは2004年1月23日の記事「Inversion of Control Containers and the Dependency Injection pattern」で、IoCは「あまりに汎用的な用語で、人を混乱させる」として、IoCの支持者たちと議論のうえ、部品を外から差し込むこのパターンを「Dependency Injection」と名付けました。

IoCは「制御の主導権がフレームワーク側にある」という広い原則で、イベントループやテンプレートメソッドも含みます。DIは、そのうち依存オブジェクトを外から差し込むことに絞った手法です。コンストラクタ注入・セッター注入の書き分けや、同じ型のBeanが複数あるときの@Primary・@QualifierはSpringのDI(依存性注入)とは?仕組みと注入方法を動くコードでわかりやすく解説で扱っています。

名前が似ている依存性逆転の原則(DIP)は、上位モジュールも下位モジュールも抽象(インタフェース)に依存させるという設計原則で、IoCともDIとも別物です。DIはDIPを満たす構成を組み立てやすくしますが、DIを使ってもDIPを満たすとは限りません。両者の切り分けは依存性逆転の原則(DIP)とは?意味・違反コードの直し方とDI(依存性の注入)との違いで整理しています。

Service LocatorとDIの違い:業務コードのコンテナ依存

Fowlerの2004年の記事は、DIの対抗馬としてService Locatorを並べています。Service Locatorは、クラスが必要な部品を自分でレジストリに問い合わせて取り出す方式です。Springの公式リファレンスの導入節も、IoCコンテナの注入は「Beanが直接の生成やService Locatorパターンで依存を自分で探しにいくことの反対(それゆえInversion of Controlという名前)」だと説明しています。

Springでこの違いが表に出るのがApplicationContext.getBean()です。業務クラスの中でcontext.getBean(OrderRepository.class)と書くと、コンテナを使っていても中身はService Locatorになり、そのクラスはSpringのAPIに依存し、テストではApplicationContextごとスタブを用意しないと動かせなくなります。getBeanを呼んでよいのは、mainメソッドやテストの起点など、コンテナの外から最初の1個を取り出す場所だけにします。なお、Fowler自身は記事の結論で、どちらを選ぶかより「サービスの設定と利用を分離する」原則の方が重要だと書いています。

Spring IoCコンテナの実体:BeanFactoryとApplicationContext

Springで「IoCコンテナ」と呼ばれる本体は、org.springframework.beans.factory.BeanFactoryと、それを継承したorg.springframework.context.ApplicationContextです。BeanFactoryがBeanの生成と組み立ての基盤を持ち、ApplicationContextはそこにAOPとの統合、国際化メッセージ、イベント配信、Web用のコンテキストを足しています。

BeanFactoryとApplicationContextの機能差

Spring公式リファレンスの「BeanFactory or ApplicationContext?」は、両者の機能差を次の表で示し、「特別な理由がない限りApplicationContextを使う」としています。素のBeanFactoryを選ぶ理由として挙げられているのは、Beanの処理を完全に自分で制御したい場面です。

機能 BeanFactory ApplicationContext
Beanの生成と依存の組み立て あり あり
ライフサイクル管理の統合 なし あり
BeanPostProcessorの自動登録 なし あり
BeanFactoryPostProcessorの自動登録 なし あり
国際化メッセージ(MessageSource) なし あり
イベント配信(ApplicationEvent) なし あり

実装クラスは、アノテーション設定ならAnnotationConfigApplicationContext、素のBeanFactoryならDefaultListableBeanFactoryです。Spring BootではSpringApplication.run()がApplicationContextを組み立てて返します。

素のBeanFactory:@Autowiredと@PostConstructの処理条件

表の「BeanPostProcessorの自動登録:なし」は、実務では「アノテーションが黙って無視される」形で現れます。@Autowiredを処理するAutowiredAnnotationBeanPostProcessorも、@PostConstructを処理する仕組みもBeanPostProcessorとして実装されているためです。次の章で使うLifeクラス(セッターに@Autowired、メソッドに@PostConstruct、さらにInitializingBeanを実装)を、両方のコンテナに登録して比べました。

実行結果(spring-context 7.0.10) DefaultListableBeanFactory AnnotationConfigApplicationContext
singletonの生成タイミング getBean()を呼んだとき 起動(refresh)時
@Autowiredのセッター 呼ばれない 呼ばれる
@PostConstruct・@PreDestroy 呼ばれない 呼ばれる
afterPropertiesSet()・destroy() 呼ばれる 呼ばれる

素のDefaultListableBeanFactoryで出力されたのは「1 コンストラクタ」「4 afterPropertiesSet」「7 destroy」の3行だけでした。例外も警告も出ないまま注入が抜け落ちるので、設定ミスと見分けがつきません。公式リファレンスも、この状況は「Beanの設定に何も問題がないので混乱しやすい」と注意しています。素のBeanFactoryを使うならaddBeanPostProcessor()で必要なプロセッサを自分で登録する必要があり、その手間を負う理由がない限りApplicationContextを選びます。

Beanのスコープ:singletonなど6種と既定値

Beanは、IoCコンテナが生成し管理するオブジェクトです。クラスとしては普通のJavaオブジェクトと同じで、「コンテナの管理下にある」ことだけが違います。スコープは、そのBeanをどの範囲で使い回すかを決める設定で、公式リファレンスによればSpringが標準で用意するのは6種、うち4種はWeb対応のApplicationContextでしか使えません。

6種のスコープと使える環境

スコープ インスタンスの単位 使える環境
singleton(既定) コンテナ内のBean定義ごとに1個 すべて
prototype 要求のたびに新規 すべて
request HTTPリクエストごと Web
session HTTPセッションごと Web
application ServletContextごと Web
websocket WebSocketセッションごと Web

古い解説に出てくるglobalSessionスコープはPortlet向けで、Portletサポートとともに削除されており、Spring Framework 7.0には存在しません。singletonは「コンテナ内のBean定義ごとに1個」です。同じクラスでも@Beanメソッドを2つ書けば同じコンテナに2個でき、同じ定義を持つApplicationContextを2つ起動すれば別々に2個できます。

singletonへのprototype注入:インスタンスの固定と都度取得

スコープの設定では、寿命の長いBeanが寿命の短いBeanを直接保持する組み合わせに注意が必要です。singletonのHolderがprototypeのProtoをコンストラクタで受け取る構成を実行しました。

@Bean @Scope("prototype")
public Proto proto() { return new Proto(); }

@Bean
public Holder holder(Proto proto, ObjectProvider<Proto> provider) {
    return new Holder(proto, provider);
}
prototype同士: false
Holderに注入されたProtoは固定: true
ObjectProvider経由の2回は同じインスタンスか: false

getBean(Proto.class)を2回呼ぶと別々のインスタンスが返る(false)一方、Holderのフィールドに入ったProtoは、Holderが生成された1回きりで固定されます(true)。注入はBeanの生成時に1度しか起きないためで、Springはこれを警告しません。毎回新しいインスタンスが必要なら、ProtoそのものではなくObjectProvider<Proto>を受け取り、使う時点でgetObject()を呼びます(実行結果の3行目は、2回のgetObject()が別インスタンスを返したことを示しています)。

.NETのDIのガイドラインも、singletonがscopedやtransientの依存を取り込んで事実上singletonにしてしまう問題を「Scope leakage」として挙げています。.NETで検出の仕組みがあるのはScopedのほうだけで、サービス有効期間の解説によれば、開発環境では寿命の長いサービスからScopedを解決すると既定で例外になります(Scope validation)。Transientの取り込みはSpringのprototypeと同じく検出されません。Springではprototypeの直接注入に警告が出ないので、レビューで型とスコープの組み合わせを確認し、上の実行例のようにインスタンスの同一性をテストで確かめます。

Beanのライフサイクル:初期化と破棄のコールバックの実行順

コンテナはBeanを生成したあと、依存を注入し、初期化コールバックを呼んでから利用可能にします。初期化の書き方は3通りあり、同じBeanに全部書いたときの順序は公式リファレンスで決まっています。3つすべてを実装したクラスで確かめました。下のコードは検証用クラスの抜粋で、依存にはspring-context 7.0.10とjakarta.annotation-api 3.0.0を使い、new AnnotationConfigApplicationContext(Config.class)で起動してclose()で終了しています。

public static class Life implements InitializingBean, DisposableBean {
    public Life() { System.out.println("1 コンストラクタ"); }
    @Autowired
    public void setDep(Dep d) { System.out.println("2 依存の注入(setter)"); }
    @PostConstruct public void post() { System.out.println("3 @PostConstruct"); }
    @Override public void afterPropertiesSet() { System.out.println("4 afterPropertiesSet"); }
    public void initMethod() { System.out.println("5 initMethod"); }
    @PreDestroy public void pre() { System.out.println("6 @PreDestroy"); }
    @Override public void destroy() { System.out.println("7 destroy"); }
    public void destroyMethod() { System.out.println("8 destroyMethod"); }
}

// 設定クラス側
@Bean(initMethod = "initMethod", destroyMethod = "destroyMethod")
public Life life() { return new Life(); }

AnnotationConfigApplicationContextで起動し、close()するまでの出力です。

1 コンストラクタ
2 依存の注入(setter)
3 @PostConstruct
4 afterPropertiesSet
5 initMethod
-- refresh完了
(中略)
6 @PreDestroy
7 destroy
8 destroyMethod
-- close完了

初期化は@PostConstruct→afterPropertiesSet()→init-method、破棄も@PreDestroy→destroy()→destroy-methodと同じ並びです。1〜5がすべて「refresh完了」より前に出ているのは、ApplicationContextが遅延初期化を指定していないsingletonを起動時に生成するためで、ここで例外が出ればアプリケーションは起動に失敗します。@Lazyを付けたBeanは、最初に要求されたときに生成されます。

基本はSpringへの依存を増やさない@PostConstruct・@PreDestroy(jakarta.annotationパッケージ)です。InitializingBeanはクラスをSpringに縛るのでフレームワーク側の部品以外では避け、init-method・destroy-methodはソースを変えられない外部クラスを@Beanで登録するときに使います。

prototypeスコープには注意が1つあります。公式リファレンスのとおり、コンテナはprototypeを生成して渡したあと追跡しないので、初期化コールバックは呼ばれても破棄コールバックは呼ばれません。ファイルや接続を握るオブジェクトをprototypeにするなら、解放は呼び出し側の責任です。

Spring Bootでの@Beanとコンポーネントスキャン

Spring Boot(2026年10月28日時点の最新GAは4.1.2、Spring Frameworkは7.0.10)では、IoCコンテナを組み立てるコードはほとんど書きません。@SpringBootApplicationは@SpringBootConfiguration・@EnableAutoConfiguration・@ComponentScanをまとめたもので、このクラスのパッケージ以下がスキャンされ、@Component・@Service・@Repository・@Controllerを付けたクラスがBeanとして登録されます。データソースやJSON変換器は、依存ライブラリに応じて自動構成が登録します。自分でBeanを登録する方法は2つです。

  • 自分で書いたクラス:クラスに@Serviceなどを付けてスキャンさせる。Bean名は原則として先頭を小文字にしたクラス名になる。
  • ソースを変えられないクラスや、生成に引数や分岐が要るクラス:@Configurationクラスのメソッドに@Beanを付け、戻り値をBeanにする。Bean名はメソッド名になる。
@Configuration
public class AppConfig {
    @Bean
    public PaymentClient paymentClient() {
        return new PaymentClient("https://api.example.com");
    }
}

Spring Bootのアノテーションの全体像はSpring Bootのアノテーション一覧|仕組みと使い分け・Boot 4での変更点、プロジェクトの作り方から動かすまではSpring Bootとは|4.1系の依存名の変更と最小Web APIの作り方にまとめています。

Spring以外のIoCコンテナ:Laravel・.NET・NestJSとの寿命の比較

IoCコンテナはJava固有の仕組みではありません。主要なWebフレームワークの寿命の種類を並べると、考え方はほぼ共通です。

フレームワーク コンテナの呼び名 寿命の種類 既定 確認した版
Spring(Java) IoCコンテナ singleton・prototype・Web用4種 singleton 7.0.10
Laravel(PHP) サービスコンテナ bind・singleton・scoped 解決のたびに生成 13.39.0
.NET DIコンテナ Transient・Scoped・Singleton 登録時に必ず指定 10.0.14
NestJS(TypeScript) DIシステム DEFAULT・REQUEST・TRANSIENT DEFAULT(singleton) 12.3.2

違いが出るのは既定値です。SpringとNestJSは共有(singleton)が既定なので、利用者ごとに分離すべき可変状態をインスタンスフィールドに保存すると、その状態が利用者間で共有されます。Laravelのサービスコンテナは、インタフェースに依存しないクラスなら登録なしで自動解決し(Zero Configuration Resolution)、使い回したいときにsingletonや、リクエスト・ジョブ単位のscopedで明示します。LaravelのFacadeがコンテナとどう結び付いているかはFacadeパターンとは?窓口クラスの実装とLaravel Facadeとの違いを解説で扱っています。

IoCコンテナを使わない方がよい場面

IoCコンテナが効くのは、依存するクラスが多く、実装の差し替えやテストでのモック化を繰り返すアプリケーションです。次の2つの場面では持ち込みません。

  • 数百行で終わるバッチやCLIツール:mainでnewを並べる方が短く、追いやすい。
  • 他のプロジェクトに配るライブラリ:特定のコンテナを前提にすると利用側と衝突する。依存をコンストラクタで受け取る形にしておけば、どのコンテナからでもコンテナなしでも使える。

チームにコンテナの仕組みを知らないメンバーが多い場合は、導入はしても使う機能を絞ります。前の章の「素のBeanFactoryでアノテーションが無視される」「prototypeが固定される」は、どちらも例外が出ないまま動作が変わるためです。コンストラクタ注入と、状態を持たないsingletonだけに絞る規約から始めます。

既存のJavaシステムにSpringを入れるかは、依存の数と差し替えの頻度で決めます。作り直しを含めて検討するなら、弊社のフルスクラッチ開発でも設計から相談を受けています。

よくある質問

IoCコンテナとDIコンテナは違うものですか?

いまは同じものを指します。IoCが広い原則、DIがその実現手段で、SpringもLaravelもDIでIoCを実現しています。DIという呼び名は、Martin Fowlerが2004年に「IoCは汎用すぎる」として付けたものです。

IoCとは一言でいうと何ですか?

「フレームワークがこちらのコードを呼ぶ」ことです。オブジェクトの組み立てでは、どの実装をいつ生成して誰に渡すかを、自分のクラスではなくコンテナが決めることを指します。

Beanとは何ですか?

Springのコンテナが生成し管理するオブジェクトです。@Component系のアノテーションや@Beanメソッドで登録されたものがBeanになり、既定ではコンテナ内のBean定義ごとに1個のインスタンスが共有されます。

@Beanと@Componentはどう使い分けますか?

自分で書いたクラスは@Component系を付けてスキャンさせ、外部ライブラリのクラスや生成に分岐が要るものは@Configurationクラスの@Beanメソッドで登録します。

ApplicationContext.getBean()を業務コードで使ってもよいですか?

避けます。中身がService Locatorになり、クラスがSpringのAPIに依存します。依存はコンストラクタで受け取り、getBean()はmainやテストの起点だけで使います。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

ほか 2 件の記事からもリンクされています。

資料請求

RELATED POSTS 関連記事

目次