《quarkus依賴註入》系列聚焦quarkus框架下bean的創建、使用、配置等場景的知識點,本文是系列的開篇,介紹CDI,實戰創建bean ...
歡迎訪問我的GitHub
這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos
關於依賴註入
- 對一名java程式員來說,依賴註入應該是個熟悉的概念,簡單的說就是:我要用XXX,但我不負責XXX的生產
- 以下代碼來自spring官方,serve方法要使用MyComponent類的doWork方法,但是不負責MyComponent對象的實例化,只要用註解Autowired修飾成員變數myComponent,spring環境會負責為myComponent賦值一個實例
@Service
public class MyService {
@Autowired
MyComponent myComponent;
public String serve() {
myComponent.doWork();
return "success";
}
}
- 關於依賴註入,網上有很多優秀文章,這裡就不展開了,咱們要關註的是quarkus框架的依賴註入
關於《quarkus依賴註入》系列
-
《quarkus依賴註入》共六篇文章,整體規划上隸屬於《quarkus實戰》系列,但專註於依賴註入的知識點和實戰
-
如果您熟悉spring的依賴註入,那麼閱讀本系列時會發現quarkus與spring之間有太多相似之處,很多地方一看就懂
本篇概覽
- 作為《quarkus依賴註入》的開篇,本文先介紹CDI,再學習如何創建bean實例,全文內容如下
- 學習quarkus的依賴註入之前,來自官方的提醒非常重要
官方提醒
- 在使用依賴註入的時候,quankus官方建議不要使用私有變數(用預設可見性,即相同package內可見),因為GraalVM將應用製作成二進位可執行文件時,編譯器名為Substrate VM,操作私有變數需要用到反射,而GraalVM使用反射的限制,導致靜態編譯的文件體積增大
Quarkus is designed with Substrate VM in mind. For this reason, we encourage you to use *package-private* scope instead of *private*.
關於CDI
- 《 Contexts and Dependency Injection for Java 2.0》,簡稱CDI,該規範是對JSR-346的更新,quarkus對依賴註入的支持就是基於此規範實現的
- 從 2.0 版開始,CDI 面向 Java SE 和 Jakarta EE 平臺,Java SE 中的 CDI 和 Jakarta EE 容器中的 CDI 共用core CDI 中定義的特性。
- 簡單看下CDI規範的內容(請原諒欣宸的英語水平):
- 該規範定義了一組強大的補充服務,有助於改進應用程式代碼的結構
- 給有狀態對象定義了生命周期,這些對象會綁定到上下文,上下文是可擴展的
- 複雜的、安全的依賴註入機制,還有開發和部署階段選擇依賴的能力
- 與Expression Language (EL)集成
- 裝飾註入對象的能力(個人想到了AOP,你拿到的對象其實是個代理)
- 攔截器與對象關聯的能力
- 事件通知模型
- web會話上下文
- 一個SPI:允許攜帶型擴展與容器的集成(integrate cleanly )
關於CDI的bean
- CDI的實現(如quarkus),允許對象做這些事情:
-
綁定到生命周期上下文
-
註入
-
與攔截器和裝飾器關聯
-
通過觸發和觀察事件,以鬆散耦合的方式交互
-
上述場景的對象統稱為bean,上下文中的 bean 實例稱為上下文實例,上下文實例可以通過依賴註入服務註入到其他對象中
-
關於CDI的背景知識就介紹到這裡吧,接下來要寫代碼了
源碼下載
- 本篇實戰的完整源碼可在GitHub下載到,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos)
名稱 | 鏈接 | 備註 |
---|---|---|
項目主頁 | https://github.com/zq2599/blog_demos | 該項目在GitHub上的主頁 |
git倉庫地址(https) | https://github.com/zq2599/blog_demos.git | 該項目源碼的倉庫地址,https協議 |
git倉庫地址(ssh) | [email protected]:zq2599/blog_demos.git | 該項目源碼的倉庫地址,ssh協議 |
- 這個git項目中有多個文件夾,本次實戰的源碼在quarkus-tutorials文件夾下,如下圖紅框
- quarkus-tutorials是個父工程,裡面有多個module,本篇實戰的module是basic-di,如下圖紅框
創建demo工程
- 您可以參考《quarkus實戰之二:應用的創建、構建、部署》,創建個最簡單的web工程,預設生成一個web服務類HobbyResource.java,代碼如下,後面的演示代碼都寫在這個工程中
package com.bolingcavalry;
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("/actions")
public class HobbyResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello RESTEasy, " + LocalDateTime.now();
}
}
- 接下來,從最基礎的創建bean實例創建開始
創建bean實例:註解修飾在類上
- 先來看看spring是如何創建bean實例的,回顧文章剛開始的那段代碼,myComponent對象來自哪裡?
- 繼續看spring官方的demo,如下所示,用Component註解修飾在類上,spring就會實例化MyComponent對象並註冊在bean容器中,需要用此bean的時候用Autowired註解就可以註入了
@Component
public class MyComponent {
public void doWork() {}
}
- quarkus框架下也有類似方式,演示類ClassAnnotationBean.java如下,用註解ApplicationScoped去修飾ClassAnnotationBean.類,如此quarkus就會實例化此類並放入容器中
package com.bolingcavalry.service.impl;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ClassAnnotationBean {
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
- 這種註解修飾在類上的bean,被quarkus官方成為class-based beans
- 使用bean也很簡單,如下,用註解Inject修飾ClassAnnotationBean類型的成員變數即可
package com.bolingcavalry;
import com.bolingcavalry.service.impl.ClassAnnotationBean;
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("/classannotataionbean")
public class ClassAnnotationController {
@Inject
ClassAnnotationBean classAnnotationBean;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return String.format("Hello RESTEasy, %s, %s",
LocalDateTime.now(),
classAnnotationBean.hello());
}
}
- 如何驗證上述代碼是否有效?運行服務,再用瀏覽器訪問classannotataionbean介面,肉眼判斷返回內容是否符合要求,這樣雖然可行,但總覺得會被嘲諷低效...
- 還是寫一段單元測試代碼吧,如下所示,註意要用QuarkusTest註解修飾測試類(不然服務啟動有問題),測試方法中檢查了返回碼和body,如果前面的依賴註入沒問題,則下麵的測試應該能通過才對
package com.bolingcavalry;
import com.bolingcavalry.service.impl.ClassAnnotationBean;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.containsString;
@QuarkusTest
class ClassAnnotationControllerTest {
@Test
public void testGetEndpoint() {
given()
.when().get("/classannotataionbean")
.then()
.statusCode(200)
// 檢查body內容,是否含有ClassAnnotationBean.hello方法返回的字元串
.body(containsString("from " + ClassAnnotationBean.class.getSimpleName()));
}
}
- 執行命令mvn clean test -U開始測試,控制台輸出如下,提示測試通過
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.702 s
[INFO] Finished at: 2022-03-12T15:48:45+08:00
[INFO] ------------------------------------------------------------------------
- 如果您的開發工具是IDEA,也可以用它的圖形化工具執行測試,如下圖,能得到更豐富的測試信息
- 掌握了最基礎的實例化方式,接著看下一種方式:修飾在方法上
創建bean實例:註解修飾在方法上
- 下一種創建bean的方式,我們還是先看spring是怎麼做的,有了它作對比,對quarkus的做法就好理解了
- 來看spring官方文檔上的一段代碼,如下所示,用Bean註解修飾myBean方法,spring框架就會執行此方法,將返回值作為bean註冊到容器中,spring把這種bean的處理過程稱為lite mode
@Component
public class Calculator {
public int sum(int a, int b) {
return a+b;
}
@Bean
public MyBean myBean() {
return new MyBean();
}
}
- kuarkus框架下,也能用註解修飾方法來創建bean,為了演示,先定義個普通介面
package com.bolingcavalry.service;
public interface HelloService {
String hello();
}
- 以及HelloService介面的實現類
package com.bolingcavalry.service.impl;
import com.bolingcavalry.service.HelloService;
public class HelloServiceImpl implements HelloService {
@Override
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
- 註意,HelloService.java和HelloServiceImpl.java都是普通的java介面和類,與quarkus沒有任何關係
- 下麵的代碼演示了用註解修飾方法,使得quarkus調用此方法,將返回值作為bean實例註冊到容器中,Produces通知quarkus做實例化,ApplicationScoped表明瞭bean的作用域是整個應用
package com.bolingcavalry.service.impl;
import com.bolingcavalry.service.HelloService;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
public class MethodAnnonationBean {
@Produces
@ApplicationScoped
public HelloService getHelloService() {
return new HelloServiceImpl();
}
}
- 這種用於創建bean的方法,被quarkus稱為producer method
- 看過上述代碼,相信聰明的您應該明白了用這種方式創建bean的優點:在創建HelloService介面的實例時,可以控制所有細節(構造方法的參數、或者從多個HelloService實現類中選擇一個),沒錯,在SpringBoot的Configuration類中咱們也是這樣做的
- 前面的getHelloService方法的返回值,可以直接在業務代碼中依賴註入,如下所示
package com.bolingcavalry;
import com.bolingcavalry.service.HelloService;
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("/methodannotataionbean")
public class MethodAnnotationController {
@Inject
HelloService helloService;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String get() {
return String.format("Hello RESTEasy, %s, %s",
LocalDateTime.now(),
helloService.hello());
}
}
- 單元測試代碼如下
package com.bolingcavalry;
import com.bolingcavalry.service.impl.HelloServiceImpl;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.containsString;
@QuarkusTest
class MethodAnnotationControllerTest {
@Test
public void testGetEndpoint() {
given()
.when().get("/methodannotataionbean")
.then()
.statusCode(200)
// 檢查body內容,HelloServiceImpl.hello方法返回的字元串
.body(containsString("from " + HelloServiceImpl.class.getSimpleName()));
}
}
- 測試通過
- producer method有個特性需要重點關註:如果剛纔生產bean的getHelloService方法有個入參,如下所示,入參是OtherService對象,那麼,這個OtherService對象也必須是個bean實例(這就像你用@Inject註入一個bean的時候,這個bean必須存在一樣),如果OtherService不是個bean,那麼應用初始化的時候會報錯,(其實這個特性SpringBoot中也有,相信經驗豐富的您在使用Configuration類的時候應該用到過)
public class MethodAnnonationBean {
@Produces
@ApplicationScoped
public HelloService getHelloService(OtherService otherService) {
return new HelloServiceImpl();
}
}
- quarkus還做了個簡化:如果有了ApplicationScoped這樣的作用域註解,那麼Produces可以省略掉,寫成下麵這樣也是正常運行的
public class MethodAnnonationBean {
@ApplicationScoped
public HelloService getHelloService() {
return new HelloServiceImpl();
}
}
創建bean實例:註解修飾在成員變數上
- 再來看看最後一種方式,註解在成員變數上,這個成員變數就成了bean
- 先寫個普通類用於稍後測試
package com.bolingcavalry.service.impl;
import com.bolingcavalry.service.HelloService;
public class OtherServiceImpl {
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
- 通過成員變數創建bean的方式如下所示,給otherServiceImpl增加兩個註解,Produces通知quarkus做實例化,ApplicationScoped表明瞭bean的作用域是整個應用,最終OtherServiceImpl實例會被創建後註冊到bean容器中
package com.bolingcavalry.service.impl;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
public class FieldAnnonationBean {
@Produces
@ApplicationScoped
OtherServiceImpl otherServiceImpl = new OtherServiceImpl();
}
-
這種用於創建bean的成員變數(如上面的otherServiceImpl),被quarkus稱為producer field
-
上述bean的使用方法如下,可見與前面的使用並無區別,都是從quarkus的依賴註入
@Path("/fieldannotataionbean")
public class FieldAnnotationController {
@Inject
OtherServiceImpl otherServiceImpl;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String get() {
return String.format("Hello RESTEasy, %s, %s",
LocalDateTime.now(),
otherServiceImpl.hello());
}
}
- 測試代碼與前面類似就不贅述了,請您自行完成編寫和測試
關於synthetic bean
- 還有一種bean,quarkus官方稱之為synthetic bean(合成bean),這種bean只會在擴展組件中用到,而咱們日常的應用開發不會涉及,synthetic bean的特點是其屬性值並不來自它的類、方法、成員變數的處理,而是由擴展組件指定的,在註冊syntheitc bean到quarkus容器時,常用SyntheticBeanBuildItem類去做相關操作,來看一段實例化synthetic bean的代碼
@BuildStep
@Record(STATIC_INIT)
SyntheticBeanBuildItem syntheticBean(TestRecorder recorder) {
return SyntheticBeanBuildItem.configure(Foo.class).scope(Singleton.class)
.runtimeValue(recorder.createFoo("parameters are recorder in the bytecode"))
.done();
}
- 至此,《quarkus依賴註入》的開篇已經完成,創建bean之後還有更精彩的內容為您奉上,敬請期待