### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本文是《quarkus依賴註入》系列的第 ...
歡迎訪問我的GitHub
這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos
本篇概覽
- 本文是《quarkus依賴註入》系列的第四篇,在應用中,一個介面有多個實現是很常見的,那麼依賴註入時,如果類型是介面,如何準確選擇實現呢?前文介紹了五種註解,用於通過配置項、profile等手段選擇註入介面的實現類,面對複雜多變的業務場景,有時候僅靠這兩種手段是不夠的,最好是有更自由靈活的方式來選擇bean,這就是本篇的內容,通過註解、編碼等更多方式選擇bean
- 本篇涉及的選擇bean的手段有以下四種:
- 修飾符匹配
- Named註解的屬性匹配
- 根據優先順序選擇
- 寫代碼選擇
關於修飾符匹配
- 為了說明修飾符匹配,先來看一個註解Default,其源碼如下
@Target({ TYPE, METHOD, PARAMETER, FIELD })
@Retention(RUNTIME)
@Documented
@Qualifier
public @interface Default {
public static final class Literal extends AnnotationLiteral<Default> implements Default {
public static final Literal INSTANCE = new Literal();
private static final long serialVersionUID = 1L;
}
}
- Default的源碼在這裡不重要,關鍵是它被註解Qualifier修飾了,這種被Qualifier修飾的註解,咱們姑且稱之為Qualifier修飾符
- 如果咱們新建一個註解,也用Qualifier來修飾,如下所示,這個MyQualifier也是個Qualifier修飾符
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface MyQualifier {
@Nonbinding String value();
}
- 在quarkus容器中的每一個bean都應該有一個Qualifier修飾符在修飾,如下圖紅框,如果沒有,就會被quarkus添加Default註解
- 依賴註入時,直接用Qualifier修飾符修飾註入對象,這樣quarkus就會去尋找被這個Qualifier修飾符修飾的bean,找到就註入(找不到報錯,找到多個也報錯,錯誤邏輯和之前的一樣)
- 所以用修飾符匹配來選擇bean的實現類,一共分三步:
- 假設有名為HelloQualifier的介面,有三個實現類:HelloQualifierA、HelloQualifierB、HelloQualifierC,業務需求是使用HelloQualifierA
- 第一步:自定義一個註解,假設名為MyQualifier,此註解要被Qualifier修飾
- 第二步:用MyQualifier修飾HelloQualifierA
- 第三步:在業務代碼的註入點,用MyQualifier修飾HelloQualifier類型的成員變數,這樣成員變數就會被註入HelloQualifierA實例
- 僅憑文字描述,很難把信息準確傳遞給讀者(畢竟欣宸文化水平極其有限),還是寫代碼實現上述場景吧,聰明的您一看就懂
編碼演示修飾符匹配:準備工作
-
先按照前面的假設將介面和實現類準備好,造成一個介面有多個實現bean的事實,然後,再用修飾符匹配來準確選定bean
-
首先是介面HelloQualifier,如下所示
package com.bolingcavalry.service;
public interface HelloQualifier {
String hello();
}
- 實現類HelloQualifierA,返回自己的類名
package com.bolingcavalry.service.impl;
import com.bolingcavalry.service.HelloQualifier;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class HelloQualifierA implements HelloQualifier {
@Override
public String hello() {
return this.getClass().getSimpleName();
}
}
- 實現類HelloQualifierB、HelloQualifierC的代碼和上面的HelloQualifierA相同,都是返回自己類名,就不貼出來了
- 關於使用HelloQualifier類型bean的代碼,咱們就在單元測試類中註入吧,如下所示:
package com.bolingcavalry;
import com.bolingcavalry.service.HelloQualifier;
import com.bolingcavalry.service.impl.HelloQualifierA;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import javax.inject.Inject;
@QuarkusTest
public class QualifierTest {
@Inject
HelloQualifier helloQualifier;
@Test
public void testQualifier() {
Assertions.assertEquals(HelloQualifierA.class.getSimpleName(),
helloQualifier.hello());
}
}
- 上面的代碼中,成員變數helloQualifier的類型是HelloQualifier,quarkus的bean容器中,HelloQualifierA、HelloQualifierB、HelloQualifierC等三個bean都符合註入要求,此時如果執行單元測試,應該會報錯:同一個介面多個實現bean的問題
- 執行單元測試,如下圖,黃框中給出了兩個線索:第一,錯誤原因是註入時發現同一個介面有多個實現bean,第二,這些bean都是用Default修飾的,然後是綠框,裡面將所有實現bean列出來,方便開發者定位問題
- 現在準備工作完成了,來看如何用修飾符匹配解決問題:在註入點準確註入HelloQualifierA類型實例
編碼演示修飾符匹配:實現匹配
- 使用修飾符匹配,繼續按照前面總結的三步走
- 第一步:自定義一個註解,名為MyQualifier,此註解要被Qualifier修飾
package com.bolingcavalry.annonation;
import javax.enterprise.util.Nonbinding;
import javax.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface MyQualifier {
@Nonbinding String value();
}
-
第二步:用MyQualifier修飾HelloQualifierA,下圖紅框是新增的代碼
-
第三步:在業務代碼的註入點,用MyQualifier修飾HelloQualifier類型的成員變數,下圖紅框是新增的代碼
-
改動完成了,再次執行單元測試,順利通過
修飾符匹配要註意的地方
- 修飾符匹配的邏輯非常簡單:bean定義和bean註入的地方用同一個修飾符即可,使用中有三個地方要註意
- 在註入bean的地方,如果有了Qualifier修飾符,可以把@Inject省略不寫了
- 在定義bean的地方,如果沒有Qualifier修飾符去修飾bean,quarkus會預設添加Default
- 在註入bean的地方,如果沒有Qualifier修飾符去修飾bean,quarkus會預設添加Default
關於預設的@Default
-
回頭看剛纔的代碼,如果保留HelloQualifierA的MyQualifier修飾,但是刪除QualifierTest的成員變數helloQualifier的MyQualifier修飾,會發生什麼呢?咱們來分析一下:
-
首先,QualifierTest的成員變數helloQualifier會被quarkus預設添加Default修飾
-
其次,HelloQualifierB和HelloQualifierC都會被quarkus預設添加Default修飾
-
所以,註入helloQualifier的時候,quarkus去找Default修飾的bean,結果找到了兩個:HelloQualifierB和HelloQualifierC,因此啟動會失敗
-
您可以自行驗證結果是否和預期一致
-
看到這裡,您應該掌握了修飾符匹配的用法,也應該發現其不便之處:要新增註解,這樣下去隨著業務發展,註解會越來越多,有沒有什麼方法來解決這個問題呢?
-
方法是有的,就是接下來要看的Named註解
Named註解的屬性匹配
-
Named註解的功能與前面的Qualifier修飾符是一樣的,其特殊之處在於通過註解屬性來匹配修飾bean和註入bean
-
以剛纔的業務代碼為例來演示Named註解,修改HelloQualifierA,如下圖紅框,將@MyQualifier("")換成@Named("A"),重點關註Named註解的屬性值,這裡等於A
-
接下來修改註入處的代碼,如下圖紅框,在註入位置也用@Named("A")來修飾,和bean定義處的一模一樣
-
如此,bean定義和bean註入的兩個地方,通過Named註解的屬性完成了匹配,至於單元測試您可以自行驗證,這裡就不贅述了
-
至此,詳細您已經知道了Named註解的作用:功能與前面的Qualifier修飾符一樣,不過bean的定義和註入處的匹配邏輯是Named註解的屬性值
-
以上就是修飾符匹配的全部內容
根據優先順序選擇
-
使用優先順序來選擇註入是一種簡潔的方式,其核心是用Alternative和Priority兩個註解修飾所有備選bean,然後用Priority的屬性值(int型)作為優先順序,該值越大代表優先順序越高
-
在註入位置,quarkus會選擇優先順序最高的bean註入
-
接下來編碼演示
-
新增演示用的介面HelloPriority.java
public interface HelloPriority {
String hello();
}
- HelloPriority的第一個實現類HelloPriorityA.java,註意它的兩個註解Alternative和Priority,前者表明這是個可供選擇的bean,後者表明瞭它的優先順序,數字1001用於和其他bean的優先順序比較,數字越大優先順序越高
@ApplicationScoped
@Alternative
@Priority(1001)
public class HelloPriorityA implements HelloPriority {
@Override
public String hello() {
return this.getClass().getSimpleName();
}
}
- HelloPriority的第二個實現類HelloPriorityB,可見Priority屬性值是1002,代表選擇的時候優先順序比HelloPriorityA更高
@ApplicationScoped
@Alternative
@Priority(1002)
public class HelloPriorityB implements HelloPriority {
@Override
public String hello() {
return this.getClass().getSimpleName();
}
}
- HelloPriority的第二個實現類HelloPriorityC,可見Priority屬性值是1003,代表選擇的時候優先順序比HelloPriorityA和HelloPriorityB更高
@ApplicationScoped
@Alternative
@Priority(1003)
public class HelloPriorityC implements HelloPriority {
@Override
public String hello() {
return this.getClass().getSimpleName();
}
}
- 接下來是單元測試,驗證註入的bean是否符合預期,理論上註入的應該是優先順序最高的HelloPriorityC
@QuarkusTest
public class PriorityTest {
@Inject
HelloPriority helloPriority;
@Test
public void testSelectHelloInstanceA() {
Assertions.assertEquals(HelloPriorityC.class.getSimpleName(),
helloPriority.hello());
}
}
- 單元測試結果如下,符合預期
- 以上就是優先順序選擇bean的操作,如果這還不夠用,那就祭出最後一招:寫代碼選擇bean
寫代碼選擇bean
- 如果不用修飾符匹配,再回到最初的問題:有三個bean都實現了同一個介面,應該如何註入?
- 在註入bean的位置,如果用Instance<T>來接收註入,就可以拿到T類型的所有bean,然後在代碼中隨心所欲的使用這些bean
- 新增演示用的介面HelloInstance.java
package com.bolingcavalry.service;
public interface HelloInstance {
String hello();
}
- HelloInstance的第一個實現類HelloInstanceA.java
package com.bolingcavalry.service.impl;
import com.bolingcavalry.service.HelloInstance;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class HelloInstanceA implements HelloInstance {
@Override
public String hello() {
return this.getClass().getSimpleName();
}
}
- HelloInstance的另外兩個實現類HelloInstanceB、HelloInstanceC,代碼與HelloInstanceA一樣,就不貼出來了
- 接下來的單元測試類演示瞭如何使用Instance接受註入,以及業務代碼如何使用指定的實現類bean,可見select(Class).get()是關鍵,select方法指定了實現類,然後get取出該實例
package com.bolingcavalry;
import com.bolingcavalry.service.HelloInstance;
import com.bolingcavalry.service.impl.HelloInstanceA;
import com.bolingcavalry.service.impl.HelloInstanceB;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
@QuarkusTest
public class InstanceTest {
@Inject
Instance<HelloInstance> instance;
@Test
public void testSelectHelloInstanceA() {
Class<HelloInstanceA> clazz = HelloInstanceA.class;
Assertions.assertEquals(clazz.getSimpleName(),
instance.select(clazz).get().hello());
}
@Test
public void testSelectHelloInstanceB() {
Class<HelloInstanceB> clazz = HelloInstanceB.class;
Assertions.assertEquals(clazz.getSimpleName(),
instance.select(clazz).get().hello());
}
}
-
執行單元測試,順利通過,符合預期
-
至此,連續兩篇關於註入bean的方式全部驗證完畢,如此豐富的手段,相信可以滿足您日常開發的需要