### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 關於bean的作用域(scope) - 官方資料:ht ...
歡迎訪問我的GitHub
這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos
關於bean的作用域(scope)
-
官方資料:https://lordofthejars.github.io/quarkus-cheat-sheet/#_injection
-
作為《quarkus依賴註入》系列的第二篇,繼續學習一個重要的知識點:bean的作用域(scope),每個bean的作用域是唯一的,不同類型的作用域,決定了各個bean實例的生命周期,例如:何時何處創建,又何時何處銷毀
-
bean的作用域在代碼中是什麼樣的?回顧前文的代碼,如下,ApplicationScoped就是作用域,表明bean實例以單例模式一直存活(只要應用還存活著),這是業務開發中常用的作用域類型:
@ApplicationScoped
public class ClassAnnotationBean {
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
- 作用域有多種,如果按來源區分一共兩大類:quarkus內置和擴展組件中定義,本篇聚焦quarkus的內置作用域
- 下麵是整理好的作用域一覽,接下來會逐個講解
常規作用域和偽作用域
- 常規作用域,quarkus官方稱之為normal scope,包括:ApplicationScoped、RequestScoped、SessionScoped三種
- 偽作用域稱之為pseudo scope,包括:Singleton、RequestScoped、Dependent兩種
- 接下來,用一段最平常的代碼來揭示常規作用域和偽作用域的區別
- 下麵的代碼中,ClassAnnotationBean的作用域ApplicationScoped就是normal scope,如果換成Singleton就是pseudo scope了
@ApplicationScoped
public class ClassAnnotationBean {
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
- 再來看使用ClassAnnotationBean的代碼,如下所示,是個再平常不過的依賴註入
@Path("/classannotataionbean")
public class ClassAnnotationController {
@Inject
ClassAnnotationBean classAnnotationBean;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String get() {
return String.format("Hello RESTEasy, %s, %s",
LocalDateTime.now(),
classAnnotationBean.hello());
}
}
- 現在問題來了,ClassAnnotationBean是何時被實例化的?有以下兩種可能:
-
第一種:ClassAnnotationController被實例化的時候,classAnnotationBean會被註入,這時ClassAnnotationBean被實例化
-
第二種:get方法第一次被調用的時候,classAnnotationBean真正發揮作用,這時ClassAnnotationBean被實例化
- 所以,一共有兩個時間點:註入時和get方法首次執行時,作用域不同,這兩個時間點做的事情也不同,下麵用表格來解釋
時間點 | 常規作用域 | 偽作用域 |
---|---|---|
註入的時候 | 註入的是一個代理類,此時ClassAnnotationBean並未實例化 | 觸發ClassAnnotationBean實例化 |
get方法首次執行的時候 | 1. 觸發ClassAnnotationBean實例化 2. 執行常規業務代碼 |
1. 執行常規業務代碼 |
- 至此,您應該明白兩種作用域的區別了:偽作用域的bean,在註入的時候實例化,常規作用域的bean,在註入的時候並未實例化,只有它的方法首次執行的時候才會實例化,如下圖
- 接下來細看每個作用域
ApplicationScoped
- ApplicationScoped算是最常用的作用域了,它修飾的bean,在整個應用中只有一個實例
RequestScoped
- 這是與當前http請求綁定的作用域,它修飾的bean,在每次http請求時都有一個全新實例,來寫一段代碼驗證
- 首先是bean類RequestScopeBean.java,註意作用域是RequestScoped,如下,在構造方法中列印日誌,這樣可以通過日誌行數知道實例化次數
package com.bolingcavalry.service.impl;
import io.quarkus.logging.Log;
import javax.enterprise.context.RequestScoped;
@RequestScoped
public class RequestScopeBean {
/**
* 在構造方法中列印日誌,通過日誌出現次數對應著實例化次數
*/
public RequestScopeBean() {
Log.info("Instance of " + this.getClass().getSimpleName());
}
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
- 然後是使用bean的代碼,是個普通的web服務類
package com.bolingcavalry;
import com.bolingcavalry.service.impl.RequestScopeBean;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.time.LocalDateTime;
@Path("/requestscope")
public class RequestScopeController {
@Inject
RequestScopeBean requestScopeBean;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String get() {
return String.format("Hello RESTEasy, %s, %s",
LocalDateTime.now(),
requestScopeBean.hello());
}
}
- 最後是單元測試代碼RequestScopeControllerTest.java,要註意的是註解RepeatedTest,有了此註解,testGetEndpoint方法會重覆執行,次數是註解的value屬性值,這裡是10次
package com.bolingcavalry;
import com.bolingcavalry.service.impl.RequestScopeBean;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.containsString;
@QuarkusTest
class RequestScopeControllerTest {
@RepeatedTest(10)
public void testGetEndpoint() {
given()
.when().get("/requestscope")
.then()
.statusCode(200)
// 檢查body內容,是否含有ClassAnnotationBean.hello方法返回的字元串
.body(containsString("from " + RequestScopeBean.class.getSimpleName()));
}
}
- 由於單元測試中介面會調用10次,按照RequestScoped作用域的定義,RequestScopeBean會實例化10次,執行單元測試試試吧
- 執行結果如下圖,紅框4顯示每次http請求都會觸發一次RequestScopeBean實例化,符合預期,另外還有意外收穫,稍後馬上就會提到
- 另外,請重點關註藍框和藍色註釋文字,這是意外收穫,居然看到了代理類的日誌,看樣子代理類是繼承了RequestScopeBean類,於是父類構造方法中的日誌代碼也執行了,還把代理類的類名列印出來了
- 從日誌可以看出:10次http請求,bean的構造方法執行了10次,代理類的構造方法只執行了一次,這是個重要結論:bean類被多次實例化的時候,代理類不會多次實例化
SessionScoped
- SessionScoped與RequestScoped類似,區別是範圍,RequestScoped是每次http請求做一次實例化,SessionScoped是每個http會話,以下場景都在session範圍內,共用同一個bean實例:
- servlet的service方法
- servlet filter的doFileter方法
- web容器調用HttpSessionListener、AsyncListener、ServletRequestListener等監聽器
Singleton
-
提到Singleton,聰明的您是否想到了單例模式,這個scope也是此意:它修飾的bean,在整個應用中只有一個實例
-
Singleton和ApplicationScoped很像,它們修飾的bean,在整個應用中都是只有一個實例,然而它們也是有區別的:ApplicationScoped修飾的bean有代理類包裹,Singleton修飾的bean沒有代理類
-
Singleton修飾的bean沒有代理類,所以在使用的時候,對bean的成員變數直接讀寫都沒有問題(safely),而ApplicationScoped修飾的bean,請不要直接讀寫其成員變數,比較拿都是代理的東西,而不是bean的類自己的成員變數
-
Singleton修飾的bean沒有代理類,所以實際使用中性能會略好(slightly better performance)
-
在使用QuarkusMock類做單元測試的時候,不能對Singleton修飾的bean做mock,因為沒有代理類去執行相關操作
-
quarkus官方推薦使用的是ApplicationScoped
-
Singleton被quarkus劃分為偽作用域,此時再回頭品味下圖,您是否恍然大悟:成員變數classAnnotationBean如果是Singleton,是沒有代理類的,那就必須在@Inject位置實例化,否則,在get方法中classAnnotationBean就是null,會空指針異常的
-
運行代碼驗證是否有代理類,找到剛纔的RequestScopeBean.java,將作用域改成Singleton,運行單元測試類RequestScopeControllerTest.java,結果如下圖紅框,只有RequestScopeBean自己構造方法的日誌
-
再將作用域改成ApplicationScoped,如下圖藍框,代理類日誌出現
Dependent
- Dependent是個偽作用域,它的特點是:每個依賴註入點的對象實例都不同
- 假設DependentClinetA和DependentClinetB都用@Inject註解註入了HelloDependent,那麼DependentClinetA引用的HelloDependent對象,DependentClinetB引用的HelloDependent對象,是兩個實例,如下圖,兩個hello是不同的實例
Dependent的特殊能力
- Dependent的特點是每個註入點的bean實例都不同,針對這個特點,quarkus提供了一個特殊能力:bean的實例中可以取得註入點的元數據
- 對應上圖的例子,就是HelloDependent的代碼中可以取得它的使用者:DependentClientA和DependentClientB的元數據
- 寫代碼驗證這個特殊能力
- 首先是HelloDependent的定義,將作用域設置為Dependent,然後註意其構造方法的參數,這就是特殊能力所在,是個InjectionPoint類型的實例,這個參數在實例化的時候由quarkus容器註入,通過此參數即可得知使用HelloDependent的類的身份
@Dependent
public class HelloDependent {
public HelloDependent(InjectionPoint injectionPoint) {
Log.info("injecting from bean "+ injectionPoint.getMember().getDeclaringClass());
}
public String hello() {
return this.getClass().getSimpleName();
}
}
- 然後是HelloDependent的使用類DependentClientA
@ApplicationScoped
public class DependentClientA {
@Inject
HelloDependent hello;
public String doHello() {
return hello.hello();
}
}
-
DependentClientB的代碼和DependentClientA一模一樣,就不貼出來了
-
最後寫個單元測試類驗證HelloDependent的特殊能力
@QuarkusTest
public class DependentTest {
@Inject
DependentClientA dependentClientA;
@Inject
DependentClientB dependentClientB;
@Test
public void testSelectHelloInstanceA() {
Class<HelloDependent> clazz = HelloDependent.class;
Assertions.assertEquals(clazz.getSimpleName(), dependentClientA.doHello());
Assertions.assertEquals(clazz.getSimpleName(), dependentClientB.doHello());
}
}
- 運行單元測試,如下圖紅框,首先,HelloDependent的日誌列印了兩次,證明的確實例化了兩個HelloDependent對象,其次日誌的內容也準確的將註入點的類的信息列印出來
擴展組件的作用域
-
quarkus的擴展組件豐富多彩,自己也能按照官方指引製作,所以擴展組件對應的作用域也隨著組件的不同而各不相同,就不在此列舉了,就舉一個例子吧:quarkus-narayana-jta組件中定義了一個作用域javax.transaction.TransactionScoped,該作用域修飾的bean,每個事物對應一個實例
-
至此,quarkus作用域的瞭解和實戰已經完成,這樣一來,不論是使用bean還是創建bean,都能按業務需要來準確控制其生命周期了