決済方法や通知チャネルのように、同じインターフェースに実装クラスを2つ以上用意した瞬間、アプリケーションが起動しなくなった経験はありませんか。NoUniqueBeanDefinitionException というエラーで、DIコンテナが「どちらを注入すればいいか分からない」と言ってきます。この例外が起きる理由から、@Qualifier と @Primary の使い分け、両者が競合したときの優先順位までを実例コードで整理します。

NoUniqueBeanDefinitionExceptionはなぜ起きるのか

PaymentService というインターフェースに対して、クレジットカードと銀行振込の2実装を @Component で登録してみます。

public interface PaymentService {
    void pay(int amount);
}

@Component
public class CreditCardPaymentService implements PaymentService {
    @Override
    public void pay(int amount) {
        System.out.println("クレジットカードで決済: " + amount);
    }
}

@Component
public class BankTransferPaymentService implements PaymentService {
    @Override
    public void pay(int amount) {
        System.out.println("銀行振込で決済: " + amount);
    }
}

この状態で単純に @Autowired すると、起動時に例外が発生します。

@Service
public class OrderService {
    @Autowired
    private PaymentService paymentService; // ここで起動時エラー
}

DIコンテナは PaymentService 型のBeanを2つ見つけていて、@Componentで登録されたBeanの基本的な扱いだけではどちらを注入すべきか判断できないのです。

エラーメッセージから原因を特定する

このとき表示される例外はこういう形です。

org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'com.example.PaymentService' available:
expected single matching bean but found 2: creditCardPaymentService,bankTransferPaymentService

列挙されている名前は、クラス名の先頭を小文字にしたデフォルトのBean名です。候補が2つ出ているということは、「どちらかを明示的に選ぶコードを書けば解決する」というサインなので、次からは @Qualifier と @Primary で意図を伝えていきます。

@Qualifierで注入先を明示する

まず実装クラス側に識別名を付けます。

@Component
@Qualifier("creditCard")
public class CreditCardPaymentService implements PaymentService {
    // ...
}

@Component
@Qualifier("bankTransfer")
public class BankTransferPaymentService implements PaymentService {
    // ...
}

注入側にも同じ名前の @Qualifier を指定すれば、意図した実装だけが注入されます。

@Service
public class OrderService {
    @Autowired
    @Qualifier("creditCard")
    private PaymentService paymentService;
}

指定した識別名がどの実装にも一致しない場合、候補がゼロになるため NoUniqueBeanDefinitionException ではなく NoSuchBeanDefinitionException (no matching qualifier value) という別の例外になります。「候補が多すぎる」エラーと「候補が見つからない」エラーは別物だと覚えておくと、ログを読むときに迷いません。

コンストラクタ注入で@Qualifierを使う

フィールド注入よりコンストラクタインジェクションの方が推奨されるのは @Qualifier を使う場合も同じです。引数に直接付けられます。

@Service
public class OrderService {

    private final PaymentService paymentService;

    public OrderService(@Qualifier("creditCard") PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

依存が引数として見えるので、テストでモックに差し替えるときも new OrderService(mockPaymentService) とするだけで済みます。

@Primaryでデフォルト実装を指定する

毎回 @Qualifier を書くのが冗長なら、「基本はこれを使う」という実装に @Primary を付けておく方法があります。

@Component
@Primary
public class CreditCardPaymentService implements PaymentService {
    // ...
}

これだけで、先ほどの最初の OrderService(@Qualifier を付けないフィールド注入)のコードは一切変更せずに CreditCardPaymentService が選ばれるようになります。

@Primaryの適用範囲と注意点

@Primary の影響範囲は「そのクラスを付けた1箇所」ではなく、「アプリケーション内でその型を注入している全箇所」です。PaymentService を注入しているクラスが10個あれば、10箇所すべてで CreditCardPaymentService がデフォルトになります。

一部の箇所だけ別実装を使いたい場合は、その箇所にだけ @Qualifier を指定して上書きします。また、複数のクラスに @Primary を付けてしまうと、今度は「どのPrimaryを使うか」が一意に決まらず、同じ NoUniqueBeanDefinitionException が再発します。@Primary は型ごとに1つまでというのが基本ルールです。

@Qualifierと@Primaryが競合した場合の優先順位

@Primary 付きの実装と @Qualifier 付きの実装が混在していても大丈夫です。注入側に @Qualifier があればそれが優先され、無ければ @Primary が使われます。

@Component
@Primary
public class CreditCardPaymentService implements PaymentService { /* ... */ }

@Component
@Qualifier("bankTransfer")
public class BankTransferPaymentService implements PaymentService { /* ... */ }

@Service
public class OrderService {
    @Autowired
    private PaymentService defaultPayment; // @Primaryのクレジットカード実装

    @Autowired
    @Qualifier("bankTransfer")
    private PaymentService alternativePayment; // 明示指定が優先
}

同じ型でも注入箇所ごとにどちらを使うか切り分けられるのがポイントです。

パラメータ名によるBean名フォールバック解決

@Qualifier も @Primary もない状態でも、フィールド名やコンストラクタ引数名がBean名と一致していれば、Springはそれを使って自動的に解決してくれます。

@Service
public class OrderService {
    @Autowired
    private PaymentService creditCardPaymentService; // 名前一致で解決される
}

便利ではありますが、リファクタリングで変数名を変えた瞬間に解決できなくなり、起動時に例外が復活します。チームで保守するコードでは、名前解決に頼らず @Qualifier で明示する方が安全です。

優先順位のまとめ

ここまでの解決方法には明確な優先順位があります。

  1. 注入側の @Qualifier 指定
  2. @Primary
  3. フィールド名・パラメータ名によるBean名解決

どれにも当てはまらず候補が複数残っていれば NoUniqueBeanDefinitionException になります。チーム開発では「デフォルトは @Primary で決めておき、例外的な箇所だけ @Qualifier で上書きする」という運用にしておくと、新しいメンバーもコードを読むだけで意図を理解できます。

設定やプロファイルで実装を切り替える

決済方法をハードコードせず、設定で切り替えたいなら @ConditionalOnProperty が使えます。

payment:
  default-method: credit-card
@Component
@ConditionalOnProperty(name = "payment.default-method", havingValue = "credit-card")
public class CreditCardPaymentService implements PaymentService { /* ... */ }

@Component
@ConditionalOnProperty(name = "payment.default-method", havingValue = "bank-transfer")
public class BankTransferPaymentService implements PaymentService { /* ... */ }

havingValue の条件は互いに排他的なので、起動時にはどちらか一方のBeanしかコンテナに登録されません。候補がそもそも1つしかないため @Primary は不要です。

開発環境ではモック、本番では実際のAPIクライアントを使いたい場合は @Profile でも同じ考え方が使えます。

@Component
@Profile("dev")
public class MockPaymentGatewayService implements PaymentService { /* ... */ }

@Component
@Profile("prod")
public class RealPaymentGatewayService implements PaymentService { /* ... */ }

spring.profiles.active=dev のように指定するだけで、アクティブなプロファイルに対応するBeanだけが登録され、環境ごとに正しい実装が選ばれます。

まとめ

NoUniqueBeanDefinitionException は、DIコンテナに「どの実装を使うか」を伝えていないだけのエラーです。基本方針として @Primary でデフォルトの実装を決め、個別の箇所だけ @Qualifier で上書きするという運用にしておけば、DIの基本的な考え方の上で複数実装を安全に扱えるようになります。さらに @ConditionalOnProperty やプロファイルと組み合わせれば、環境や設定に応じて実装を切り替える構成も難しくありません。