Quarkus依賴注入之一:創建Bean
作者:程序員欣宸
作為《Quarkus依賴注入》的開篇,本文先介紹CDI,再學習如何創建Bean實例。
關于依賴注入
- 對一名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)。
- 這個git項目中有多個文件夾,本次實戰的源碼在quarkus-tutorials文件夾下,如下圖紅框。
- quarkus-tutorials是個父工程,里面有多個module,本篇實戰的module是basic-di,如下圖紅框。
創建demo工程
- 創建個最簡單的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();
}
- 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之后還有更精彩的內容為您奉上,敬請期待。
責任編輯:姜華
來源:
今日頭條