iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0
Software Development

spring boot 3 學習筆記系列 第 16

Day16 - Spring Boot 依賴注入指南:用 @Primary 與 @Qualifier 解決多實作困境

  • 分享至 

  • xImage
  •  

前言

當您開始使用依賴注入 (Dependency Injection, DI) 時,一定會對 @Autowired 的便利性感到驚艷。然而,當一個介面 (Interface) 有多個實作類別 (Implementation Class) 時,Spring Boot 就會陷入「選擇困難症」。

今日來了解如何使用 @Primary@Qualifier 這兩個強大的註解 (Annotation),優雅地解決這個問題。

1. 問題浮現:NoUniqueBeanDefinitionException

想像一個情境:我們正在開發一個訊息通知系統,需要支援 Email 和簡訊 (SMS) 兩種通知方式。

首先,定義一個 NotificationService 介面:

NotificationService.java

public interface NotificationService {
    String send(String message);
}

接著,我們建立兩個實作類別 EmailServiceImplSmsServiceImpl,並將它們註冊為 Spring 的元件 (Bean)

EmailServiceImpl.java

import org.springframework.stereotype.Service;

@Service("emailNotification") // 給這個 Bean 一個明確的名字 "emailNotification"
public class EmailServiceImpl implements NotificationService {
    @Override
    public String send(String message) {
        System.out.println("正在透過 Email 發送...");
        return "Email sent with message: " + message;
    }
}

SmsServiceImpl.java

import org.springframework.stereotype.Service;

@Service("smsNotification") // 同樣地,給它一個名字 "smsNotification"
public class SmsServiceImpl implements NotificationService {
    @Override
    public String send(String message) {
        System.out.println("正在透過簡訊發送...");
        return "SMS sent with message: " + message;
    }
}

現在,問題來了。當我們在 Controller 中嘗試注入 NotificationService 時:

NotificationController.java (錯誤的範例)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class NotificationController {

    private final NotificationService notificationService;

    @Autowired // 錯誤發生點!
    public NotificationController(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    @GetMapping("/notify")
    public String sendNotification() {
        return notificationService.send("Hello, World!");
    }
}

當您啟動應用程式時,Spring 會在終端機噴出一段錯誤訊息,其中最關鍵的一行是 NoUniqueBeanDefinitionException

為什麼會出錯?
Spring 發現有兩個符合 NotificationService 型別的 Bean (emailNotificationsmsNotification),它不知道您到底想要哪一個,於是只好放棄並報錯。

接下來,我們來看看如何解決這個問題。

2. 解決方案一:@Primary - 設定預設選項

@Primary 是最簡單的解決方案。它就像是告訴 Spring:「如果有多個選擇,而且沒有人特別指定要哪一個時,請優先選擇我!」

這好比餐廳套餐裡的「預設飲料」是紅茶,您不特別說,店家就自動給您紅茶。

讓我們將 EmailServiceImpl 設為預設的通知方式:

EmailServiceImpl.java (使用 @Primary)

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

@Service("emailNotification")
@Primary // 就是這一行!告訴 Spring 這是首選
public class EmailServiceImpl implements NotificationService {
    @Override
    public String send(String message) {
        System.out.println("正在透過 Email 發送...");
        return "Email sent with message: " + message;
    }
}

現在,您不需要修改 NotificationController 的任何程式碼,直接重新啟動應用程式,它就能正常運作了!Spring 會因為 @Primary 的存在,自動注入 EmailServiceImpl

3. 解決方案二:@Qualifier - 精準指定

@Primary 很好用,但如果我們在某個地方不想用預設的 Email,而是想用簡訊 (SMS) 來發送通知呢?這時 @Qualifier 就要登場了。

@Qualifier 的作用就像是「指定點餐」。您明確告訴 Spring:「我不要預設的,我指定要名稱為『這個』的 Bean。」

@Qualifier 如何對應到 Bean?

@Qualifier 是透過 Bean 的名稱 來識別的。這個名稱有兩種來源:

  1. 手動指定:如我們範例中的 @Service("smsNotification"),我們手動將 Bean 命名為 smsNotification
  2. 預設名稱:如果您不手動指定,Spring 會使用類別名稱的首字母小寫作為預設名稱。例如,SmsServiceImpl 的預設 Bean 名稱就是 smsServiceImpl

@Qualifier 的三種注入方法比較

現在,讓我們修改 Controller,明確指定要注入 smsNotification

建構子注入 (Constructor Injection) - 官方推薦

@Qualifier 放在建構子的參數前面。

NotificationController.java (修正後)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; // 記得 import
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class NotificationController {

    private final NotificationService notificationService;

    @Autowired
    public NotificationController(@Qualifier("smsNotification") NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    @GetMapping("/notify")
    public String sendNotification() {
        return notificationService.send("這是一則來自 SMS 的重要通知!");
    }
}

在這個例子中,即使 EmailServiceImpl 標有 @Primary,Spring 也會聽從 @Qualifier 的指令,精準地注入 SmsServiceImpl

屬性注入 (Field Injection)

雖然方便,但因其缺點(不易測試、隱藏依賴),較不建議在正式專案中使用。

@RestController
public class NotificationController {

    @Autowired
    @Qualifier("smsNotification")
    private NotificationService notificationService;

    // ...
}

Setter 注入 (Setter Injection)

適用於可選的依賴,或解決循環依賴問題。

@RestController
public class NotificationController {

    private NotificationService notificationService;

    @Autowired
    public void setNotificationService(@Qualifier("smsNotification") NotificationService notificationService) {
        this.notificationService = notificationService;
    }
    
    // ...
}

4. @Primary vs @Qualifier 大對決

讓我們用生動的「餐廳點餐比喻」來做個總結,幫助您徹底理解兩者的區別。

想像您走進一家餐廳 (Spring 容器),餐廳提供多種飲料 (多個同型別的 Bean)。

@Primary = 餐廳的「今日推薦」

  • 您對服務生說:「給我一杯飲料。」 (您使用 @Autowired 但未指定)
  • 服務生會自動送上菜單上的「今日推薦」,例如奶茶
  • 「今日推薦」就是標示了 @Primary 的那個 Bean。它是一個被動的預設選項

@Qualifier = 您「指定點餐」

  • 您直接跟服務生說:「我要檸檬紅茶!」 (您使用了 @Qualifier("lemonTea"))
  • 服務生會完全忽略今日推薦是什麼,精準地為您送上檸檬紅茶。
  • @Qualifier 是一個主動的、精確的指令

比喻對照表

情境 Spring 對應 行為
沒特別說 → 上今日推薦 只有 @Autowired,且其中一個 Bean 有 @Primary 注入標有 @Primary 的 Bean
沒特別說 → 服務生困惑 只有 @Autowired,沒有 @Primary,但有多個選項 拋出 NoUniqueBeanDefinitionException 錯誤
指定要檸檬紅茶 使用 @Autowired 搭配 @Qualifier("lemonTea") 精準注入名為 "lemonTea" 的 Bean
今日推薦是奶茶,但我指定要檸檬紅茶 @PrimarymilkTea 上,但注入點使用 @Qualifier("lemonTea") @Qualifier 優先級更高,注入 "lemonTea"

相關資料來源


上一篇
Day15 - Spring Boot 依賴注入指南:@Autowired 的三種注入方法比較
系列文
spring boot 3 學習筆記16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言