Spring Bootの@Qualifierと@Primaryの違いを解説する記事のアイキャッチ。Java道場風の学習空間で若手エンジニアがDIとBeanの使い分けを考えているイラスト。

Spring

Spring Bootの@Qualifierと@Primaryの違いは?Beanが複数ある時の使い分け

AIによる要約

@Primaryは同じ型のBeanが複数ある時の標準候補を決め、@Qualifierは注入先ごとに候補を明示します。アプリ全体で通常使う実装が1つなら@Primary、業務ごとに実装を選び分けるなら@Qualifierが基本です。エラーを消すためだけでなく、どの実装を使う設計なのかをコードで説明できることが重要です。

新人SE
新人SE
同じインターフェースの実装を2つ作ったら、Springが起動しなくなりました。@Primaryと@Qualifierのどちらを付ければよいですか?
ポンコツSE
ポンコツSE
まず「通常使う実装が決まっているか」「呼び出し側ごとに選ぶ必要があるか」を確認します。エラーを消すために適当に付けると、後で別の実装が誤って使われます。

Spring Bootでは、ControllerやServiceが必要とするオブジェクトをDIコンテナから注入できます。実装が1つなら型だけで候補を決められますが、同じインターフェースを実装するBeanが複数あると、Springはどれを注入すべきか判断できません。

決済方法、通知方法、外部APIの本番用・スタブ用など、SIerの業務システムでも複数実装はよくあります。この記事では、@Primary@Qualifierを付ける基準を、現場のレビューで説明できる形に整理します。

この記事のポイント

  • @Primaryは型注入で優先する標準Beanを決める
  • @Qualifierは注入先で使うBeanを明示する
  • 複数のBeanをすべて使うならList<型>で受け取る
  • Bean名への暗黙依存ではなく、選定理由が読めるコードにする

Beanが複数あると何が起きるか

例として、通知を送るインターフェースを考えます。メール通知とチャット通知の2実装をBeanとして登録します。

public interface NotificationSender {
    void send(String message);
}

@Component
public class MailNotificationSender implements NotificationSender {
    public void send(String message) {
        // メール送信
    }
}

@Component
public class ChatNotificationSender implements NotificationSender {
    public void send(String message) {
        // チャット送信
    }
}

この状態でNotificationSenderを1つだけコンストラクタ注入すると、型に一致する候補が2つあります。選定材料がなければ、NoUniqueBeanDefinitionExceptionなどの形で起動に失敗します。

@Service
public class OrderService {
    private final NotificationSender notificationSender;

    public OrderService(NotificationSender notificationSender) {
        this.notificationSender = notificationSender;
    }
}

エラーの本質は「Beanが2つあること」ではなく、注入先がどちらを必要としているかをコードで表現していないことです。

@Primaryは標準で使うBeanを決める

アプリケーション全体ではメール通知を通常使い、一部の処理だけチャット通知を使うなら、メール側を標準候補にできます。

@Primary
@Component
public class MailNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        // メール送信
    }
}

@Primaryを付けると、NotificationSenderを型だけで注入した場所ではメール実装が選ばれます。呼び出し側を簡潔に保てるのが利点です。

@Primaryが向くのは、「特に指定がなければこの実装を使う」というアプリ全体の既定値がある場合です。

@Qualifierは注入先ごとに実装を選ぶ

障害通知だけはチャットへ送りたいなど、呼び出し側の用途によって実装が決まる場合は@Qualifierで候補を絞ります。

@Service
public class FailureAlertService {
    private final NotificationSender notificationSender;

    public FailureAlertService(
            @Qualifier("chatNotificationSender")
            NotificationSender notificationSender) {
        this.notificationSender = notificationSender;
    }
}

このコードは、障害通知がチャット実装を必要としていることを注入箇所で示しています。@Primaryが付いたBeanが別にあっても、明示したQualifierが優先されます。

ただし、実装クラス名をそのままQualifier文字列にすると、クラス名変更の影響を受けます。用途を示す名前にするか、複数箇所で使うなら独自Qualifierアノテーションを検討します。新人のうちは、まずプロジェクト内の既存ルールに合わせてください。

使い分けを表で整理する

判断すること@Primary@Qualifier
選ぶ場所Bean側注入先側
意味型注入の標準候補用途に合う候補へ絞る
向く状況通常実装が1つ決まっている処理ごとに実装を選ぶ
注意点既定値が本当に妥当か文字列やBean名への依存を増やさないか

迷ったら、「指定しなかった呼び出し側はどの実装を使うべきか」と質問します。答えが1つなら@Primary、呼び出し側の目的を見ないと決められないなら@Qualifierが候補です。

複数のBeanを全部使うならListで受け取る

すべての通知手段へ送る処理なら、どれか1つを選ぶ必要はありません。同じ型のBeanをListとして注入できます。

@Service
public class BroadcastService {
    private final List<NotificationSender> senders;

    public BroadcastService(List<NotificationSender> senders) {
        this.senders = senders;
    }

    public void broadcast(String message) {
        for (NotificationSender sender : senders) {
            sender.send(message);
        }
    }
}

処理順が重要なら、@Orderなどプロジェクトで採用している並び順の仕組みを確認します。登録順を偶然の仕様として扱ってはいけません。

よくあるNG対応

もっとも危険なのは、例外を消すためだけに片方へ@Primaryを付ける対応です。注文処理が本来チャット通知を使うべきなのに、たまたまメール実装が注入されても、アプリケーションは起動してしまいます。

  • 理由を確認せず、先に見つけた実装へ@Primaryを付ける
  • フィールド名とBean名が一致した時の暗黙解決に依存する
  • 実装クラスを直接注入し、インターフェースを使う意味を失わせる
  • 用途の異なるBeanを1つの巨大なif文へまとめる

Springのバージョンやコンパイル設定によっては、コンストラクタ引数名とBean名の一致が候補解決に使われることがあります。しかし、それを設計意図の代わりにすると読み手が判断しづらいため、用途が重要なら明示的なQualifierを使います。

現場レビューでの見られ方

// レビューコメント例
NoUniqueBeanDefinitionExceptionを消す目的ではなく、
通常利用する実装がMailでよい理由をコメントに残してください。

// レビューコメント例
このServiceは障害通知専用なので、@Primary任せではなく
用途が読めるQualifierでChat実装を指定したいです。

// レビューコメント例
全実装へ送る要件なら、1件を選ばずList注入で表現できないか確認してください。

レビュー前のセルフチェック

確認リスト

  • 同じ型のBeanが何個登録されるか把握しているか
  • 標準実装があるのか、呼び出し側ごとに選ぶのか説明できるか
  • @Primaryをエラー回避だけの目的で付けていないか
  • Qualifier名が実装クラス名ではなく用途を表しているか
  • 全部使う要件なのに1件だけを注入していないか
書籍「Spring徹底入門 第2版」の表紙
商品画像:Amazon.co.jp
PR Springを体系的に学ぶ若手向け

Spring徹底入門 第2版

株式会社NTTデータ (著), 株式会社NTTデータグループ (編集)

Spring Frameworkの主要機能を、実務レベルで整理する。

DI、MVC、データアクセス、テスト、セキュリティまで幅広く確認できます。ピンポイント検索から体系学習へ進みたい人向けです。

  • Springの全体像を整理したい
  • 現場で使う機能を体系的に学びたい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

@Primaryは型注入の標準候補を決め、@Qualifierは注入先の用途に合う候補へ絞ります。どちらも複数Beanのエラーを解消できますが、表している設計意図は同じではありません。

新人のうちは、アノテーションを付ける前に「この処理はどの実装を必要としているか」「指定がない場所では何を使うか」を言葉にしてください。その答えが、そのまま@Primary@Qualifierの判断基準になります。

-Spring
-, , , ,