當您開始使用依賴注入 (Dependency Injection, DI) 時,一定會對 @Autowired
的便利性感到驚艷。然而,當一個介面 (Interface) 有多個實作類別 (Implementation Class) 時,Spring Boot 就會陷入「選擇困難症」。
今日來了解如何使用 @Primary
與 @Qualifier
這兩個強大的註解 (Annotation),優雅地解決這個問題。
NoUniqueBeanDefinitionException
想像一個情境:我們正在開發一個訊息通知系統,需要支援 Email 和簡訊 (SMS) 兩種通知方式。
首先,定義一個 NotificationService
介面:
NotificationService.java
public interface NotificationService {
String send(String message);
}
接著,我們建立兩個實作類別 EmailServiceImpl
和 SmsServiceImpl
,並將它們註冊為 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 (emailNotification
和 smsNotification
),它不知道您到底想要哪一個,於是只好放棄並報錯。
接下來,我們來看看如何解決這個問題。
@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
。
@Qualifier
- 精準指定@Primary
很好用,但如果我們在某個地方不想用預設的 Email,而是想用簡訊 (SMS) 來發送通知呢?這時 @Qualifier
就要登場了。
@Qualifier
的作用就像是「指定點餐」。您明確告訴 Spring:「我不要預設的,我指定要名稱為『這個』的 Bean。」
@Qualifier
如何對應到 Bean?@Qualifier
是透過 Bean 的名稱 來識別的。這個名稱有兩種來源:
@Service("smsNotification")
,我們手動將 Bean 命名為 smsNotification
。SmsServiceImpl
的預設 Bean 名稱就是 smsServiceImpl
。@Qualifier
的三種注入方法比較現在,讓我們修改 Controller
,明確指定要注入 smsNotification
。
將 @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
。
雖然方便,但因其缺點(不易測試、隱藏依賴),較不建議在正式專案中使用。
@RestController
public class NotificationController {
@Autowired
@Qualifier("smsNotification")
private NotificationService notificationService;
// ...
}
適用於可選的依賴,或解決循環依賴問題。
@RestController
public class NotificationController {
private NotificationService notificationService;
@Autowired
public void setNotificationService(@Qualifier("smsNotification") NotificationService notificationService) {
this.notificationService = notificationService;
}
// ...
}
@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 |
今日推薦是奶茶,但我指定要檸檬紅茶 | @Primary 在 milkTea 上,但注入點使用 @Qualifier("lemonTea") |
@Qualifier 優先級更高,注入 "lemonTea" |