quarkus依赖注入之十三:其他重要知识点大串讲(终篇)

打印 上一主题 下一主题

主题 876|帖子 876|积分 2628

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
本篇概览


  • 本篇是《quarkus依赖注入》系列的终篇,前面十二篇已覆盖quarkus依赖注入的大部分核心内容,但依然漏掉了一些知识点,今天就将剩下的内容汇总,来个一锅端,轻松愉快的结束这个系列
  • 总的来说,本篇由以下内容构成,每个段落都是个独立的知识点

  • 几处可以简化编码的地方,如bean注入、构造方法等
  • WithCaching:特定场景下,减少bean实例化次数
  • 静态方法是否可以被拦截器拦截?
  • All注解,让多个bean的注入更加直观
  • 统一处理异步事件的异常


  • 咱们从最简单的看起:表达方式的简化,一共有三个位置可以简化:bean的注入、bean构造方法、bean生产方法
简化之一:bean注入


  • quarkus在CDI规范的基础上做了简化,可以让我们少写几行代码
  • 将配置文件中名为greeting.message的配置项注入到bean的成员变量greetingMsg中,按照CDI规范的写法如下
  1.   @Inject
  2.   @ConfigProperty(name = "greeting.message")
  3.   String greetingMsg;
复制代码

  • 在quarkus框架下可以略去@Inject,写成下面这样的效果和上面的代码一模一样
  1. @ConfigProperty(name = "greeting.message")
  2. String greetingMsg;
复制代码
简化之二:bean构造方法


  • 关于bean的构造方法,CDI有两个规定:首先,必须要有无参构造方法,其次,有参数的构造方法需要@Inject注解修饰,实例代码如下所示
  1. @ApplicationScoped
  2. public class MyCoolService {
  3.   private SimpleProcessor processor;
  4.   MyCoolService() { // dummy constructor needed
  5.   }
  6.   @Inject // constructor injection
  7.   MyCoolService(SimpleProcessor processor) {
  8.     this.processor = processor;
  9.   }
  10. }
复制代码

  • 但是,在quarkus框架下,无参构造方法可不写,有参数的构造方法也可以略去@Inject,写成下面这样的效果和上面的代码一模一样
  1. @ApplicationScoped
  2. public class MyCoolService {
  3.   private SimpleProcessor processor;
  4.   MyCoolService(SimpleProcessor processor) {
  5.     this.processor = processor;
  6.   }
  7. }
复制代码
简化之三:bean生产方法


  • 在CDI规范中,通过方法生产bean的语法如下,可见要同时使用ProducesApplicationScoped注解修饰返回bean的方法
  1. class Producers {
  2.   
  3.   @Produces
  4.   @ApplicationScoped
  5.   MyService produceServ
  6.     ice() {
  7.     return new MyService(coolProperty);
  8.   }
  9. }
复制代码

  • 在quarkus框架下可以略去@Produces,写成下面这样的效果和上面的代码一模一样
  1. class Producers {
  2.   @ApplicationScoped
  3.   MyService produceService() {
  4.     return new MyService(coolProperty);
  5.   }
  6. }
复制代码

  • 好了,热身结束,接下来看几个略有深度的技能
WithCaching注解:避免不必要的多次实例化


  • 在介绍WithCaching注解之前,先来看一个普通场景
  • 下面是一段单元测试代码,HelloDependent类型的bean通过Instance的方式被注入,再用Instance#get来获取此bean
  1. @QuarkusTest
  2. public class WithCachingTest {
  3.     @Inject
  4.     Instance<HelloDependent> instance;
  5.     @Test
  6.     public void test() {
  7.         // 第一次调用Instance#get方法
  8.         HelloDependent helloDependent = instance.get();
  9.         helloDependent.hello();
  10.         // 第二次调用Instance#get方法
  11.         helloDependent = instance.get();
  12.         helloDependent.hello();
  13.     }
  14. }
复制代码

  • 上述代码是种常见的bean注入和使用方式,我们的本意是在WithCachingTest实例中多次使用HelloDependent类型的bean,可能是在test方法中使用,也可能在WithCachingTest的其他方法中使用
  • 如果HelloDependent的作用域是ApplicationScoped,上述代码一切正常,但是,如果作用域是Dependent呢?代码中执行了两次Instance#get,得到的HelloDependent实例是同一个吗?Dependent的特性是每次注入都实例化一次,这里的Instance#get又算几次注入呢?
  • 最简单的方法就是运行上述代码看实际效果,这里先回顾HelloDependent.java的源码,如下所示,构造方法中会打印日志,这下好办了,只要看日志出现几次,就知道实例化几次了
  1. @Dependent
  2. public class HelloDependent {
  3.     public HelloDependent(InjectionPoint injectionPoint) {
  4.         Log.info("injecting from bean "+ injectionPoint.getMember().getDeclaringClass());
  5.     }
  6.     public String hello() {
  7.         return this.getClass().getSimpleName();
  8.     }
  9. }
复制代码

  • 运行单元测试类WithCachingTest,如下图红框所示,构造方法中的日志打印了两次,所以:每次Instance#get都相当于一次注入,如果bean的作用域是Dependent,就会创建一个新的实例并返回


  • 现在问题来了:如果bean的作用域必须是Dependent,又希望多次Instance#get返回的是同一个bean实例,这样的要求可以做到吗?
  • 答案是可以,用WithCaching注解修饰Instance即可,改动如下图红框1,改好后再次运行,红框2显示HelloDependent只实例化了一次

拦截静态方法


  • 先回顾一下拦截器的基本知识,定义一个拦截器并用来拦截bean中的方法,总共需要完成以下三步


  • 实现拦截器的具体功能时,还要用注解指明拦截器类型,一共有四种类型

  • AroundInvoke:拦截bean方法
  • PostConstruct:生命周期拦截器,bean创建后执行
  • PreDestroy:生命周期拦截器,bean销毁前执行
  • AroundConstruct:生命周期拦截器,拦截bean构造方法


  • 现在问题来了:拦截器能拦截静态方法吗?
  • 答案是可以,但是有限制,具体的限制如下

  • 仅支持方法级别的拦截(即拦截器修饰的是方法)
  • private型的静态方法不会被拦截
  • 下图是拦截器实现的常见代码,通过入参InvocationContext的getTarget方法,可以得到被拦截的对象,然而,在拦截静态方法时,getTarget方法的返回值是null,这一点尤其要注意,例如下图红框中的代码,在拦截静态方法是就会抛出空指针异常
All更加直观的注入


  • 假设有个名为SayHello的接口,源码如下
  1. public interface SayHello {
  2.     void hello();
  3. }
复制代码

  • 现在有三个bean都实现了SayHello接口,如果想要调用这三个bean的hello方法,应该怎么做呢?
  • 按照CDI的规范,应该用Instance注入,然后使用Instance中的迭代器即可获取所有bean,代码如下
  1. public class InjectAllTest {
  2.     /**
  3.      * 用Instance接收注入,得到所有SayHello类型的bean
  4.      */
  5.     @Inject
  6.     Instance<SayHello> instance;
  7.     @Test
  8.     public void testInstance() {
  9.         // instance中有迭代器,可以用遍历的方式得到所有bean
  10.         for (SayHello sayHello : instance) {
  11.             sayHello.hello();
  12.         }
  13.     }
  14. }
复制代码

  • quarkus提供了另一种方式,借助注解io.quarkus.arc.All,可以将所有SayHello类型的bean注入到List中,如下所示
  1. @QuarkusTest
  2. public class InjectAllTest {
  3.     /**
  4.      * 用All注解可以将SayHello类型的bean全部注入到list中,
  5.      * 这样更加直观
  6.      */
  7.     @All
  8.     List<SayHello> list;
  9.     @Test
  10.     public void testAll() {
  11.         for (SayHello sayHello : list) {
  12.             sayHello.hello();
  13.         }
  14.     }
  15. }
复制代码

  • 和CDI规范相比,使用All注解可以让代码显得更为直观,另外还有以下三个特点

  • 此list是immutable的(内容不可变)
  • list中的bean是按照priority排序的
  • 如果您需要的不仅仅是注入bean,还需要bean的元数据信息(例如bean的scope),可以将List中的类型从SayHello改为InstanceHandle,这样即可以得到注入bean,也能得到注入bean的元数据(在InjectableBean中),参考代码如下
  1. @QuarkusTest
  2. public class InjectAllTest {
  3.    
  4.     @All
  5.     List<InstanceHandle<SayHello>> list;
  6.     @Test
  7.     public void testQuarkusAllAnnonation() {
  8.         for (InstanceHandle<SayHello> instanceHandle : list) {
  9.             // InstanceHandle#get可以得到注入bean
  10.             SayHello sayHello = instanceHandle.get();
  11.             // InjectableBean封装了注入bean的元数据信息
  12.             InjectableBean<SayHello> injectableBean = instanceHandle.getBean();
  13.             // 例如bean的作用域就能从InjectableBean中取得
  14.             Class clazz = injectableBean.getScope();
  15.             // 打印出来验证
  16.             Log.infov("bean [{0}], scope [{1}]", sayHello.getClass().getSimpleName(), clazz.getSimpleName() );
  17.         }
  18.     }
  19. }
复制代码

  • 代码的执行结果如下图红框所示,可见注入bean及其作用域都能成功取得(要注意的是注入bean是代理bean)

统一处理异步事件的异常


  • 需要提前说一下,本段落涉及的知识点和AsyncObserverExceptionHandler类有关,而《quarkus依赖注入》系列所用的quarkus-2.7.3.Final版本中并没有AsyncObserverExceptionHandler类,后来将quarkus版本更新为2.8.2.Final,就可以正常使用AsyncObserverExceptionHandler类了
  • 本段落的知识点和异步事件有关:如果消费异步事件的过程中发生异常,而开发者有没有专门写代码处理异步消费结果,那么此异常就默默无闻的被忽略了,我们也可能因此错失了及时发现和处理问题的时机
  • 来写一段代码复现上述问题,首先是事件定义TestEvent.java,就是个普通类,啥都没有
  1. public class TestEvent {
  2. }
复制代码

  • 然后是事件的生产者TestEventProducer.java,注意其调用fireAsync方法发送了一个异步事件
  1. @ApplicationScoped
  2. public class TestEventProducer {
  3.     @Inject
  4.     Event<TestEvent> event;
  5.     /**
  6.      * 发送异步事件
  7.      */
  8.     public void asyncProduce() {
  9.         event.fireAsync(new TestEvent());
  10.     }
  11. }
复制代码

  • 事件的消费者TestEventConsumer.java,这里在消费TestEvent事件的时候,故意抛出了异常
  1. @ApplicationScoped
  2. public class TestEventConsumer {
  3.     /**
  4.      * 消费异步事件,这里故意抛出异常
  5.      */
  6.     public void aSyncConsume(@ObservesAsync TestEvent testEvent) throws Exception {
  7.        throw new Exception("exception from aSyncConsume");
  8.     }
  9. }
复制代码

  • 最后是单元测试类将事件的生产和消费运行起来
  1. @QuarkusTest
  2. public class EventExceptionHandlerTest {
  3.     @Inject
  4.     TestEventProducer testEventProducer;
  5.     @Test
  6.     public void testAsync() throws InterruptedException {
  7.        testEventProducer.asyncProduce();
  8.     }
  9. }
复制代码

  • 运行EventExceptionHandlerTest,结果如下图,DefaultAsyncObserverExceptionHandler处理了这个异常,这是quarkus框架的默认处理逻辑


  • DefaultAsyncObserverExceptionHandler只是输出了日志,这样的处理对于真实业务是不够的(可能需要记录到特定地方,调用其他告警服务等),所以,我们需要自定义默认的异步事件异常处理器
  • 自定义的全局异步事件异常处理器如下
  1. package com.bolingcavalry.service.impl;
  2. import io.quarkus.arc.AsyncObserverExceptionHandler;
  3. import io.quarkus.logging.Log;
  4. import javax.enterprise.context.ApplicationScoped;
  5. import javax.enterprise.inject.spi.EventContext;
  6. import javax.enterprise.inject.spi.ObserverMethod;
  7. @ApplicationScoped
  8. public class NoopAsyncObserverExceptionHandler implements AsyncObserverExceptionHandler {
  9.     @Override
  10.     public void handle(Throwable throwable, ObserverMethod<?> observerMethod, EventContext<?> eventContext) {
  11.         // 异常信息
  12.         Log.info("exception is - " + throwable);
  13.         // 事件信息
  14.         Log.info("observer type is - " + observerMethod.getObservedType().getTypeName());
  15.     }
  16. }
复制代码

  • 此刻,咱们再执行一次单元测试,如下图所示,异常已经被NoopAsyncObserverExceptionHandler#handler处理,异常和事件相关的信息都能拿到,您可以按照实际的业务需求来进行定制了


  • 另外还要说明一下,自定义的全局异步事件异常处理器,其作用域只能是ApplicationScoped或者Singleton
  • 至此,《quarkus依赖注入》系列全部完成,与bean相关的故事也就此结束了,十三篇文章凝聚了欣宸对quarkus框架bean容器的思考和实践,希望能帮助您更快的掌握和理解quarkus最核心的领域
  • 虽然《quarkus依赖注入》已经终结,但是《quarkus实战》系列依然还在持续更新中,有了依赖注入的知识作为基础,接下来的quarkus之旅会更加轻松和高效
欢迎关注博客园:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

冬雨财经

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表