Spring

SpringのDI(依存性注入)とは?仕組みと注入方法を動くコードでわかりやすく解説

SpringのDI(依存性注入)とは?仕組みと注入方法を動くコードでわかりやすく解説

SpringのDI(依存性注入)は、クラスが使う部品(依存オブジェクト)を自分でnewせず、Springから受け取る仕組みです。言葉で読むと抽象的なので、この記事ではDIなし・ありのコードを並べ、Springのコンテナを実際に起動したときの出力を見ながら説明します。掲載コードは Spring Framework 7.0.9(Spring Boot 4.1.1 が採用している版)と JDK 25 で実行し、出力とエラー文はその結果をそのまま載せています。

まとめ:SpringのDIで押さえる5つの要点

  • DIとは:依存する相手を自分でnewせず、外から受け取る設計。Springでは DIコンテナ(ApplicationContext)が「Bean」を生成し、必要な場所へ渡す。
  • 注入方法はコンストラクタ注入が基本:Springの公式リファレンスがコンストラクタ注入を推奨し、セッター注入は主に妥当な既定値を持つ任意の依存向けとしている。本記事では、新規コードにフィールド注入を採用しない方針とする。
  • 1つだけ受け取る注入先に同じ型のBeanが2つあると起動に失敗する:NoUniqueBeanDefinitionExceptionになる。@Primary・@Qualifier・@Fallback(6.2以降)で選ぶ。
  • Beanは既定でシングルトン:同じコンテナ内の同じBean定義では1つのインスタンスを共有するので、利用者ごとの状態をフィールドに持たせない。
  • 循環参照はSpring Bootでは既定で起動エラー:Spring Boot 2.6 以降の挙動。設定で許可せず、依存の向きを直す。

SpringのDI(依存性注入)の意味:必要な部品を外から受け取る設計

DI(Dependency Injection=依存性注入)の「依存」は「AがBを使っている(AはBがないと動かない)」関係を、「注入」はそのBを外から差し込むことを指します。料理に例えると、シェフ(クラス)が食材(依存オブジェクト)を自分で畑に採りに行くのがDIなし、仕入れ担当(Springのコンテナ)が食材を届けてくれるのがDIありです。シェフは「何を使うか」だけを宣言し、「どこからどう調達するか」は気にしません。

DIなしのコードとDIありのコードの違い

DIを使わない場合、UserServiceは依存先のUserRepositoryを自分で生成します。

public class UserService {
    private final UserRepository userRepository = new JdbcUserRepository();

    public void register(String name) {
        userRepository.save(name);
    }
}

この書き方ではUserServiceがJdbcUserRepositoryという具体的な実装に固定されます。テストでDBを使わない偽物に差し替えたくても、内部で生成しているので外から置き換えられません。

DIを使う場合は、依存先をコンストラクタの引数として受け取ります。

public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void register(String name) {
        userRepository.save(name);
    }
}

変わったのは「作る」から「受け取る」への一点だけです。UserServiceはインターフェースUserRepositoryにしか依存しなくなり、本番ではDB実装を、テストでは偽物を渡せます。この「渡す」役をSpringが自動で担うのが、SpringのDIです。

DIとIoC(制御の反転)・DIコンテナの関係

DIはIoC(Inversion of Control=制御の反転)とセットで語られます。オブジェクトの生成と組み立てを自分のコードからフレームワークへ移すのがIoCで、DIはその代表的な実現方法です。DIという呼び名は、Martin Fowler が2004年1月に公開したInversion of Control Containers and the Dependency Injection patternで広まりました。同記事は「Separating Configuration from Use」という節を設け、サービスの設定(configuration)を利用(use)から分離することが要点だと述べています。どの実装を使うかという「設定」を、それを使うクラスの外に出す。DIの価値はこの一点に集約されます。

IoCコンテナの内部(BeanFactoryとApplicationContextの違い、Beanのスコープとライフサイクル)はIoCコンテナとは?制御の反転の仕組みとSpring IoCコンテナ・Beanをわかりやすく解説で掘り下げています。DIと名前の似た設計原則DIP(依存性逆転の原則)との違いは依存性の注入(DI)と依存性逆転の原則(DIP)の違い|コード例で使い分けを参照してください。

DIコンテナを動かして仕組みを確かめる最小構成

SpringのDIは Spring Boot がなくても動きます。spring-context(Spring Boot のスターターにも含まれるコアの部品)だけで、コンテナの動きを確認できます。Spring Boot で新しくプロジェクトを作る手順はSpring Bootとは|4.1系の依存名の変更と最小Web APIの作り方にまとめています。

@Configuration・@BeanによるBean定義とコンテナ起動

Mavenならpom.xmlのdependenciesに次の1つを足すだけで準備は終わりです。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>7.0.9</version>
</dependency>

次のコードは、@Configurationを付けた設定クラスに「どの実装を使うか」を書き、AnnotationConfigApplicationContextでコンテナを起動します。UserService自身はInMemoryUserRepositoryの存在を知りません。

package demo;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

interface UserRepository {
    void save(String name);
}

class InMemoryUserRepository implements UserRepository {
    InMemoryUserRepository() {
        System.out.println("1. InMemoryUserRepository を生成");
    }
    public void save(String name) {
        System.out.println("保存: " + name);
    }
}

class UserService {
    private final UserRepository userRepository;

    UserService(UserRepository userRepository) {
        System.out.println("2. UserService を生成(依存を受け取る)");
        this.userRepository = userRepository;
    }
    void register(String name) {
        userRepository.save(name);
    }
}

@Configuration
class AppConfig {
    @Bean
    UserRepository userRepository() {
        return new InMemoryUserRepository();
    }

    @Bean
    UserService userService(UserRepository userRepository) {
        return new UserService(userRepository);
    }
}

public class App {
    public static void main(String[] args) {
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            System.out.println("3. コンテナの起動完了");
            UserService first = context.getBean(UserService.class);
            UserService second = context.getBean(UserService.class);
            first.register("tanaka");
            System.out.println("同じインスタンスか: " + (first == second));
        }
    }
}

src/main/java/demo/App.javaに保存し、mvn compile exec:java -Dexec.mainClass=demo.Appで実行した結果は次のとおりです。

1. InMemoryUserRepository を生成
2. UserService を生成(依存を受け取る)
3. コンテナの起動完了
保存: tanaka
同じインスタンスか: true

DIコンテナ起動時の依存解決・Bean生成・共有

この5行に、DIコンテナの仕事がそのまま表れています。

  • 依存される側から先に生成する:userServiceメソッドの引数がUserRepositoryなので、コンテナは先にInMemoryUserRepositoryを作り(1行目)、それを渡してUserServiceを作ります(2行目)。順番をコードに書いていないのに、引数の型から組み立て順が決まります。
  • 起動の時点で全部作り終える:「3. コンテナの起動完了」より前に2つのBeanが生成されています。ApplicationContextは既定で、シングルトンのBeanを起動時にまとめて生成します。設定の誤りが、最初のリクエストではなく起動時に見つかるのはこのためです。
  • 何度取り出しても同じインスタンス:getBeanを2回呼んでもtrueです。Springの公式リファレンスはBean Scopesで「The singleton scope is the default scope in Spring.」と明記しています。

Beanを登録する4つの方法

上の例は@Beanメソッドで登録しましたが、実務ではクラスに@Component系のアノテーションを付けて自動検出させる方法が中心です。Spring Framework 7.0 では、条件に応じてコードでBeanを登録するBeanRegistrarが加わりました(公式リファレンスProgrammatic Bean Registration)。

登録方法 書き方 向いている対象
コンポーネントスキャン クラスに@Component・@Serviceなど 自作クラスの大半
Java Config @Configuration内の@Beanメソッド 外部ライブラリのクラス
BeanRegistrar BeanRegistrarを実装し@Import 環境で登録内容を変えるとき(7.0以降)
XML設定 XMLファイルのbean定義 既存システムの保守

新規開発では、自作クラスはコンポーネントスキャン、手を入れられない外部クラスは@Beanメソッドで登録すれば足ります。BeanRegistrarはライブラリやフレームワークを作る側の道具で、アプリケーションのコードで最初に覚える必要はありません。

Springの3つの注入方法と使い分け

Beanをどの経路で受け取るかには、コンストラクタ注入・セッター注入・フィールド注入の3つがあります。選び方の結論は公式リファレンスのDependency Injectionの章にあり、迷う余地はほとんどありません。

コンストラクタ注入:必須依存の明示と初期化

公式リファレンスは「The Spring team generally advocates constructor injection」と書き、理由として、部品を不変(immutable)にでき、必須の依存がnullにならず、完全に初期化された状態で渡されることを挙げています。

@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

コンストラクタが1つだけなら@Autowiredは書かなくても注入されます。これは Spring Framework 4.3(2016年)で入った挙動で、当時のSpringブログの告知で紹介されています。コンストラクタが複数あるときは、どれを使うかを@Autowiredで指定します。

引数が5個、6個と増えてきたら、注入方法ではなくクラスの責務を疑ってください。公式リファレンスも、コンストラクタ引数が多いことを「クラスが責務を持ちすぎている」兆候としています。

セッター注入:任意依存を中心とした使い分け

公式リファレンスは、セッター注入を「クラス内で妥当な既定値を割り当てられる任意の依存」に主に使うべきだとしています。セッターに付けた@Autowiredも既定では必須扱いで、依存が見つからなければBeanの生成に失敗します。任意の依存をnullのまま許す場合は、利用時のチェックが必要です。

@Service
public class ReportService {
    private MailSender mailSender; // 無ければメール送信を省く

    @Autowired(required = false)
    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }
}

フィールド注入を新規コードで避ける3つの理由

フィールドに直接@Autowiredを付ける書き方は短く書けますが、新規コードでは使いません。

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
}
  • finalにできない:生成後に書き込まれるため、不変にできません。
  • 依存をコンストラクタから渡してテストできない:外から依存を渡す口がないので、単体テストでもコンテナかリフレクションが必要になります。
  • 循環参照が素のSpringでは通ってしまう:後述のとおり、素の Spring Framework ではフィールド注入どうしの循環参照が黙って成立します。コンストラクタ注入なら起動時に必ず失敗します。

既存コードがフィールド注入なら、そのクラスを修正するついでにコンストラクタ注入へ移します。

同じ型のBeanが複数あるときの選び方:@Primary・@Qualifier・@Fallback

DIで最初につまずきやすいのが「同じインターフェースの実装が2つある」場面です。次のように通知手段を2つ登録し、OrderServiceにNotifierを1つ渡そうとすると、コンテナはどちらを渡すか決められません。

interface Notifier {
    String channel();
}

record OrderService(Notifier notifier) {
}

@Configuration
class NotifierConfig {
    @Bean
    Notifier mailNotifier() {
        return () -> "mail";
    }

    @Bean
    Notifier slackNotifier() {
        return () -> "slack";
    }

    @Bean
    OrderService orderService(Notifier notifier) {
        return new OrderService(notifier);
    }
}

起動はUnsatisfiedDependencyExceptionで失敗し、その原因として次のNoUniqueBeanDefinitionExceptionが記録されます。

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'demo.Notifier' available: expected single matching bean but found 2: mailNotifier,slackNotifier

メッセージ末尾の「found 2: mailNotifier,slackNotifier」が候補のBean名です。解決方法は3つあり、同じ構成で付け替えて実行した結果は次のとおりでした。

付けたもの 付ける場所 渡されたBean
なし – 起動失敗(上の例外)
@Primary mailNotifier の定義 mail
@Primary+@Qualifier("slackNotifier") Primaryは定義、Qualifierは受け取る引数 slack
@Fallback slackNotifier の定義 mail

使い分けは次のとおりです。

  • @Primary:「普段はこれ」と決まっている既定の実装に付けます。
  • @Qualifier:受け取る側が個別に指定します。上の3行目では、@Qualifierが候補をslackNotifierに絞るため、mailNotifierの@Primaryは選択に影響しません。
  • @Fallback:Spring Framework 6.2 で追加された、@Primaryの逆です。「ほかに候補が無いときだけ使う」実装に付け、残りが1つならそれが選ばれます(公式リファレンスFine-tuning Annotation-based Autowiring with @Primary or @Fallback)。

どれを選ぶかの判断基準は「誰が知っているべきか」です。既定の実装を設定側で決めたいなら@Primary、特定のクラスだけ別の実装を使うなら@Qualifierにします。全実装を受け取りたいときはList<Notifier>で受け取れば、登録された全Beanが渡されます。

DIで単体テストが書きやすくなる理由

DIのメリットとして「テストしやすい」とよく書かれますが、具体的にはSpringを起動せずに、テスト用の偽物をnewで渡せることです。ここでは、名前が空白なら保存せずに例外を投げるUserServiceをテストします。

public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void register(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("name is blank");
        }
        userRepository.save(name);
    }
}

コンストラクタ注入で書いてあるので、JUnit だけで次のように検証できます。

package demo;

import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {

    // テスト用の偽物。DBにはつながない
    static class FakeUserRepository implements UserRepository {
        final List<String> saved = new ArrayList<>();
        public void save(String name) { saved.add(name); }
    }

    @Test
    void registerSavesUser() {
        var repository = new FakeUserRepository();
        var service = new UserService(repository); // Springを起動せず new で渡す

        service.register("tanaka");

        assertEquals(List.of("tanaka"), repository.saved);
    }

    @Test
    void blankNameIsRejected() {
        var repository = new FakeUserRepository();
        var service = new UserService(repository);

        assertThrows(IllegalArgumentException.class, () -> service.register(" "));
        assertTrue(repository.saved.isEmpty());
    }
}

テストはsrc/test/java/demoに置きます。Spring Boot 4.1.1 のテスト用スターター(JUnit 6.0.3 を含む)を依存に入れてmvn testで実行すると、2件とも成功し、所要時間は2件合わせて0.1秒でした。コンテナを起動しないので速く終わります。

偽物のクラスを書く代わりに、Spring Boot のテスト用スターターに含まれる Mockito でmock(UserRepository.class)を渡す方法もあります。偽物とモックの使い分けはモックとは?スタブとの違い・テストダブル5分類と5言語の書き方を実行例で解説、JUnitの基本はJUnitとは?Javaの単体テストフレームワークの基本と書き方【2026年版】を参照してください。

SpringのDIでつまずきやすい2つの落とし穴

シングルトンBeanにおける利用者別の状態共有リスク

Beanは既定でシングルトンなので、フィールドに値を持たせると全利用者で共有されます。

@Component
class VisitCounter {
    private int count = 0; // シングルトンなので全利用者で共有される

    int visit() {
        return ++count;
    }
}

利用者Aと利用者Bがそれぞれ1回ずつvisit()を呼ぶと、出力は次のようになります。

利用者Aの1回目: 1
利用者Bの1回目: 2

Bにとっては1回目なのに2が返ります。Webアプリでは複数のリクエストが同じBeanを同時に使うため、ログインユーザー名や処理中の値をフィールドに入れると、別の利用者の値が混ざります。公式リファレンスも「状態を持つBeanにはprototypeスコープ、状態を持たないBeanにはsingletonスコープを使う」としています。サービスクラスは状態を持たない作りにし、値はメソッドの引数と戻り値で受け渡します。サービス層の書き方はサービスクラスとは?Java・Springでの役割と書き方、コントローラーとの違いで解説しています。

循環参照はSpring Bootでは既定で起動エラー

AがBを、BがAを必要とする循環参照は、注入方法と、Spring Boot を使うかどうかで結果が変わります。同じ2クラスで試した結果は次のとおりです。

構成 素のSpring Framework 7.0.9 Spring Boot 4.1.1
コンストラクタ注入どうし 起動失敗 起動失敗
フィールド注入どうし 起動成功(循環が残る) 起動失敗

コンストラクタ注入では、AとBのどちらも相手より先に作れないため、どの構成でもBeanCurrentlyInCreationExceptionになります。フィールド注入では素の Spring Framework がいったん作りかけのBeanを渡して循環を成立させてしまいますが、Spring Boot は2.6から循環参照を既定で禁止しました(Spring Boot 2.6 Release Notes)。Spring Boot 4.1.1 では次の表示で起動が止まります。

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  bootCircular.X (field demo.BootCircular$Y demo.BootCircular$X.y)
↑     ↓
|  bootCircular.Y (field demo.BootCircular$X demo.BootCircular$Y.x)
└─────┘

Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

メッセージにあるとおり、spring.main.allow-circular-references=trueは「最後の手段」です。設定で黙らせず、両方が使っている処理を第3のクラスへ切り出して、依存の向きを一方向にそろえます。

DIで使う主要アノテーションの早見表

DIまわりで最初に覚えるアノテーションは次の範囲です。

アノテーション 付ける場所 役割
@Component クラス 自動検出の対象にする汎用の印
@Service クラス 業務ロジック層のBean
@Repository クラス データアクセス層。設定により例外変換の対象
@Controller クラス Web層のBean
@Configuration・@Bean クラス・メソッド メソッドの戻り値をBeanにする
@Autowired コンストラクタなど 注入の指示(コンストラクタ1つなら省略可)
@Primary・@Qualifier・@Fallback 定義・引数 同じ型の候補から選ぶ

@Service・@Repository・@Controllerはどれも@Componentの一種で、Beanとして登録される点は同じです。層を示すことで設計の意図が読み手に伝わり、@Repositoryはデータアクセス例外の変換対象にもなります。各アノテーションの細かな挙動はSpringアノテーションとは?仕組みと主要アノテーションの使い分けを解説、Spring Boot 全体の一覧はSpring Bootのアノテーション一覧|役割別の早見表とBoot 4での変更点にあります。

Spring以外のDIとの違い:Jakarta CDI・Guice・Dagger

DIはSpring固有の考え方ではありません。Javaでは、Jakarta EE の標準仕様である Jakarta CDI(Contexts and Dependency Injection)、Google の軽量ライブラリ Guice、コンパイル時に注入コードを生成する Dagger(Androidで広く使われる)があります。Java以外でも、Angular や NestJS はフレームワーク自体にDIを組み込んでいます。

これらと比べたSpringの特徴は、DIコンテナを土台に、Web・データアクセス・セキュリティ・バッチまでを同じBeanの仕組みで揃えている点です。Spring Framework の全体像とSpring Bootとの関係はSpring Frameworkとは?特徴・DI/AOPとSpring Bootとの違い・最新版7を解説【2026年最新】で整理しています。業務システムを Java と Spring で新規に作る場合の進め方は、フルスクラッチ開発の支援内容も参考にしてください。

よくある質問

DIとDIコンテナは何が違いますか?

DIは「依存を外から渡す」という設計の考え方で、DIコンテナはそれを実際に行うSpringの部品(ApplicationContext)です。DIはコンテナがなくても、コンストラクタに手でnewしたものを渡せば成り立ちます。コンテナは、その組み立てを依存関係の数が増えても自動でこなす役割です。

SpringでDIを使うメリットは何ですか?

主なメリットは2つです。実装を差し替えるときに使う側のクラスを修正しなくてよいこと、そしてSpringを起動せずに偽物の依存を渡して単体テストできることです。加えて、既定の非遅延シングルトンBeanは起動時に生成・組み立てされるため、依存関係の設定誤りを起動時に検出できます。遅延初期化や他のスコープでは、生成時まで検出されない場合があります。

@Autowiredは必ず書く必要がありますか?

コンストラクタが1つだけのクラスなら不要です(Spring Framework 4.3以降)。複数のコンストラクタから注入先を明示する場合は@Autowiredを使います。どれにも付けていなくても、引数なしのコンストラクタがあればそれが使われます。セッターやフィールドには、@Autowiredのほか@Injectや@Resourceも利用できます。

@Componentと@Serviceはどう違いますか?

どちらもクラスをBeanとして登録します。@Serviceは@Componentの一種で、業務ロジック層であることを示す意味付けが違いです。層が決まっているクラスには@Service・@Repository・@Controllerを使い、どの層にも当てはまらない部品に@Componentを使います。

Spring BootとSpring FrameworkでDIの書き方は変わりますか?

アノテーションと注入方法は同じです。Spring Boot は@SpringBootApplicationを付けたクラスのパッケージ以下を自動でスキャンし、ライブラリのBeanも自動構成で登録します。挙動の差として、Spring Boot は循環参照を既定で禁止している点に注意してください。

関連記事

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

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

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.16 テックブログ バーガーキング不正アクセスで約1,118万件の情報漏洩|対象確認・案内メールの真偽と運営側のログ点検
  2. 2026.10.16 テックブログ ソースネクスト不正アクセスで最大2,252万件流出|ログインできない理由・パスワード再設定と会員APIの点検
  3. 2026.10.15 テックブログ リゾートトラスト不正アクセス:会員2.6万件・カハラ横浜3.6万件の対象確認と窓口
  4. 2026.10.15 テックブログ 銀のさら不正アクセス:最大650万件の対象確認と窓口、パスワード変更の手順
  5. 2026.10.09 テックブログ 京王電鉄のランサムウェア被害とグループ共通基盤:決済・ポイント・予約が止まった範囲と遮断の初動

RELATED POSTS 関連記事

目次