📦

Spring DIコンテナの中身に迫る

に公開

はじめに

Springでは、@Service@Componentなどのアノテーションを付けるだけで、依存関係が自動的に注入されます。この機能はとても便利ですが、ふとこんなことを思いました。

「これ、どうやって動いてるんだっけ?」

私は以前、Spring以外の環境でDIを実装した経験がありました。そのときは、アプリケーションの起動時に「構成用のコード」を書いて、依存オブジェクトを手動で組み立てていました。

// 手動でDIする場合のイメージ
public class Main {
    public static void main(String[] args) {
        // 依存関係を手動で組み立てる
        UserRepository repository = new UserRepository();
        UserService service = new UserService(repository);
        UserController controller = new UserController(service);

        // アプリケーション起動...
    }
}

しかしSpringでは、こういった「組み立てコード」を書く必要がありません。アノテーションを付けるだけで、あとはフレームワークがよしなにやってくれます。

@Component
@RequiredArgsConstructor  // これだけで依存が注入される
public class UserController {
    private final UserService userService;
}

私たちは普段意識していませんが、このとき裏側で 「DIコンテナ」 と呼ばれる仕組みが働き、面倒なインスタンス生成や紐付けを一手に引き受けてくれています。

では、このDIコンテナは具体的にどう動いているのでしょうか?裏側の部分で何が起きているのか?それを理解するために、シンプルなDIコンテナを自作してみることにしました。

この記事では、DIコンテナの核心部分を実装しながら、Springがどのように依存関係を解決しているのかを解説します。

関連知識

DIコンテナの実装に入る前に、関連する用語を整理しておきます。

Bean

Beanとは、DIコンテナによって管理されるオブジェクトのことです。

Springでは、@Component@Service@Repositoryなどのアノテーションを付けたクラスがBeanとしてコンテナに登録されます。登録されたBeanは、コンテナが自動的にインスタンス化し、必要な場所に注入してくれます。

DI(依存性注入)

DIに関しては様々な書籍や技術記事で詳しい説明が書かれているため、ここでは簡単な説明にとどめておきたいと思います。

DI(Dependency Injection:依存性注入)とは、オブジェクトが必要とする依存オブジェクトを外部から注入するパターンです。

通常、あるクラスが別のクラスを利用する場合、自分自身で依存オブジェクトを生成します。しかし、この方法では依存先の生成方法を知っている必要があり、クラス同士が密結合になってしまいます。

DIでは、依存オブジェクトを自分で生成せず、外部から受け取ります。こうすることで、クラスは依存先の生成方法を知る必要がなくなり、疎結合になります。

DIの実現方法にはいくつかありますが、代表的なものがコンストラクタインジェクションです。コンストラクタの引数で依存オブジェクトを受け取る方式で、Springでも推奨されています。

例えば、UserControllerUserServiceに依存している場合を考えてみましょう。

// DIなし:自分で依存を生成(密結合)
public class UserController {
    private final UserService userService;

    public UserController() {
        this.userService = new UserService(new UserRepository());
    }
}

// DI(コンストラクタインジェクション):外部から依存を受け取る(疎結合)
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }
}

DIを使うことで、UserControllerUserServiceの生成方法を知る必要がなくなります。テスト時にモックを渡すことも容易になります。

では、誰が依存オブジェクトを生成して注入するのか?それがDIコンテナです。

シンプルなDIコンテナを実装する

「DIコンテナ」は、具体的にどのような処理をしているのでしょうか?

それを理解するために、シンプルなDIコンテナを自作してみます。

例として以下のような3層アーキテクチャの依存関係を自動解決するコンテナを作ります。

UserController → UserService → UserRepository

SimpleContainerクラスの実装

public class SimpleContainer {

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

    public <T> T getBean(Class<T> type) {
        // 既にBean(インスタンス)が存在すればそれを返す(シングルトン)
        if (singletons.containsKey(type)) {
            return type.cast(singletons.get(type));
        }

        try {
            // コンストラクタを取得し、最も引数の多いものを選択
            Constructor<?>[] constructors = type.getConstructors();
            Constructor<?> target = constructors[0];
            for (Constructor<?> c : constructors) {
                if (c.getParameterCount() > target.getParameterCount()) {
                    target = c;
                }
            }

            // コンストラクタの引数を再帰的に解決
            Class<?>[] paramTypes = target.getParameterTypes();
            Object[] params = new Object[paramTypes.length];

            for (int i = 0; i < paramTypes.length; i++) {
                params[i] = getBean(paramTypes[i]);  // 再帰呼び出し!
            }

            // Bean(インスタンス)を生成してシングルトンとして登録
            T instance = type.cast(target.newInstance(params));
            singletons.put(type, instance);
            return instance;

        } catch (Exception e) {
            throw new RuntimeException("Bean creation failed for: " + type.getName(), e);
        }
    }
}

たったこれだけのコードで、DIコンテナの核心部分が実装できます。

コードの解説

1. シングルトンマップ

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

生成したBean(DIコンテナが管理するインスタンス)をClassオブジェクトをキーにして保持します。この実装では、UserRepositoryUserServiceUserControllerの各インスタンスがBeanとしてこのマップに登録されます。

Classオブジェクトとは、クラスの情報(名前、コンストラクタ、メソッドなど)を持つオブジェクトです。UserService.classのように.classを付けることで取得できます。パッケージ名を含む完全修飾名で識別されるため、別パッケージに同名クラスがあってもコンフリクトしません。

このマップにより:

  • 同じクラスを何度getBeanしても同じインスタンスが返る
  • Springのデフォルトスコープである「シングルトン」を実現

2. コンストラクタの選択

Constructor<?> target = constructors[0];
for (Constructor<?> c : constructors) {
    if (c.getParameterCount() > target.getParameterCount()) {
        target = c;
    }
}

この例では 最も引数の多いコンストラクタを選択 しています。

DIでは「依存関係はコンストラクタで受け取る」のが基本です。引数が多いということは、それだけ多くの依存関係を明示的に受け取っているということです。

public class UserService {
    // 引数0個:依存なし(デフォルトコンストラクタ)
    public UserService() { }

    // 引数1個:UserRepositoryに依存
    public UserService(UserRepository repo) { }
}

Springも同様のロジックを持っていますが、@Autowiredアノテーションが付いたコンストラクタを優先するなど、より洗練されています。

3. 依存関係の再帰的解決

for (int i = 0; i < paramTypes.length; i++) {
    params[i] = getBean(paramTypes[i]);  // 再帰呼び出し
}

ここが最も重要なポイントです。

コンストラクタの引数の型に対してgetBeanを再帰的に呼び出すことで、依存の依存の依存...と連鎖的に解決していきます。

依存解決の流れを追う

getBean(UserController.class)を呼び出すと、以下のように処理が進みます:

getBean(UserController.class)

├─ UserControllerのコンストラクタを調査
│  → public UserController(UserService userService)
│  → UserServiceが必要だと判明

├─ getBean(UserService.class)  ← 再帰呼び出し
│  │
│  ├─ UserServiceのコンストラクタを調査
│  │  → public UserService(UserRepository userRepository)
│  │  → UserRepositoryが必要だと判明
│  │
│  ├─ getBean(UserRepository.class)  ← 再帰呼び出し
│  │  │
│  │  ├─ UserRepositoryのコンストラクタを調査
│  │  │  → public UserRepository()  ← 引数なし!
│  │  │
│  │  └─ UserRepositoryをインスタンス化して返す
│  │
│  └─ UserService(userRepository) をインスタンス化して返す

└─ UserController(userService) をインスタンス化して返す

引数なしのコンストラクタに到達したら、そこから巻き戻しながらインスタンス化していきます。

使用例

public class Main {
    public static void main(String[] args) {
        SimpleContainer container = new SimpleContainer();

        // これだけで全ての依存関係が解決される!
        UserController controller = container.getBean(UserController.class);

        // シングルトンの確認
        UserController controller2 = container.getBean(UserController.class);
        System.out.println(controller == controller2);  // true
    }
}

実際のSpringとの比較

今回のSimpleContainerでは、コンストラクタインジェクションによる依存解決とシングルトンでのBean管理を実装しました。これはDIコンテナの最も核心的な部分です。

実際のSpringのDIコンテナ(ApplicationContext)は、これに加えて多くの機能を提供しています。

依存注入の方式

  • フィールドインジェクション:フィールドに直接注入
  • セッターインジェクション:セッターメソッド経由で注入

Beanの管理

  • プロトタイプスコープ:毎回新しいインスタンスを生成
  • リクエストスコープ:HTTPリクエストごとにインスタンスを生成
  • ライフサイクル管理:@PostConstruct@PreDestroyによる初期化・破棄処理

その他の機能

  • コンポーネントスキャン:@Componentが付いたクラスを自動検出
  • 循環参照の検出:依存関係のループを検知してエラー
  • AOP:アスペクト指向プログラミングによるプロキシ生成
  • @Primary/@Qualifier:同一インターフェースの複数実装を解決

このようにSpringは非常に多機能ですが、コアとなる依存解決の仕組みは今回実装したものと同じ原理です。リフレクションでコンストラクタを調べ、再帰的に依存を解決してインスタンスを生成する——この基本的な流れはSpringでも変わりません。

まとめ

この記事では、DIコンテナを自作することでSpringのDIの仕組みを理解しました。

重要なポイント:

  1. DIコンテナはリフレクションを使う

    • コンストラクタの引数の型を実行時に調査
    • type.getConstructors()でコンストラクタ一覧を取得
  2. 依存関係は再帰的に解決される

    • getBeanが自分自身を呼び出すことで、依存の連鎖を解決
    • 引数なしコンストラクタに到達したら巻き戻しながらインスタンス化
  3. シングルトンはマップで管理

    • 一度生成したインスタンスをキャッシュして再利用
    • Map<Class<?>, Object>でクラスとインスタンスを紐付け

普段アノテーションを付けるだけで動いているSpringのDIですが、その裏側ではこのような処理が行われています。仕組みを理解することで、DIに関するエラーやトラブルシューティングがしやすくなるはずです。

Discussion