Spring DI(依存性注入)とは、クラスが使う部品(依存オブジェクト)を自分で new せず、Spring コンテナに外から渡してもらう仕組みです。 渡し方には「フィールド」「セッター」「コンストラクタ」の3つがあり、Spring 公式が推奨しているのは コンストラクタインジェクション です。コンストラクタが1つだけなら @Autowired も省略できます。

この記事では、DI の考え方を「new で書いた場合」と並べて比べたうえで、3つの注入方法の比較表、同じ型の Bean が複数あるときの @Primary・@Qualifier・List 注入、循環参照や Bean が見つからないときの 実際のエラー全文と直し方、モックを使ったテストまでを順に解説します。DI の挙動とエラーは Java 21.0.10 と Spring Framework 4.3.30(Debian の apt パッケージ)で実際に実行し、出力をそのまま載せています。Lombok・Mockito・Spring Boot の部分はこの環境で入手できなかったため未実行で、公式ドキュメントに沿って書いています。最新の Spring Framework 6.x/7.x、Spring Boot 3.x/4.x との違いは、公式ドキュメントで確認したうえで注記しています。

Spring DIの早見表

まず「やりたいこと → 書き方」を一覧にします。迷ったらこの表から該当する節に進んでください。

やりたいこと 書き方 ポイント
クラスを Bean として登録する @Component / @Service / @Repository / @Controller コンポーネントスキャンの対象パッケージ配下に置く
外部ライブラリのクラスを Bean にする @Configuration クラスの @Bean メソッド 自分で @Component を付けられないクラスに使う
依存を受け取る(推奨) final フィールド+コンストラクタ コンストラクタが1つなら @Autowired 省略可
コンストラクタを自動生成する Lombok @RequiredArgsConstructor final フィールドだけが引数になる
任意の依存を後から差し替える セッターに @Autowired 必須でない依存向け
同じ型が複数あり、基本はこれを使う 実装クラスに @Primary 指定がない注入先はすべてこれになる
同じ型が複数あり、ここだけ別のを使う 引数に @Qualifier("beanName") @Primary より優先される
同じ型の Bean を全部使う List<型> / Map<String, 型> で受け取る Map のキーは Bean 名
あってもなくてもよい依存 ObjectProvider<型> / Optional<型> 無くても起動できる
循環参照をひとまず回避する @Lazy / ObjectProvider<型> 根本対応は設計の見直し
テストで依存をモックにする(単体) コンストラクタにモックを渡して new Spring を起動しないので速い
テストで Bean をモックに差し替える @MockitoBean(Spring Framework 6.2 / Boot 3.4 以降) Boot 4.0 で @MockBean は削除
よく出るエラー 意味 主な直し方
NoUniqueBeanDefinitionException 候補の Bean が2つ以上ある @Primary、@Qualifier、List 注入
NoSuchBeanDefinitionException 候補の Bean が1つもない @Component 付け忘れ、スキャン範囲、条件付き登録を確認
BeanCurrentlyInCreationException Bean 同士が互いを必要としている(循環参照) 責務を分ける、イベント化、@Lazy
NullPointerException(注入したはずのフィールド) Spring を通さず new している コンストラクタインジェクションにする

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring DIとは|@Autowiredとコンストラクタインジェクション」

DIとは:newする場合と比べてわかる考え方

DI を一言でいうと 「使う部品を自分で作らず、外から受け取る」 書き方です。違いは、new で書いたコードと並べるといちばんよくわかります。

直す前と後

注文を受け付けたら会員に通知を送る OrderService を例にします。

// 直す前:クラスの中で new している
class OrderServiceWithNew {
    private final NotificationSender sender = new MailSender(); // 自分で作る
}

// 直した後:コンストラクタで受け取る
class OrderService {
    private final NotificationSender sender;
    OrderService(NotificationSender sender) { this.sender = sender; } // 外から受け取る
}
観点 直す前(new) 直した後(DI)
結びつき MailSender という具体クラスに固定 「NotificationSender 役の何か」なら何でもよい
テスト placeOrder() で本物の送信処理まで動く 偽物やモックを渡せる
通知手段の変更 OrderService 自体を書き換える 渡すものを変えるだけ
部品の準備 設定値や接続先の準備まで抱える 準備は外側(Spring コンテナ)の仕事

誰が何を渡すかを決めるのは外側 です。この「外側」の役目を自動でやってくれるのが Spring の DI コンテナ(ApplicationContext)です。

テストのしやすさを実行して比べる

違いがいちばんはっきり出るのはテストです。JUnit 4.13.2 で、2つのクラスを同じようにテストしました(Spring は使っていません)。

import org.junit.Test;
import static org.junit.Assert.*;
import java.util.*;

public class OrderServiceTest {
    interface NotificationSender { void send(String to, String msg); }

    // 直す前:中で new している(本物の送信は接続先が無いので例外を投げる想定)
    static class MailSender implements NotificationSender {
        public void send(String to, String msg) {
            throw new IllegalStateException("SMTPサーバーに接続できません");
        }
    }
    static class OrderServiceWithNew {
        private final NotificationSender sender = new MailSender();
        String placeOrder(String member) { sender.send(member, "注文を受け付けました"); return "ORDERED"; }
    }
    // 直した後:コンストラクタで受け取る
    static class OrderService {
        private final NotificationSender sender;
        OrderService(NotificationSender sender) { this.sender = sender; }
        String placeOrder(String member) { sender.send(member, "注文を受け付けました"); return "ORDERED"; }
    }

    @Test public void newしているとテストでも本物のメール送信が動く() {
        IllegalStateException e = assertThrows(IllegalStateException.class,
            () -> new OrderServiceWithNew().placeOrder("member-001"));
        assertEquals("SMTPサーバーに接続できません", e.getMessage());
    }

    @Test public void コンストラクタに偽物を渡せば送信内容を確認できる() {
        List<String> sent = new ArrayList<>();
        OrderService service = new OrderService((to, msg) -> sent.add(to + ":" + msg));

        assertEquals("ORDERED", service.placeOrder("member-001"));
        assertEquals(List.of("member-001:注文を受け付けました"), sent);
    }
}

実行コマンドと結果(Java 21.0.10、JUnit 4.13.2):

javac -encoding UTF-8 -cp junit-4.13.2.jar:hamcrest-core-1.3.jar -d out OrderServiceTest.java
java -cp junit-4.13.2.jar:hamcrest-core-1.3.jar:out org.junit.runner.JUnitCore OrderServiceTest
JUnit version 4.13.2
..
Time: 0.017

OK (2 tests)

new している版は、テストでも本物の送信処理まで動いてしまいます(1つ目のテストは、その例外が出ることを確認しています)。一方、コンストラクタで受け取る版はラムダ1行の偽物を渡すだけで、何がどこに送られたかを確認できます。DI の価値は、この「差し替えやすさ」にあります。

検証環境と実行方法

この記事のコードは、次の環境で実行しています。

項目 バージョン 備考
Java OpenJDK 21.0.10
Spring Framework 4.3.30(Debian apt の libspring-context-java) DI の基本挙動とエラーの種類を確認するため
JUnit 4.13.2 テストの確認用
Spring Boot 未実行 起動時のエラー表示・@MockitoBean は公式ドキュメントとソースで確認

実行は javac -cp <Spring 4.3 の jar 群> -d out X.java でコンパイルし、java -cp <jar 群>:out X で起動しています(Spring の @Lazy が作るプロキシのクラスローダーの都合で、java X.java の単一ファイル実行ではなくコンパイルしてから実行しました)。

Spring Framework 4.3 は現在の Spring とは世代が離れていますが、DI の解決ルール(型で探す → @Primary → @Qualifier → 見つからない/複数ならエラー)は 6.x/7.x でも同じ です。エラーの「文言の細部」は版で変わるため、該当箇所で注記します。

3つの注入方法を比べる(フィールド・セッター・コンストラクタ)

比較表

観点 フィールドインジェクション セッターインジェクション コンストラクタインジェクション
書き方 フィールドに @Autowired セッターに @Autowired コンストラクタの引数で受け取る
final にできるか できない できない できる(不変になる)
@Autowired の省略 不可 不可 コンストラクタが1つなら省略可(Spring 4.3 以降)
Spring なしで new したとき 依存が null のまま セッターを呼べば設定できる 引数が必須なので渡し忘れがコンパイルで分かる
テストでの差し替え リフレクションや Spring 起動が必要 セッターで渡せる コンストラクタで渡すだけ
依存の多さに気づけるか 気づきにくい 気づきにくい 引数が増えると目に見える(責務分割のサイン)
向いている用途 テストクラス内の注入など限定的 任意(なくてもよい)依存、後から差し替える依存 必須の依存(基本はこれ)

Spring Framework の公式リファレンスも、必須の依存はコンストラクタ、任意の依存はセッター という使い分けを勧めています。

3つの方法を同じ条件で動かす

import org.springframework.context.annotation.*;
import org.springframework.stereotype.*;
import org.springframework.beans.factory.annotation.*;

public class InjectionStyles {
    @Component public static class Clock2 { public String now() { return "2026-10-02T09:00"; } }

    @Service public static class FieldStyle {
        @Autowired private Clock2 clock;                 // フィールド
        public String info() { return "field: " + clock.now(); }
    }
    @Service public static class SetterStyle {
        private Clock2 clock;
        @Autowired public void setClock(Clock2 clock) { this.clock = clock; }   // セッター
        public String info() { return "setter: " + clock.now(); }
    }
    @Service public static class ConstructorStyle {
        private final Clock2 clock;
        public ConstructorStyle(Clock2 clock) { this.clock = clock; }          // コンストラクタ(@Autowired 省略)
        public String info() { return "constructor: " + clock.now(); }
    }
    @Service public static class TwoConstructors {
        private final Clock2 clock;
        public TwoConstructors() { this.clock = null; }
        public TwoConstructors(Clock2 clock) { this.clock = clock; }
        public String info() { return "two constructors: clock=" + clock; }
    }

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
                Clock2.class, FieldStyle.class, SetterStyle.class, ConstructorStyle.class, TwoConstructors.class)) {
            System.out.println(ctx.getBean(FieldStyle.class).info());
            System.out.println(ctx.getBean(SetterStyle.class).info());
            System.out.println(ctx.getBean(ConstructorStyle.class).info());
            System.out.println(ctx.getBean(TwoConstructors.class).info());
        }
        // Spring を使わずに new した場合
        try { System.out.println(new FieldStyle().info()); }
        catch (NullPointerException e) { System.out.println("new FieldStyle().info() -> " + e); }
        System.out.println(new ConstructorStyle(new Clock2()).info());
    }
}

実行結果(Java 21.0.10、Spring Framework 4.3.30):

field: 2026-10-02T09:00
setter: 2026-10-02T09:00
constructor: 2026-10-02T09:00
two constructors: clock=null
new FieldStyle().info() -> java.lang.NullPointerException: Cannot invoke "InjectionStyles$Clock2.now()" because "this.clock" is null
constructor: 2026-10-02T09:00

結果から読み取れることは3つです。

  1. Spring 経由なら3方式とも同じように注入されます。 違いは「Spring の外」で表れます。
  2. フィールドインジェクションのクラスを new すると、依存が null のまま NullPointerException になります。 コンストラクタ版は引数が必須なので、この渡し忘れが起きません。
  3. コンストラクタが2つあると、@Autowired なしでは引数なしのコンストラクタが使われ、clock=null になります。 @Autowired を省略できるのは「コンストラクタが1つだけ」のときです。

@Autowiredを省略できる条件

クラスの状態 Spring が使うコンストラクタ @Autowired
コンストラクタが1つだけ そのコンストラクタ 不要(付けても動く)
複数あり、どれにも @Autowired なし 引数なしのコンストラクタ(あれば) 使いたい方に 必要
複数あり、1つに @Autowired @Autowired を付けたもの 必要
Java の record 正準コンストラクタ(1つ) 不要

コンストラクタが2つ必要な場面はあまり多くありません。「Bean はコンストラクタを1つにする」と決めておく と、@Autowired を書くかどうかで迷わなくなります。

Lombokの@RequiredArgsConstructorで短く書く

依存が増えるとコンストラクタを書くのが手間になります。Lombok の @RequiredArgsConstructor は、final フィールドを引数に持つコンストラクタを自動生成 します(Lombok はこの環境では入手できなかったため未実行。Lombok 公式ドキュメントの仕様に沿った例です)。

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class OrderService {
    private final NotificationSender sender;   // final → コンストラクタ引数になる
    private final OrderRepository repository;  // final → コンストラクタ引数になる

    public String placeOrder(String member) {
        repository.save(member);
        sender.send(member, "注文を受け付けました");
        return "ORDERED";
    }
}

生成されるコンストラクタは1つだけなので、@Autowired なしで注入されます。注意点が1つあります。フィールドに付けた @Qualifier は、そのままでは生成されたコンストラクタの引数にコピーされません。 @Qualifier を使うときは、プロジェクトのルートに lombok.config を置いてコピー対象に加えます。

# lombok.config
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier

@Qualifier を使うクラスだけ、コンストラクタを手で書くのも分かりやすい方法です。

Beanを登録する方法

DI で注入できるのは コンテナに登録された Bean だけ です。登録の方法は大きく2つです。

方法 書き方 向いている用途
コンポーネントスキャン クラスに @Component・@Service・@Repository・@Controller(@RestController) 自分で書いたクラス
Java コンフィグ @Configuration クラスの @Bean メソッドで return new ...(例:@Bean Clock clock() { return Clock.systemDefaultZone(); }) 外部ライブラリのクラス、生成に設定値が必要なクラス

@Service と @Repository は機能的にはどちらも @Component の一種 です。役割(業務ロジック/データアクセス)を読み手に伝えるために使い分けます(@Repository にはデータアクセス例外の変換対象になるという追加の意味があります)。

Spring Boot では、@SpringBootApplication を付けたクラスのパッケージとその配下 がスキャン対象です。別のパッケージに置いたクラスは見つからないため、後述の NoSuchBeanDefinitionException の原因になります。

同じ型のBeanが複数あるとき:@Primary・@Qualifier・List注入

通知手段を「メール」「チャット」「プッシュ」の3つに増やすと、NotificationSender 型の Bean が複数になります。Spring は型で注入先を探すため、どれを渡せばよいか決められずにエラーになります。

エラーを再現する

以下は検証に使ったコードの全体です(1つのファイルでエラーの再現と直し方をまとめて確認できるようにしています)。

import org.springframework.context.annotation.*;
import org.springframework.stereotype.*;
import org.springframework.beans.factory.annotation.*;
import java.util.*;

public class DiErrors {
    public interface NotificationSender { String channel(); }
    @Component("mailSender") public static class MailSender implements NotificationSender { public String channel() { return "mail"; } }
    @Component("chatSender") public static class ChatSender implements NotificationSender { public String channel() { return "chat"; } }
    @Primary @Component("pushSender") public static class PushSender implements NotificationSender { public String channel() { return "push"; } }

    @Service public static class OrderService {
        final NotificationSender sender;
        public OrderService(NotificationSender sender) { this.sender = sender; }
    }
    @Service public static class QualifiedService {
        final NotificationSender sender;
        public QualifiedService(@Qualifier("chatSender") NotificationSender sender) { this.sender = sender; }
    }
    @Service public static class BroadcastService {
        final List<NotificationSender> senders; final Map<String, NotificationSender> byName;
        public BroadcastService(List<NotificationSender> senders, Map<String, NotificationSender> byName) {
            this.senders = senders; this.byName = byName;
        }
    }
    // 循環(コンストラクタ)
    @Service public static class A { public A(B b) {} }
    @Service public static class B { public B(A a) {} }
    // 循環(フィールド)
    @Service public static class C { @Autowired D d; }
    @Service public static class D { @Autowired C c; }
    // Bean が無い
    public interface StockClient {}
    @Service public static class StockService { public StockService(StockClient c) {} }

    static void run(String title, boolean allowCircular, Class<?>... classes) {
        System.out.println("=== " + title);
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.setAllowCircularReferences(allowCircular);
        ctx.register(classes);
        try {
            ctx.refresh();
            for (Class<?> c : classes) {
                Object b = ctx.getBean(c);
                if (b instanceof OrderService) System.out.println("OrderService <- " + ((OrderService) b).sender.channel());
                if (b instanceof QualifiedService) System.out.println("QualifiedService <- " + ((QualifiedService) b).sender.channel());
                if (b instanceof BroadcastService) { BroadcastService bs = (BroadcastService) b;
                    System.out.println("List  <- " + bs.senders.stream().map(NotificationSender::channel).toList());
                    System.out.println("Map   <- " + bs.byName.keySet()); }
                if (b instanceof C) System.out.println("C.d = " + ((C) b).d.getClass().getSimpleName()
                        + ", D.c = " + ((C) b).d.c.getClass().getSimpleName());
            }
            ctx.close();
        } catch (Exception e) {
            System.out.println(e.getClass().getName() + ": " + e.getMessage()); // エラー全文
        }
    }

    public static void main(String[] args) {
        run("1. 実装が2つ(@Primary なし)", true, MailSender.class, ChatSender.class, OrderService.class);
        run("2. @Primary を付けた PushSender を追加", true, MailSender.class, ChatSender.class, PushSender.class, OrderService.class);
        run("3. @Qualifier(\"chatSender\")", true, MailSender.class, ChatSender.class, PushSender.class, QualifiedService.class);
        run("4. List / Map で全部受け取る", true, MailSender.class, ChatSender.class, PushSender.class, BroadcastService.class);
        run("5. コンストラクタ同士の循環", true, A.class, B.class);
        run("6. フィールド同士の循環(allowCircularReferences=true)", true, C.class, D.class);
        run("7. フィールド同士の循環(allowCircularReferences=false)", false, C.class, D.class);
        run("9. 注入先の Bean が無い", true, StockService.class);
    }
}

まず「1.」の、@Primary も @Qualifier もない状態の実行結果です(Java 21.0.10、Spring Framework 4.3.30。1行が長いので折り返して表示される場合があります)。

=== 1. 実装が2つ(@Primary なし)
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'diErrors.OrderService': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'DiErrors$NotificationSender' available: expected single matching bean but found 2: mailSender,chatSender

読むべきところは expected single matching bean but found 2: mailSender,chatSender です。「1つを期待したが2つ見つかった。候補は mailSender と chatSender」という意味で、候補の Bean 名まで表示されます。

Spring Boot で起動した場合は、FailureAnalyzer(起動失敗を読みやすく整形する仕組み)が次の形式で表示します。Spring Boot 3.5 系のソース(NoUniqueBeanDefinitionFailureAnalyzer)で文言を確認しました(Boot は未実行のため、表示の一部を省略した形式の例です)。

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.example.OrderService required a single bean, but 2 were found:
    - mailSender: ...
    - chatSender: ...

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

Action に書かれている3つの直し方が、そのまま次の3つの節です。

直し方1:@Primaryで「基本はこれ」を決める

=== 2. @Primary を付けた PushSender を追加
OrderService <- push

@Primary は「指定がなければこれを使う」という既定値 です。同じ型に @Primary を2つ付けると、再び1つに決められずエラーになります。

直し方2:@Qualifierで注入先ごとに指定する

=== 3. @Qualifier("chatSender")
QualifiedService <- chat

@Primary(pushSender)があっても、@Qualifier で指定した chatSender が注入されました。 優先順位は @Qualifier > @Primary です。@Qualifier の文字列は Bean 名(@Component("chatSender") の値、省略時はクラス名の先頭を小文字にしたもの)です。

直し方3:List・Mapで全部受け取る

「全チャネルに送る」「種類に応じて使い分ける」場合は、まとめて受け取ります。

=== 4. List / Map で全部受け取る
List  <- [mail, chat, push]
Map   <- [mailSender, chatSender, pushSender]

List には同じ型の Bean がすべて、Map には「Bean 名 → Bean」の形で入ります。 List の並び順を決めたいときは、各クラスに @Order(1) などを付けます。Map を使うと、byName.get("chatSender") のように設定値やリクエストの値で実装を選ぶ「ストラテジー」の形を簡単に作れます。

どれを選ぶかの判断表

状況 選ぶ方法
ほとんどの場所で同じ実装を使い、一部だけ別のものを使う @Primary+例外的な箇所に @Qualifier
場所ごとに使う実装がはっきり分かれている @Qualifier(または独自修飾アノテーション)
すべての実装を順に呼ぶ List<型>(順序は @Order)
実行時の値で実装を選ぶ Map<String, 型>
環境(開発・本番)で実装を切り替える @Profile や @ConditionalOnProperty(Boot)で、そもそも1つしか登録しない

補足(パラメーター名での一致):Spring は候補が複数あるとき、最後の手段として 引数名と Bean 名が一致する候補 を選びます。たとえば OrderService(NotificationSender mailSender) と書くと mailSender が選ばれます。ただし Spring Framework 6.1 以降、この判定にはコンパイル時の -parameters オプションが必要です(Spring Boot の Maven/Gradle プラグインは既定で付けます)。引数名を変えるだけで注入先が変わるため、意図を明示したいときは @Qualifier を使う ほうが読み手に伝わります。

循環参照:BeanCurrentlyInCreationExceptionの再現と直し方

循環参照とは、Bean A が B を必要とし、B も A を必要としている状態 です。

コンストラクタ同士の循環はどの設定でも起動できない

前掲の DiErrors の A・B(コンストラクタで互いを受け取る)の実行結果です。

=== 5. コンストラクタ同士の循環
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'diErrors.A': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'diErrors.B': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'diErrors.A': Requested bean is currently in creation: Is there an unresolvable circular reference?

A を作るには B が要り、B を作るには A が要るので、どちらも作れません。Requested bean is currently in creation: Is there an unresolvable circular reference?(作成中の Bean がまた要求された。解決できない循環参照では?) というメッセージが目印です。

フィールド同士の循環は設定によって結果が変わる

C・D(@Autowired フィールドで互いを持つ)を、循環参照の許可/禁止を切り替えて実行しました。

=== 6. フィールド同士の循環(allowCircularReferences=true)
C.d = D, D.c = C
=== 7. フィールド同士の循環(allowCircularReferences=false)
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'diErrors.C': Unsatisfied dependency expressed through field 'd'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'diErrors.D': Unsatisfied dependency expressed through field 'c'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'diErrors.C': Requested bean is currently in creation: Is there an unresolvable circular reference?

フィールドインジェクションでは、Spring が「作りかけの C」を先に D へ渡すことで循環を解決できます(6.)。Spring Framework 単体ではこれが既定で許可されています。しかし Spring Boot 2.6 以降は循環参照が既定で禁止(allowCircularReferences=false 相当)になり、7. と同じくエラーで起動が止まります。Spring Boot 2.6 のリリースノートには、起動できない場合の最後の手段として spring.main.allow-circular-references=true が挙げられています。

依存の受け取り方 Spring Framework 単体(既定) Spring Boot 2.6 以降(既定)
コンストラクタ同士 起動できない 起動できない
フィールド/セッター同士 起動できる 起動できない

Spring Boot で起動した場合の Action 欄の文言は、Spring Boot 3.5 系のソース(BeanCurrentlyInCreationFailureAnalyzer)で次のとおりと確認しました(未実行)。

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.

「循環参照に頼ることは推奨されない。依存の循環を取り除くこと。最後の手段として設定で許可できる」 という意味です。設定で許可するより、次の直し方を先に検討します。

直し方の選び方

直し方 内容 向いている場面
責務を分ける(第一候補) 両者が共通で使う処理を第3のクラスへ切り出す A と B が互いの一部の機能だけを使っている
呼び出しを一方向にする 片方が必要とする値を引数で渡す 「聞きに行く」をやめて「渡す」にできる
イベントにする ApplicationEventPublisher で通知し、相手は @EventListener で受ける 「終わったら知らせる」関係
@Lazy 引数を遅延プロキシにし、初回呼び出し時に本物を取得 構造をすぐ変えられないときのつなぎ
ObjectProvider<型> 使う時点で getObject() して取り出す 同上。遅延していることがコード上で見える
spring.main.allow-circular-references=true Boot 2.6 以前の挙動に戻す 移行時の一時対応(コンストラクタ同士の循環には効かない)

責務を分ける例

// 直す前:MemberService ⇄ PointService が互いを呼ぶ
// 直した後:両方が使う「ポイント残高の参照」だけを切り出し、依存を一方向にする
@Service
public class PointBalanceReader {
    private final PointRepository repository;
    public PointBalanceReader(PointRepository repository) { this.repository = repository; }
    public int points(String member) { return repository.sumPoints(member); }
}

@Service
public class MemberService {
    private final PointBalanceReader balance;   // PointService ではなくこちらに依存
    public MemberService(PointBalanceReader balance) { this.balance = balance; }
}
// PointService は MemberService と PointBalanceReader に依存(一方向)

依存の向きが一方向になれば、循環参照は起きません。 コンストラクタインジェクションなら循環は起動時のエラーとしてすぐ分かるので、設計を見直すきっかけになります。

@LazyとObjectProviderで回避する例(実行結果つき)

import org.springframework.context.annotation.*;
import org.springframework.stereotype.*;
import org.springframework.beans.factory.ObjectProvider;

public class DiLazy {
    public interface PointApi { int points(String member); }
    public interface MemberApi { String rank(String member); }

    @Service public static class MemberService implements MemberApi {
        private final PointApi pointApi;
        public MemberService(@Lazy PointApi pointApi) { this.pointApi = pointApi; }
        public String rank(String m) { return pointApi.points(m) >= 100 ? "GOLD" : "NORMAL"; }
    }
    @Service public static class PointService implements PointApi {
        private final MemberApi memberApi;
        public PointService(MemberApi memberApi) { this.memberApi = memberApi; }
        public int points(String m) { return 120; }
    }
    // ObjectProvider で必要になった時に取得
    @Service public static class MemberService2 implements MemberApi {
        private final ObjectProvider<PointApi> pointApi;
        public MemberService2(ObjectProvider<PointApi> pointApi) { this.pointApi = pointApi; }
        public String rank(String m) { return pointApi.getObject().points(m) >= 100 ? "GOLD" : "NORMAL"; }
    }

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
            ctx.setAllowCircularReferences(false);   // Boot 2.6 以降の既定と同じ条件
            ctx.register(MemberService.class, PointService.class);
            ctx.refresh();
            MemberService ms = ctx.getBean(MemberService.class);
            System.out.println("@Lazy: rank = " + ms.rank("member-001"));
            System.out.println("injected type = " + ms.pointApi.getClass().getName());
        }
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
            ctx.setAllowCircularReferences(false);
            ctx.register(MemberService2.class, PointService.class);
            ctx.refresh();
            System.out.println("ObjectProvider: rank = " + ctx.getBean(MemberService2.class).rank("member-001"));
        }
    }
}

実行結果(Java 21.0.10、Spring Framework 4.3.30):

@Lazy: rank = GOLD
injected type = jdk.proxy2.$Proxy9
ObjectProvider: rank = GOLD

@Lazy を付けた引数には本物ではなくプロキシ(jdk.proxy2.$Proxy9)が入り、最初にメソッドが呼ばれたときに本物の Bean を取りに行きます。 これで起動時の循環が解けます。

検証時の注意として、Spring 4.3 では @Lazy の対象を インターフェース型 にしています。具象クラスを対象にすると CGLIB によるクラスプロキシが必要になり、Spring 4.3 同梱の CGLIB が Java 21 のクラスに対応していないため AopConfigException で失敗しました。Java 17 以降に対応した Spring Framework 6.x/7.x では具象クラスにも @Lazy を使えます(公式で確認。この環境では未実行)。

Beanが見つからない:NoSuchBeanDefinitionExceptionの再現と直し方

public interface StockClient {}

@Service
public class StockService {
    public StockService(StockClient client) {}   // StockClient の実装が登録されていない
}
=== 9. 注入先の Bean が無い
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'diErrors.StockService': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'DiErrors$StockClient' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

expected at least 1 bean(少なくとも1つ必要)と、探した型 StockClient が表示されます。 Spring Boot では FailureAnalyzer により次の形式で表示されます(Spring Boot 3.5 系のソース NoSuchBeanDefinitionFailureAnalyzer の文言から組み立てた例。未実行)。

Description:

Parameter 0 of constructor in com.example.StockService required a bean of type 'com.example.StockClient' that could not be found.

Action:

Consider defining a bean of type 'com.example.StockClient' in your configuration.

原因チェックリスト

確認すること よくある状況 直し方
実装クラスに @Component 系が付いているか インターフェースだけ作って実装クラスに付け忘れた @Service などを付ける
スキャン範囲に入っているか @SpringBootApplication のクラスより上や別のパッケージに置いた パッケージを移す、または @ComponentScan/scanBasePackages で指定
@Bean メソッドの @Configuration クラスが読み込まれているか 設定クラスがスキャン範囲外 同上、または @Import
条件付き登録で外れていないか @Profile("prod")、@ConditionalOnProperty の条件を満たしていない 起動プロファイルやプロパティを確認
依存ライブラリが入っているか 自動構成される Bean(例:JdbcTemplate)のスターターが無い 必要なスターターを追加
テストのスライスで読み込まれているか @WebMvcTest は Controller 周りだけを読み込む @MockitoBean で用意する、または @Import
無くてもよい依存か 任意の機能 ObjectProvider<型> や Optional<型> で受け取る

Spring Boot では、起動時に --debug を付けると 自動構成の条件評価レポート が表示され、どの条件で Bean が登録されなかったかを確認できます。

テスト:コンストラクタにモックを渡す・@MockitoBeanで差し替える

コンストラクタインジェクションで書いておくと、テストの選択肢が広がります。

テストの種類 書き方 Spring の起動 速さ 向いている対象
単体テスト new OrderService(mock) しない 速い 業務ロジックの大半
スライステスト @WebMvcTest などで一部だけ起動+@MockitoBean 一部 中 Controller の入出力
結合テスト @SpringBootTest+必要に応じて @MockitoBean すべて 遅め 設定・Bean 構成を含めた確認

JUnit 5+Mockitoで単体テストする

Mockito の mock() で作ったモックをコンストラクタに渡すだけです。Spring を起動しないので、DI の設定ミスとは切り離して業務ロジックだけを確認できます。(Mockito はこの環境で入手できなかったため未実行。前半の JUnit 4 の例と同じ考え方を Mockito で書いたものです。Mockito の基本は既存記事「Mockitoの使い方|JUnit 5での基本からよくあるエラーまで」で詳しく扱っています。)

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

class OrderServiceTest {

    @Test
    void 注文すると会員に通知が送られる() {
        NotificationSender sender = mock(NotificationSender.class);
        OrderService service = new OrderService(sender);   // コンストラクタに渡すだけ

        String result = service.placeOrder("member-001");

        assertEquals("ORDERED", result);
        verify(sender).send("member-001", "注文を受け付けました");
    }
}

@MockitoBeanでSpringのBeanを差し替える(Spring Boot 3.4以降)

Spring を起動したテストで、特定の Bean だけをモックにしたいときは @MockitoBean を使います(Spring Boot は未実行。Spring Framework 公式リファレンスの記載に沿った例です)。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import static org.mockito.Mockito.verify;

@SpringBootTest
class OrderServiceIntegrationTest {

    @Autowired
    OrderService orderService;

    @MockitoBean                 // コンテナ内の NotificationSender をモックに置き換える
    NotificationSender sender;

    @Test
    void 注文すると通知が呼ばれる() {
        orderService.placeOrder("member-001");
        verify(sender).send("member-001", "注文を受け付けました");
    }
}

公式リファレンスで確認した要点です。

項目 内容
パッケージ org.springframework.test.context.bean.override.mockito
提供元 Spring Framework 6.2(Spring Boot 3.4 が対応)
置き換え対象の指定 フィールドの型で決まる(byType)。@MockitoBean("mailSender") で Bean 名を指定(byName)
対象の Bean が無い場合 新しくモックを作って登録する(戦略は REPLACE_OR_CREATE)
スパイにしたい場合 @MockitoSpyBean(本物の Bean をスパイで包む)
クラスに付ける書き方 @MockitoBean(types = {OrderService.class, MemberService.class})
旧アノテーション Spring Boot の @MockBean/@SpyBean は Boot 3.4 で非推奨、Boot 4.0 で削除

同じ型の Bean が複数あるときは、@MockitoBean("chatSender") のように名前で指定 します。テストごとにモック構成が変わるとテスト用コンテナのキャッシュが効きにくくなるため、基本は単体テスト(new+モック)、Spring 全体での確認は少数の結合テスト という配分にすると、テスト全体が速く保てます。Controller のテスト(@WebMvcTest と MockMvc)は、既存記事「Spring Bootテスト入門|@WebMvcTestとMockMvcの使い分け」と「Spring BootでREST APIを作る|入力チェック・エラー処理・テストまで」で扱っています。

バージョンによる違い

項目 Spring Framework 4.3(本記事の検証) Spring Framework 6.x/7.x・Spring Boot 3.x/4.x
単一コンストラクタの @Autowired 省略 可(4.3 で導入) 可
@Primary・@Qualifier・List/Map 注入 同じ 同じ
例外の種類 UnsatisfiedDependencyException の中に NoUniqueBeanDefinitionException などが入る 同じ構造
例外メッセージ 原因が nested exception is ... で1行に連結される 6.0 以降は連結されない。根本原因はスタックトレースの Caused by: で確認
循環参照の既定 Framework 単体は許可 Spring Boot 2.6 以降は禁止(spring.main.allow-circular-references)
引数名による Bean 選択 クラスファイルのデバッグ情報があれば可 6.1 以降は -parameters が必要(Boot のビルドプラグインは既定で付与)
テストの Bean 差し替え — @MockitoBean(6.2〜)。Boot の @MockBean は 4.0 で削除
@Lazy の対象 Java 21 ではインターフェース型で確認(クラスは CGLIB 非対応で失敗) クラスにも使える
必要な Java Java 6〜8 世代向け Spring Boot 3.x/4.x は Java 17 以上。Java 25 での書き方の違いは DI の範囲ではありません(公式で確認、この環境は Java 21 のため未実行)

DI の書き方そのものは版をまたいで共通 です。違いが出るのは主に「エラーの見え方」と「Spring Boot の既定値」「テスト用アノテーション」なので、上の表を移行時のチェックに使ってください。

よくある質問

Q. @Autowiredは付けたほうがよいですか? A. コンストラクタが1つだけなら付けなくても注入されます(Spring 4.3 以降)。付けても動作は同じです。チームで「単一コンストラクタには付けない」などルールをそろえておくと読みやすくなります。コンストラクタが複数あるときは、使ってほしいものに必ず付けます。

Q. @Primaryと@Qualifierはどちらが優先されますか? A. @Qualifier が優先されます。本記事の実行結果でも、@Primary の pushSender があっても @Qualifier("chatSender") の箇所には chatSender が注入されました。「基本は @Primary、例外的な箇所は @Qualifier」と組み合わせるのが定番です。

Q. 循環参照はspring.main.allow-circular-references=trueで直してよいですか? A. Spring Boot 2.6 のリリースノートでも、この設定は構造をすぐ変えられない場合の手段として案内されています。コンストラクタ同士の循環には効かないため、まずは共通処理を別クラスに切り出して依存を一方向にするのがおすすめです。すぐに直せない場合は @Lazy や ObjectProvider をつなぎに使えます。

まとめ

  • Spring DI は、依存を自分で new せずコンテナから受け取る仕組み で、テストで偽物やモックに差し替えやすくなる
  • 必須の依存はコンストラクタインジェクション が基本。final にでき、渡し忘れがコンパイル時に分かる
  • コンストラクタが1つなら @Autowired は省略可。2つあると引数なしのコンストラクタが使われる点に注意
  • 同じ型が複数あると NoUniqueBeanDefinitionException。@Primary・@Qualifier(こちらが優先)・List/Map 注入で解決
  • 循環参照は BeanCurrentlyInCreationException。Spring Boot 2.6 以降は既定で禁止 なので、責務を分けて依存を一方向にする
  • NoSuchBeanDefinitionException は @Component 付け忘れ・スキャン範囲・条件付き登録をチェックリストで確認
  • テストは コンストラクタにモックを渡す単体テストが中心。Spring の Bean を差し替えるなら @MockitoBean(Boot 4.0 で @MockBean は削除)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring DIとは|@Autowiredとコンストラクタインジェクション」

株式会社RJCでは、Spring の DI やテストしやすい設計を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?