欢迎访问我的GitHub
这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
关于bean的作用域(scope)
- @ApplicationScoped
- public class ClassAnnotationBean {
- public String hello() {
- return "from " + this.getClass().getSimpleName();
- }
- }
复制代码
- 作用域有多种,如果按来源区分一共两大类:quarkus内置和扩展组件中定义,本篇聚焦quarkus的内置作用域
- 下面是整理好的作用域一览,接下来会逐个讲解
graph LRL1(作用域) --> L2-1(内置)L1 --> L2-2(扩展组件)L2-1 --> L3-1(常规作用域)L2-1 --> L3-2(伪作用域)L3-1 --> L4-1(ApplicationScoped)L3-1 --> L4-2(RequestScoped)L3-1 --> L4-3(SessionScoped)L3-2 --> L4-4(Singleton)L3-2 --> L4-5(Dependent)L2-2 --> L3-6(例如 : TransactionScoped)常规作用域和伪作用域
- 常规作用域,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();
- }
- }
复制代码- 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,都能按业务需要来准确控制其生命周期了
欢迎关注博客园:程序员欣宸
学习路上,你不孤单,欣宸原创一路相伴...
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |