Day 8 - 指定注入的 Bean - @Qualifier

古古

2024/07/08


哈囉大家好,我是古古。

在上一篇文章中,我們介紹了如何使用 @Component 來創建 Bean,也有介紹要如何使用 @Autowired 來注入 Bean。

那麼接著這篇文章,我們就會來介紹,當 Spring 容器中有 2 個以上同樣類型的 Bean 存在時,該怎麼去選擇要注入的 Bean。

回顧:注入 Bean 的方法 - @Autowired #

在前一篇文章中我們有介紹到,我們可以在變數上加上 @Autowired,將想要的 Bean 給注入進來。

不過在使用 @Autowired 來注入 Bean 時,必須滿足以下事項:

  • 首先必須要確保 「自己也是一個 Bean」(即是有在 class 上面加上 @Component)。
  • 並且 @Autowired 是透過 「變數的類型」 來注入 Bean。

所以在使用 @Autowired 去注入 Bean 進來時,Spring Boot 就是會透過「變數的類型」,去 Spring 容器中尋找是否有類型符合的 Bean,如果有同類型的 Bean 存在時,即可以注入成功,如果沒有 Bean 存在,則注入失敗,Spring Boot 就會報錯,並且運行失敗。

所以像是在下面的例子中,因為 Spring 容器中只有 hpPrinter 這個 Bean,並且 hpPrinter 可以向上轉型成 Printer 類型,所以 Spring Boot 就會判定他們類型符合,因此就能夠注入成功。

但是,假設在 Spring 容器中,同時有兩個一樣類型的 Bean 存在,譬如說像是下面這張圖,在 Spring 容器中同時有 hpPrinter 和 canonPrinter 這兩個 Bean 存在,那麼在這個情況下,Spring Boot 會如何運作呢?

正確答案是:Spring Boot 會出現錯誤並且運行失敗。

而 Spring Boot 之所以會出現錯誤,就是因為 hpPrinter 和 canonPrinter 都可以向上轉型成 Printer 類型,所以 Spring Boot 不知道該注入哪一個 Bean 進來,因此就發生錯誤。

所以會導致這個錯誤的根本原因,就是因為在 Spring 容器中「同時有多個同樣類型的 Bean 存在」,因此 Spring Boot 就無法選擇要注入哪一個 Bean 進來。

所以為了解決這個問題,就是 @Qualifier 登場的時候了!

指定注入的 Bean 的名字:@Qualifier #

@Qualifier 的用途,是去指定要注入的 Bean 的「名字」是什麼,進而解決同時有兩個同樣類型的 Bean 存在的問題。

因此在一般的情況下,我們只要使用 @Autowired 就可以注入 Bean,但是假設今天有兩個同樣類型的 Bean 存在時,那麼我們在使用 @Autowired 的時候,就必須同時去搭配 @Qualifier,才能夠去選擇要注入的 Bean 是哪一個。

所以簡單來說,Spring Boot 就是先由 @Autowired 篩選 Bean 的類型、再由 @Qualifier 篩選 Bean 的名字,透過這樣子的連環組合拳來解決這個問題!

因此如果我們回頭看剛剛的例子,假設目前在 Spring 容器中有兩個 Bean:hpPrinter 和 canonPrinter,這時候如果我們想要指定要注入 hpPrinter 這個 Bean 的話,那麼就只要在 printer 變數上面,再加上一個 @Qualifier,並且在 @Qualifier 裡面指定「要注入的 Bean 的名字是hpPrinter」,這樣子就可以成功的注入 hpPrinter Bean 進來了!

使用 @Qualifer 的注意事項之一:必須搭配 @Autowired 一起使用 #

在使用 @Qualifier 去指定「要注入的 Bean」時,一定要搭配 @Autowired 一起使用,單純使用 @Qualifier 是沒有任何用處的。

其實大家也可以直接把 @Qualifier 當成是 @Autowired 的小弟看待,@Qualifier 只是專門在輔助 @Autowired 的,如果沒有 @Autowired 的話,那麼 @Qualifier 是完全沒有什麼作用的!

使用 @Qualifer 的注意事項之二:指定的是「Bean 的名字」 #

如同前面所介紹的,@Qualifier 是為了解決「多個類型同時存在」的問題被發明出來的,因此他所指定的是 「Bean 的名字」。 也由於 @Qualifier 指定的是 Bean 的名字,因此掌握 Bean 的名字的生成方式就非常的重要!

當我們平常使用 @Component 去創建 Bean 時,這些 Bean 的名字,就會是「class 名的第一個字母轉成小寫」。 所以像是由 HpPrinter class 所生成的 Bean,就會叫做 hpPrinter,由 CanonPrinter 所生成的 Bean,名字就會叫做 canonPrinter。

因此大家在使用 @Qualifier 去指定要注入的 Bean 的名字時,一定要撰寫正確的 Bean 的名字,這樣子才能夠成功的去注入該 Bean 進來。

補充:有關 Bean 的名字生成機制,可以回頭參考 Day 7 - Bean 的創建和注入 - @Component、@Autowired 的相關介紹

在 Spring Boot 中練習 @Qualifier 的用法 #

了解了 @Qualifier 的用法之後,我們也可以實際到 Spring Boot 中,來練習 @Qualifier 的用法。

延續上一篇文章的程式,目前在 Spring Boot 程式中,我們已經創建了 Printer interface 以及 HpPrinter class 出來,並且在 MyController 裡面,我們也使用了 @Autowired 去注入一個 Printer 類型的 Bean 進來。

由於目前在這個 Spring Boot 程式裡面,只有一個 hpPrinter Bean 有辦法向上轉型成 Printer 類型,因此在 MyController 這裡所注入的,就會是 hpPrinter 這個 Bean。

如果要練習 @Qualifier 的用法的話,我們可以先在 com.example.demo 這個 package 底下,再去新增一個 CanonPrinter 的 class 出來,並且在這個 CanonPrinter 的 class 中,實作以下的程式:

@Component
public class CanonPrinter implements Printer {

    @Override
    public void print(String message) {
        System.out.println("Canon印表機: " + message);
    }
}

創建好 CanonPrinter class 之後,整個結構會長得像是下面這個樣子:

所以當我們這樣寫之後,就等於是 HpPrinter 和 CanonPrinter 這兩個 class,到時候都會被 Spring Boot 所管理,所以 Spring Boot 到時候就會各 new 出一份 bean,並且存放在 Spring 容器中。

創建好 CanonPrinter 之後,這時候我們回到 MyController 上面來看一下的話,就會發現在第 11 行的 printer 變數下面,出現了一個紅色的波浪線。

這是因為 IntelliJ 會主動檢查我們所寫的程式,並偵測出這裡到時候會出現注入的問題(Spring 容器同時存在多個同類型的 Bean),因此 IntelliJ 就提前出現了紅色的波浪線,提示我們此處有錯誤。

如果我們不管這個紅色波浪線,執意要運行 Spring Boot 程式的話,那麼在啟動 Spring Boot 的過程中,console 就會噴出下列的錯誤訊息,提示我們啟動 Spring Boot 失敗。

在上面這段 console 的錯誤訊息中,有出現一句「MyController required a single bean, but 2 were found」的訊息,這一行的訊息就是表示「MyController 想要注入一個 Bean,但是卻發現 Spring 容器中存在兩個同樣類型的 Bean,導致 MyController 不知道要選擇哪一個 Bean 注入,因此才導致注入失敗」。

並且在下方的紅框處,Spring Boot 也有建議我們使用 @Qualifier,來解決這個「多個同樣類型的 Bean 同時存在」的問題。

所以要解決這個問題的話,就是 @Qualifier 出場的時候了!

因此我們可以回到 MyController 上,然後在 printer 變數上面,去加上一行 @Qualifier("canonPrinter") 的程式,表示我們想要注入的 Bean,是「名字為 canonPrinter」的那個 Bean。

當我們這樣寫之後,到時候 Spring Boot 在注入 Bean 時,就會從 Printer 類型的 Bean 中,選出名字為 canonPrinter 的那個 Bean,然後把他注入到 MyController 裡面了!

補充:因為當我們使用 @Component 去創建 Bean 時,這個 Bean 的名字就會是「class 名稱的第一個字母轉成小寫」,因此 CanonPrinter 所產生的 Bean,名字即是 canonPrinter(注意第一個字母為小寫)。

運行 Spring Boot 程式 #

寫好上述程式之後,我們可以重新運行 Spring Boot 程式,來看一下效果。

運行起來之後,當看到下方的 console 出現「Started DemoApplication in 0.663 seconds」時,就表示 Spring Boot 程式運行成功了。

接著我們可以打開 Google 瀏覽器,然後在裡面輸入 http://localhost:8080/test ,再按下 Enter 鍵。

這時候如果頁面中有呈現「Hello World」的字樣的話,就表示請求成功了,所以我們可以回到 IntelliJ 上來看一下結果。

這時回到 IntelliJ 上,就可以在 console 下方看到一行「Canon印表機: Hello World」的輸出。

而這裡之所以會出現「Canon印表機: Hello World」這一行輸出,就是因為我們在 MyController 中加上了第 12 行的 @Qualifier("canonPrinter")而這一行 @Qualifier("canonPrinter") 所代表的意思,就是去指定「要去注入名字為 canonPrinter 的那個 Bean」。

因此 Spring Boot 到時候就會將 canonPrinter 注入到 MyController 的 printer 變數裡面,所以後續執行到第 17 行的 printer.print("Hello World") 時,實際上就是去執行 canonPrinter 的 print() 方法,所以最後才會在 console 上輸出「Canon印表機: Hello World」的訊息。

所以只要大家在 console 上看到「Canon印表機: Hello World」這一行輸出,就表示我們成功的透過 @Qualifier,去指定要注入哪一個 Bean 進來了!

補充:大家也可以試著把 MyController 的第 17 行修改一下,多去體驗一下 @Qualifier 的用法。譬如說可以把他改成 @Qualifier("hpPrinter"),就表示要改成去注入 hpPrinter 那個 Bean 進來,因此到時候在 console 所上輸出的,就會變成是「HP印表機: Hello World」。

總結 #

這篇文章我們介紹了要如何透過 @Qualifier,去指定要注入的 Bean 的名字,進而去輔助 @Autowired 透過變數的類型注入 Bean 所引發的衍生問題,並且我們也有實際到 Spring Boot 中練習了 @Qualifier 的用法,讓大家感受一下 @Qualifier 的效果為何。

那麼在介紹完 Bean 的創建和注入之後,接著我們會來介紹「Bean 的初始化」,這也是在實戰中非常常用到的用法,那我們就下一篇文章見啦!

補充:本文是擷取自我開設的線上課程 「Java 工程師必備!Spring Boot 零基礎入門」 的內容,如果你想了解更多的 Spring Boot 的用法,歡迎參考課程簡介 (輸入折扣碼「HH202504KU」即可享 85 折優惠)。