Quarkus依賴注入之一:創(chuàng)建Bean
作者:程序員欣宸
作為《Quarkus依賴注入》的開(kāi)篇,本文先介紹CDI,再學(xué)習(xí)如何創(chuàng)建Bean實(shí)例。
關(guān)于依賴注入
- 對(duì)一名java程序員來(lái)說(shuō),依賴注入應(yīng)該是個(gè)熟悉的概念,簡(jiǎn)單的說(shuō)就是:我要用XXX,但我不負(fù)責(zé)XXX的生產(chǎn)
- 以下代碼來(lái)自spring官方,serve方法要使用MyComponent類的doWork方法,但是不負(fù)責(zé)MyComponent對(duì)象的實(shí)例化,只要用注解Autowired修飾成員變量myComponent,spring環(huán)境會(huì)負(fù)責(zé)為myComponent賦值一個(gè)實(shí)例
@Service
public class MyService {
@Autowired
MyComponent myComponent;
public String serve() {
myComponent.doWork();
return "success";
}
}
- 關(guān)于依賴注入,網(wǎng)上有很多優(yōu)秀文章,這里就不展開(kāi)了,咱們要關(guān)注的是quarkus框架的依賴注入
關(guān)于《quarkus依賴注入》系列
- 《quarkus依賴注入》共十三篇文章,整體規(guī)劃上隸屬于《quarkus實(shí)戰(zhàn)》系列,但專注于依賴注入的知識(shí)點(diǎn)和實(shí)戰(zhàn)
- 如果您熟悉spring的依賴注入,那么閱讀本系列時(shí)會(huì)發(fā)現(xiàn)quarkus與spring之間有太多相似之處,很多地方一看就懂
本篇概覽
- 作為《quarkus依賴注入》的開(kāi)篇,本文先介紹CDI,再學(xué)習(xí)如何創(chuàng)建bean實(shí)例,全文內(nèi)容如下:
- 學(xué)習(xí)quarkus的依賴注入之前,來(lái)自官方的提醒非常重要。
官方提醒
- 在使用依賴注入的時(shí)候,quankus官方建議不要使用私有變量(用默認(rèn)可見(jiàn)性,即相同package內(nèi)可見(jiàn)),因?yàn)镚raalVM將應(yīng)用制作成二進(jìn)制可執(zhí)行文件時(shí),編譯器名為Substrate VM,操作私有變量需要用到反射,而GraalVM使用反射的限制,導(dǎo)致靜態(tài)編譯的文件體積增大。
Quarkus is designed with Substrate VM in mind. For this reason, we encourage you to use *package-private* scope instead of *private*.
關(guān)于CDI
- 《 Contexts and Dependency Injection for Java 2.0》,簡(jiǎn)稱CDI,該規(guī)范是對(duì)JSR-346的更新,quarkus對(duì)依賴注入的支持就是基于此規(guī)范實(shí)現(xiàn)的
- 從 2.0 版開(kāi)始,CDI 面向 Java SE 和 Jakarta EE 平臺(tái),Java SE 中的 CDI 和 Jakarta EE 容器中的 CDI 共享core CDI 中定義的特性。
- 簡(jiǎn)單看下CDI規(guī)范的內(nèi)容(請(qǐng)?jiān)徯厘返挠⒄Z(yǔ)水平):
- 該規(guī)范定義了一組強(qiáng)大的補(bǔ)充服務(wù),有助于改進(jìn)應(yīng)用程序代碼的結(jié)構(gòu)。
- 給有狀態(tài)對(duì)象定義了生命周期,這些對(duì)象會(huì)綁定到上下文,上下文是可擴(kuò)展的。
- 復(fù)雜的、安全的依賴注入機(jī)制,還有開(kāi)發(fā)和部署階段選擇依賴的能力。
- 與Expression Language (EL)集成。
- 裝飾注入對(duì)象的能力(個(gè)人想到了AOP,你拿到的對(duì)象其實(shí)是個(gè)代理)。
- 攔截器與對(duì)象關(guān)聯(lián)的能力。
- 事件通知模型。
- web會(huì)話上下文。
- 一個(gè)SPI:允許便攜式擴(kuò)展與容器的集成(integrate cleanly )。
關(guān)于CDI的bean
- CDI的實(shí)現(xiàn)(如quarkus),允許對(duì)象做這些事情:
- 綁定到生命周期上下文。
- 注入。
- 與攔截器和裝飾器關(guān)聯(lián)。
- 通過(guò)觸發(fā)和觀察事件,以松散耦合的方式交互。
- 上述場(chǎng)景的對(duì)象統(tǒng)稱為bean,上下文中的 bean 實(shí)例稱為上下文實(shí)例,上下文實(shí)例可以通過(guò)依賴注入服務(wù)注入到其他對(duì)象中。
- 關(guān)于CDI的背景知識(shí)就介紹到這里吧,接下來(lái)要寫代碼了。
源碼下載
- 本篇實(shí)戰(zhàn)的完整源碼可在GitHub下載到,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos)。
- 這個(gè)git項(xiàng)目中有多個(gè)文件夾,本次實(shí)戰(zhàn)的源碼在quarkus-tutorials文件夾下,如下圖紅框。
- quarkus-tutorials是個(gè)父工程,里面有多個(gè)module,本篇實(shí)戰(zhàn)的module是basic-di,如下圖紅框。
創(chuàng)建demo工程
- 創(chuàng)建個(gè)最簡(jiǎn)單的web工程,默認(rèn)生成一個(gè)web服務(wù)類HobbyResource.java,代碼如下,后面的演示代碼都寫在這個(gè)工程中。
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();
}
}
- 接下來(lái),從最基礎(chǔ)的創(chuàng)建bean實(shí)例創(chuàng)建開(kāi)始。
創(chuàng)建bean實(shí)例:注解修飾在類上
- 先來(lái)看看spring是如何創(chuàng)建bean實(shí)例的,回顧文章剛開(kāi)始的那段代碼,myComponent對(duì)象來(lái)自哪里?
- 繼續(xù)看spring官方的demo,如下所示,用Component注解修飾在類上,spring就會(huì)實(shí)例化MyComponent對(duì)象并注冊(cè)在bean容器中,需要用此bean的時(shí)候用Autowired注解就可以注入了。
@Component
public class MyComponent {
public void doWork() {}
}
- quarkus框架下也有類似方式,演示類ClassAnnotationBean.java如下,用注解ApplicationScoped去修飾ClassAnnotationBean.類,如此quarkus就會(huì)實(shí)例化此類并放入容器中
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也很簡(jiǎn)單,如下,用注解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());
}
}
- 如何驗(yàn)證上述代碼是否有效?運(yùn)行服務(wù),再用瀏覽器訪問(wèn)classannotataionbean接口,肉眼判斷返回內(nèi)容是否符合要求,這樣雖然可行,但總覺(jué)得會(huì)被嘲諷低效…
- 還是寫一段單元測(cè)試代碼吧,如下所示,注意要用QuarkusTest注解修飾測(cè)試類(不然服務(wù)啟動(dòng)有問(wèn)題),測(cè)試方法中檢查了返回碼和body,如果前面的依賴注入沒(méi)問(wèn)題,則下面的測(cè)試應(yīng)該能通過(guò)才對(duì)。
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內(nèi)容,是否含有ClassAnnotationBean.hello方法返回的字符串
.body(containsString("from " + ClassAnnotationBean.class.getSimpleName()));
}
}
- 執(zhí)行命令mvn clean test -U開(kāi)始測(cè)試,控制臺(tái)輸出如下,提示測(cè)試通過(guò)。
[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] ------------------------------------------------------------------------
- 如果您的開(kāi)發(fā)工具是IDEA,也可以用它的圖形化工具執(zhí)行測(cè)試,如下圖,能得到更豐富的測(cè)試信息。
- 掌握了最基礎(chǔ)的實(shí)例化方式,接著看下一種方式:修飾在方法上。
創(chuàng)建bean實(shí)例:注解修飾在方法上
- 下一種創(chuàng)建bean的方式,我們還是先看spring是怎么做的,有了它作對(duì)比,對(duì)quarkus的做法就好理解了。
- 來(lái)看spring官方文檔上的一段代碼,如下所示,用Bean注解修飾myBean方法,spring框架就會(huì)執(zhí)行此方法,將返回值作為bean注冊(cè)到容器中,spring把這種bean的處理過(guò)程稱為lite mode。
@Component
public class Calculator {
public int sum(int a, int b) {
return a+b;
}
@Bean
public MyBean myBean() {
return new MyBean();
}
}
- kuarkus框架下,也能用注解修飾方法來(lái)創(chuàng)建bean,為了演示,先定義個(gè)普通接口。
package com.bolingcavalry.service;
public interface HelloService {
String hello();
}
- kuarkus框架下,也能用注解修飾方法來(lái)創(chuàng)建bean,為了演示,先定義個(gè)普通接口。
package com.bolingcavalry.service;
public interface HelloService {
String hello();
}
- 以及HelloService接口的實(shí)現(xiàn)類。
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沒(méi)有任何關(guān)系。
- 下面的代碼演示了用注解修飾方法,使得quarkus調(diào)用此方法,將返回值作為bean實(shí)例注冊(cè)到容器中,Produces通知quarkus做實(shí)例化,ApplicationScoped表明了bean的作用域是整個(gè)應(yīng)用。
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();
}
}
- 這種用于創(chuàng)建bean的方法,被quarkus稱為producer method。
- 看過(guò)上述代碼,相信聰明的您應(yīng)該明白了用這種方式創(chuàng)建bean的優(yōu)點(diǎn):在創(chuàng)建HelloService接口的實(shí)例時(shí),可以控制所有細(xì)節(jié)(構(gòu)造方法的參數(shù)、或者從多個(gè)HelloService實(shí)現(xiàn)類中選擇一個(gè)),沒(méi)錯(cuò),在SpringBoot的Configuration類中咱們也是這樣做的。
- 前面的getHelloService方法的返回值,可以直接在業(yè)務(wù)代碼中依賴注入,如下所示。
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());
}
}
- 單元測(cè)試代碼如下
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內(nèi)容,HelloServiceImpl.hello方法返回的字符串
.body(containsString("from " + HelloServiceImpl.class.getSimpleName()));
}
}
- 測(cè)試通過(guò)
- producer method有個(gè)特性需要重點(diǎn)關(guān)注:如果剛才生產(chǎn)bean的getHelloService方法有個(gè)入?yún)?,如下所示,入?yún)⑹荗therService對(duì)象,那么,這個(gè)OtherService對(duì)象也必須是個(gè)bean實(shí)例(這就像你用@Inject注入一個(gè)bean的時(shí)候,這個(gè)bean必須存在一樣),如果OtherService不是個(gè)bean,那么應(yīng)用初始化的時(shí)候會(huì)報(bào)錯(cuò),(其實(shí)這個(gè)特性SpringBoot中也有,相信經(jīng)驗(yàn)豐富的您在使用Configuration類的時(shí)候應(yīng)該用到過(guò))。
public class MethodAnnonationBean {
@Produces
@ApplicationScoped
public HelloService getHelloService(OtherService otherService) {
return new HelloServiceImpl();
}
}
- quarkus還做了個(gè)簡(jiǎn)化:如果有了ApplicationScoped這樣的作用域注解,那么Produces可以省略掉,寫成下面這樣也是正常運(yùn)行的。
public class MethodAnnonationBean {
@ApplicationScoped
public HelloService getHelloService() {
return new HelloServiceImpl();
}
}
創(chuàng)建bean實(shí)例:注解修飾在成員變量上
- 再來(lái)看看最后一種方式,注解在成員變量上,這個(gè)成員變量就成了bean。
- 先寫個(gè)普通類用于稍后測(cè)試。
package com.bolingcavalry.service.impl;
import com.bolingcavalry.service.HelloService;
public class OtherServiceImpl {
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
- 通過(guò)成員變量創(chuàng)建bean的方式如下所示,給otherServiceImpl增加兩個(gè)注解,Produces通知quarkus做實(shí)例化,ApplicationScoped表明了bean的作用域是整個(gè)應(yīng)用,最終OtherServiceImpl實(shí)例會(huì)被創(chuàng)建后注冊(cè)到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();
}
- 這種用于創(chuàng)建bean的成員變量(如上面的otherServiceImpl),被quarkus稱為producer field。
- 上述bean的使用方法如下,可見(jiàn)與前面的使用并無(wú)區(qū)別,都是從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());
}
}
- 測(cè)試代碼與前面類似就不贅述了,請(qǐng)您自行完成編寫和測(cè)試。
關(guān)于synthetic bean
- 還有一種bean,quarkus官方稱之為synthetic bean(合成bean),這種bean只會(huì)在擴(kuò)展組件中用到,而咱們?nèi)粘5膽?yīng)用開(kāi)發(fā)不會(huì)涉及,synthetic bean的特點(diǎn)是其屬性值并不來(lái)自它的類、方法、成員變量的處理,而是由擴(kuò)展組件指定的,在注冊(cè)syntheitc bean到quarkus容器時(shí),常用SyntheticBeanBuildItem類去做相關(guān)操作,來(lái)看一段實(shí)例化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依賴注入》的開(kāi)篇已經(jīng)完成,創(chuàng)建bean之后還有更精彩的內(nèi)容為您奉上,敬請(qǐng)期待。
責(zé)任編輯:姜華
來(lái)源:
今日頭條