quarkus核心编程笔记

此篇只做总结,有大佬做的更详细

大佬quarkus笔记

依赖注入

在应用中,一个接口有多个实现是很常见的,那么依赖注入时,如果类型是接口,如何准确选择实现呢?

  1. 修饰符匹配
  2. Named注解属性匹配
  3. 根据优先级选择
  4. 写代码选择

修饰符匹配

  • 先看一个注解Default,这个注解被@Qualifier修饰,这种被@Qualifier修饰的注解,称之为Qualifier修饰符
  • 如果我们新建一个注解,也用Qualifier修饰,这个MyQualifier也是Qualifier的修饰符
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface MyQualifier {@Nonbinding String value();
}
  • 在quarkus容器中的每一个bean都应该有一个Qualifier修饰符在修饰,如果没有,就会被quarkus添加Default注解

在这里插入图片描述

  • 依赖注入时,直接用Qualifier修饰符修饰注入对象,这样quarkus就会去寻找被这个Qualifier修饰符修饰的bean,找到就注入(找不到就报错,找到多个业报错)

修饰符匹配要注意的地方

​ 修饰符匹配的逻辑非常简单:bean定义和bean注入的地方用一个修饰符即可,使用中有三个笛梵要注意

  1. 在注入bean的地方,如果有了Qualifier修饰符,可以把@Infect省略不写
  2. 在定义bean的地方,如果没有了Qualifier修饰符去修饰bean,quarkus会默认添加Default

Named注解的属性匹配

  • Named注解的功能和前面的Qualifier修饰符是一样的,其特殊之处在于通过注解属性来匹配修饰符bean和注入的bean

根据优先级选择

  • 使用优先级选择注入是一种简洁的方式,其核心使用Alternative和Priority两个注解修饰备选bean,然后用Priority的属性值(int型)作为优先级,该值越大优先级越高
  • 在注入位置(@Inject),quarkus会选择优先级最高的bean注入

写代码注入bean

  • 如果不用修饰符匹配,再回到最初的问题:有三个bean都实现了同一个接口,应该如何注入?

@QuarkusTest
public class InstanceTest {@InjectInstance<HelloInstance> instance;@Testpublic void testSelectHelloInstanceA() {Class<HelloInstanceA> clazz = HelloInstanceA.class;Assertions.assertEquals(clazz.getSimpleName(),instance.select(clazz).get().hello());}@Testpublic void testSelectHelloInstanceB() {Class<HelloInstanceB> clazz = HelloInstanceB.class;Assertions.assertEquals(clazz.getSimpleName(),instance.select(clazz).get().hello());}
}

拦截器

  1. 定义和使用拦截器的操作步骤介绍
  2. 拦截异常
  3. 拦截构造方法
  4. 获取被拦截方法的参数
  5. 多个拦截器之间传递参数

定义和使用拦截器的操作介绍

  • 定义和使用拦截器一共需要做三件事

    1. 定义:新增一个注解(假设为A),要用@InterceptorBinding修饰该注解

    2. 实现:拦截器A到底要做什么事情,需要在一个类中实现,该类需要两个注解来修饰:A和Interceptor

    3. 使用:用A来修饰要拦截器的Bean

      流程图 (19)

@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HandleError {
}
/*** Priority注解的作用 设定HandlerError 拦截器的优先级(值越小优先级越高),可以同时用多个拦截器拦截同一个方法*/
@HandleError
@Interceptor
@Priority(Interceptor.Priority.APPLICATION +1)
public class HandleErrorInterceptor {/*** AroundInvoke注解的作用 是表明execute会在拦截bean方法时被调用* @param context 可以从入参context处取得被拦截实例和方法的信息* @return*/@AroundInvokeObject execute(InvocationContext context) {try {Log.info(context.getContextData());// 注意proceed方法的含义:调用下一个拦截器,直到最后一个才会执行被拦截的方法return context.proceed();} catch (Exception exception) {Log.errorf(exception,"method error from %s.%s\n",context.getTarget().getClass().getSimpleName(),context.getMethod().getName());}return null;}
}

@ApplicationScoped
@HandleError
public class HandleErrorDemo {public void executeThrowError() {throw new IllegalArgumentException("this is business logic exception");}public void hello(){System.out.println("hello world");}
}
@QuarkusTest
public class InterceptorTest {@InjectHandleErrorDemo handleErrorDemo;@Testpublic void testHandleError() {handleErrorDemo.hello();}
}

拦截构造方法

@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HandleConstruction {
}
@HandleConstruction
@Interceptor
@Priority(Interceptor.Priority.APPLICATION +1)
public class HandleConstructionInterceptor {@AroundConstructvoid execute(InvocationContext context) throws Exception {// 执行业务逻辑可以在此Log.infov("start construction interceptor");// 执行bean的构造方法context.proceed();// 注意,对于context.getTarget()的返回值,此时不是null,如果在context.proceed()之前,则是nullLog.infov("bean instance of {0}", context.getTarget().getClass().getSimpleName());}
}
@ApplicationScoped
@HandleConstruction
public class HandleonstructionDemo {public HandleonstructionDemo() {super();Log.infov("construction of {0}", HandleonstructionDemo.class.getSimpleName());}public void hello() {Log.info("hello world!");}
}
@QuarkusTest
public class InterceptorTest {@InjectHandleonstructionDemo handleonstructionDemo;@Testpublic void testHandleonstruction() {handleonstructionDemo.hello();}
}

获取被拦截方法的参数

@InterceptorBinding
@Target({TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackParams {
}
@TrackParams
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 1)
public class TrackParamsInterceptor {@AroundInvokeObject execute(InvocationContext context) throws Exception {// context.getParameters()返回拦截方法的所有参数,// 用Optional处理非空时候的数组Optional.of(Arrays.stream(context.getParameters())).ifPresent(stream -> {stream.forEach(object -> Log.infov("parameter type [{0}], value [{1}]",object.getClass().getSimpleName(),object));});return context.proceed();}
}
@ApplicationScoped
@TrackParams
public class TrackParamsDemo {public void hello(String name, int id) {Log.infov("Hello {0}, your id is {1}", name, id);}
}
@QuarkusTest
public class InterceptorTest {@InjectTrackParamsDemo trackParamsDemo;@Testpublic void testTrackParams() {trackParamsDemo.hello("Tom", 101);}
}

多个拦截器之间传递参数

  • 多个拦截器拦截同一个方法是很正常的,他们各司其职,根据优先级按顺序执行,如果这些拦截器之间有一定逻辑关系,例如第二个拦截器需要第一个拦截器的执行结果,此时又该如何呢?
@InterceptorBinding
@Target({TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ContextData {String KEY_PROCEED_INTERCEPTORS = "proceedInterceptors";
}
package com.bolingcavalry.interceptor.impl;import io.quarkus.logging.Log;import javax.interceptor.InvocationContext;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;import static com.bolingcavalry.interceptor.define.ContextData.KEY_PROCEED_INTERCEPTORS;public class BaseContextDataInterceptor {Object execute(InvocationContext context) throws Exception {// 取出保存拦截器间共享数据的mapMap<String, Object> map = context.getContextData();List<String> list;String instanceClassName = this.getClass().getSimpleName();// 根据指定key从map中获取一个listif (map.containsKey(KEY_PROCEED_INTERCEPTORS)) {list = (List<String>) map.get(KEY_PROCEED_INTERCEPTORS);} else {// 如果map中没有,就在此新建一个list,存如map中list = new ArrayList<>();map.put(KEY_PROCEED_INTERCEPTORS, list);Log.infov("from {0}, this is first processor", instanceClassName);}// 将自身内容存入list中,这样下一个拦截器只要是BaseContextDataInterceptor的子类,// 就能取得前面所有执行过拦截操作的拦截器list.add(instanceClassName);Log.infov("From {0}, all processors {0}", instanceClassName, list);return context.proceed();}
}
@ContextData
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 1)
public class ContextDataInterceptorA extends BaseContextDataInterceptor {@AroundInvokeObject execute(InvocationContext context) throws Exception {return super.execute(context);}
}
@ContextData
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 2)
public class ContextDataInterceptorB extends BaseContextDataInterceptor {@AroundInvokeObject execute(InvocationContext context) throws Exception {return super.execute(context);}
}
@ApplicationScoped
@ContextData
public class ContextDataDemo {public void hello() {Log.info("Hello world!");}
}
@QuarkusTest
public class InterceptorTest {@InjectContextDataDemo contextDataDemo;@Testpublic void testContextData() {contextDataDemo.hello();}
}

发布订阅模式

同步事件

  • 同步事件是指事件发布后,事件接受者会在同一个线程处理事件,对事件发布者来说,相当于发布之后的代码不会立即执行,要等到事件处理的代码执行完毕后

流程图 (20)

public class MyEvent {/*** 事件源*/private String source;/*** 事件被消费的总次数*/private AtomicInteger consumeNum;public MyEvent(String source) {this.source = source;consumeNum = new AtomicInteger();}/*** 事件被消费次数加一* @return*/public int addNum() {return consumeNum.incrementAndGet();}/*** 获取事件被消费次数* @return*/public int getNum() {return consumeNum.get();}@Overridepublic String toString() {return "MyEvent{" +"source='" + source + '\'' +", consumeNum=" + getNum() +'}';}
}
@ApplicationScoped
public class MyProducer {@InjectEvent<MyEvent> event;/*** 发送同步消息* @param source 消息源* @return 被消费次数*/public int syncProduce(String source) {MyEvent myEvent = new MyEvent("syncEvent");Log.infov("before sync fire, {0}", myEvent);event.fire(myEvent);Log.infov("after sync fire, {0}", myEvent);return myEvent.getNum();}
}
@ApplicationScoped
public class MyConsumer {/*** 消费同步事件* @param myEvent*/public void syncConsume(@Observes MyEvent myEvent) {Log.infov("receive sync event, {0}", myEvent);// 模拟业务执行,耗时100毫秒try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}// 计数加一myEvent.addNum();}
}
@QuarkusTest
public class EventTest {@InjectMyProducer myProducer;@InjectMyConsumer myConsumer;@Testpublic void testSync() {Assertions.assertEquals(1, myProducer.syncProduce("testSync"));}
}

异步事件

  • 发送事件的代码还是写在MyPorducer.java,如下,有两处要注意的地方稍后提到
public int asyncProduce(String source) {MyEvent myEvent = new MyEvent(source);Log.infov("before async fire, {0}", myEvent);event.fireAsync(myEvent).handleAsync((e, error) -> {if (null!=error) {Log.error("handle error", error);} else {Log.infov("finish handle, {0}", myEvent);}return null;});Log.infov("after async fire, {0}", myEvent);return myEvent.getNum();}

发送异步事件的API是fireAsync

fireAsync的返回值是CompletionStage,我们可以调用其handleAsync方法,将响应逻辑(对事件消费结果的处理)传入,这段响应逻辑会在事件消费结束后被执行,上述代码中的响应逻辑是检查异常,若有就打印

  • 消费异步事件的代码写在MyConsumer,与同步的相比唯一的变化就是修饰入参的注解改成了ObservesAsync
public void aSyncConsume(@ObservesAsync MyEvent myEvent) {Log.infov("receive async event, {0}", myEvent);// 模拟业务执行,耗时100毫秒try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}// 计数加一myEvent.addNum();}
  • 单元测试代码,有两点需要注意,稍后会提到
 @Testpublic void testAsync() throws InterruptedException {Assertions.assertEquals(0, myProducer.asyncProduce("testAsync"));// 如果不等待的话,主线程结束的时候会中断正在消费事件的子线程,导致子线程报错Thread.sleep(150);}
  • 上述代码有以下两点需要注意
  1. 异步事件的时候,发送事件的线程不会等待,所以myEvent实例的计数器在消费线程还没来得及加一,myProducer.asyncProduce方法就已经执行结束了,返回值是0,所以单元测试的assertEquals位置,期望值应该是0
  2. testAsync方法要等待100毫秒以上才能结束,否则进程会立即结束,导致正在消费事件的子线程被打断,抛出异常

同一种事件类,用在不同的业务场景

  • 设想这样一个场景:管理员发送XXX类型的事件,消费者应该是处理管理员事件的方法,普通用户也发送XXX类型的事件,消费者应该是处理普通用户事件的方法,简单的说就是同一个数据结构的事件可能用在不同场景,如下图

流程图 (21)

从技术上分析,实现上述功能的关键点是:消息的消费者要精确过滤掉不该自己消费的消息

此刻,您是否回忆起前面文章中的一个场景:依赖注入时,如何从多个bean中选择自己所需的那个,这两个问题何其相似,而依赖注入的选择问题是用Qualifier注解解决的,今天的消息场景,依旧可以用Qualifier来对消息做精确过滤,接下来编码实战

首先定义事件类ChannelEvent.java,管理员和普通用户的消息数据都用这个类(和前面的MyEvent事件类的代码一样)

public class TwoChannelEvent {/*** 事件源*/private String source;/*** 事件被消费的总次数*/private AtomicInteger consumeNum;public TwoChannelEvent(String source) {this.source = source;consumeNum = new AtomicInteger();}/*** 事件被消费次数加一* @return*/public int addNum() {return consumeNum.incrementAndGet();}/*** 获取事件被消费次数* @return*/public int getNum() {return consumeNum.get();}@Overridepublic String toString() {return "TwoChannelEvent{" +"source='" + source + '\'' +", consumeNum=" + getNum() +'}';}
}
  • 然后就是关键点:自定义注解Admin,这是管理员事件的过滤器,要用Qualifier修饰

    @Qualifier
    @Retention(RUNTIME)
    @Target({FIELD, PARAMETER})
    public @interface Admin {
    }
    
  • 自定义注解Normal,这是普通用户事件的过滤器,要用Qualifier修饰

    @Qualifier
    @Retention(RUNTIME)
    @Target({FIELD, PARAMETER})
    public @interface Normal {
    }
    
  • Admin和Normal先用在发送事件的代码中,再用在消费事件的代码中,这样就完成了匹配,先写发送代码,有几处要注意的地方稍后会提到

@ApplicationScoped
public class TwoChannelWithTwoEvent {@Inject@AdminEvent<TwoChannelEvent> adminEvent;@Inject@NormalEvent<TwoChannelEvent> normalEvent;/*** 管理员消息* @param source* @return*/public int produceAdmin(String source) {TwoChannelEvent event = new TwoChannelEvent(source);adminEvent.fire(event);return event.getNum();}/*** 普通消息* @param source* @return*/public int produceNormal(String source) {TwoChannelEvent event = new TwoChannelEvent(source);normalEvent.fire(event);return event.getNum();}
}
  1. 注入了两个Event实例adminEvent和normalEvent,它们的类型一模一样,但是分别用Admin和Normal

注解修饰,相当于为它们添加了不同的标签,在消费的时候也可以用这两个注解来过滤

  1. 发送代码并无特别之处,用adminEvent.fire发出的事件,在消费的时候不过滤、或者用Admin过滤,这两种方式都能收到
  • 接下来看消费事件的代码TwoChannelConsumer.java,有几处要注意的地方稍后会提到
@ApplicationScoped
public class TwoChannelConsumer {/*** 消费管理员事件* @param event*/public void adminEvent(@Observes @Admin TwoChannelEvent event) {Log.infov("receive admin event, {0}", event);// 管理员的计数加两次,方便单元测试验证event.addNum();event.addNum();}/*** 消费普通用户事件* @param event*/public void normalEvent(@Observes @Normal TwoChannelEvent event) {Log.infov("receive normal event, {0}", event);// 计数加一event.addNum();}/*** 如果不用注解修饰,所有TwoChannelEvent类型的事件都会在此被消费* @param event*/public void allEvent(@Observes TwoChannelEvent event) {Log.infov("receive event (no Qualifier), {0}", event);// 计数加一event.addNum();}
}
@QuarkusTest
public class EventTest {@InjectTwoChannelWithTwoEvent twoChannelWithTwoEvent;@Testpublic void testTwoChnnelWithTwoEvent() {// 对管理员来说,// TwoChannelConsumer.adminEvent消费时计数加2,// TwoChannelConsumer.allEvent消费时计数加1,// 所以最终计数是3Assertions.assertEquals(3, twoChannelWithTwoEvent.produceAdmin("admin"));// 对普通人员来说,// TwoChannelConsumer.normalEvent消费时计数加1,// TwoChannelConsumer.allEvent消费时计数加1,// 所以最终计数是2Assertions.assertEquals(2, twoChannelWithTwoEvent.produceNormal("normal"));}
}

小优化,不需要注入多个Event实例

  • 刚才的代码虽然可以正常工作,但是有一点小瑕疵:为了发送不同事件,需要注入不同的Event实例,如下图红框,如果事件类型越来越多,注入的Event实例岂不是越来越多?

image-20220403170857712

  • quarkus提供了一种缓解上述问题的方式,再写一个发送事件的类TwoChannelWithSingleEvent.java,代码中有两处要注意的地方稍后会提到
/*** @author will* @email zq2599@gmail.com* @date 2022/4/3 10:16* @description 用同一个事件结构体TwoChannelEvent,分别发送不同业务类型的事件*/
@ApplicationScoped
public class TwoChannelWithSingleEvent {@InjectEvent<TwoChannelEvent> singleEvent;/*** 管理员消息* @param source* @return*/public int produceAdmin(String source) {TwoChannelEvent event = new TwoChannelEvent(source);singleEvent.select(new AnnotationLiteral<Admin>() {}).fire(event);return event.getNum();}/*** 普通消息* @param source* @return*/public int produceNormal(String source) {TwoChannelEvent event = new TwoChannelEvent(source);singleEvent.select(new AnnotationLiteral<Normal>() {}).fire(event);return event.getNum();}
}
  • 上述发送消息的代码,有以下两处需要注意
  1. 不论是Admin事件还是Normal事件,都是用singleEvent发送的,如此避免了事件类型越多Event实例越多的情况发生
  2. 执行fire方法发送事件前,先执行select方法,入参是AnnotationLiteral的匿名子类,并且通过泛型指定事件类型,这和前面TwoChannelWithTwoEvent类发送两种类型消息的效果是一样的
  • 既然用select方法过滤和前面两个Event实例的效果一样,那么消费事件的类就不改动了
@QuarkusTest
public class EventTest {@InjectTwoChannelWithSingleEvent twoChannelWithSingleEvent;@Testpublic void testTwoChnnelWithSingleEvent() {// 对管理员来说,// TwoChannelConsumer.adminEvent消费时计数加2,// TwoChannelConsumer.allEvent消费时计数加1,// 所以最终计数是3Assertions.assertEquals(3, twoChannelWithSingleEvent.produceAdmin("admin"));// 对普通人员来说,// TwoChannelConsumer.normalEvent消费时计数加1,// TwoChannelConsumer.allEvent消费时计数加1,// 所以最终计数是2Assertions.assertEquals(2, twoChannelWithSingleEvent.produceNormal("normal"));}
}

事件元数据

在消费事件时,除了从事件对象中取得业务数据(例如MyEvent的source和consumeNum字段),有时还可能需要用到事件本身的信息,例如类型是Admin还是Normal、Event对象的注入点在哪里等,这些都算是事件的元数据

为了演示消费者如何取得事件元数据,将TwoChannelConsumer.java的allEvent方法改成下面的样子,需要注意的地方稍后会提到

public void allEvent(@Observes TwoChannelEvent event, EventMetadata eventMetadata) {Log.infov("receive event (no Qualifier), {0}", event);// 打印事件类型Log.infov("event type : {0}", eventMetadata.getType());// 获取该事件的所有注解Set<Annotation> qualifiers = eventMetadata.getQualifiers();// 将事件的所有注解逐个打印if (null!=qualifiers) {qualifiers.forEach(annotation -> Log.infov("qualify : {0}", annotation));}// 计数加一event.addNum();
}

上述代码中,以下几处需要注意

  • 给allEvent方法增加一个入参,类型是EventMetadata,bean容器会将事件的元数据设置到此参数
  • EventMetadata的getType方法能取得事件类型
  • EventMetadata的getType方法能取得事件的所有修饰注解,包括Admin或者Normal

生命周期回调

  • 本篇的知识点是bean的生命周期回调:在bean生命周期的不同阶段,都可以触发自定义代码的执行

流程图 - 2022-04-05T094019.781

  • 有两种模式可以实现生命周期回调:拦截器模式和自定义模式,接下来通过编码依次学习

拦截器模式

  • 《拦截器(Interceptor)》已详细介绍了quarkus拦截器的自定义和使用,包括以下三个步骤

流程图 (19)

  • 如果要自定义bean的生命周期回调,也是遵照上述步骤执行,接下来编码实现
  • 首先定义拦截器,名为TrackLifeCycle,就是个普通拦截器,需要用注解InterceptorBinding修饰
@InterceptorBinding
@Target({TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackLifeCycle {
}
  • 然后是实现拦截器的功能,有几处要注意的地方稍后会提到

    @TrackLifeCycle
    @Interceptor
    @Priority(Interceptor.Priority.APPLICATION + 1)
    public class LifeCycleInterceptor {@AroundConstructvoid execute(InvocationContext context) throws Exception {Log.info("start AroundConstruct");try {context.proceed();} catch (Exception e) {e.printStackTrace();}Log.info("end AroundConstruct");}@PostConstructpublic void doPostConstruct(InvocationContext ctx) {Log.info("life cycle PostConstruct");}@PreDestroypublic void doPreDestroy(InvocationContext ctx) {Log.info("life cycle PreDestroy");}
    }

    用注解Interceptor和TrackLifeCycle修饰,说明这是拦截器TrackLifeCycle的实现

    被拦截bean实例化的时候,AroundConstruct修饰的方法execute就会被执行,这和《拦截器》一文中的AroundInvoke的用法很相似

    被拦截bean创建成功后,PostConstruct修饰的方法doPostConstruct就会被执行

    被拦截bean在销毁之前,PreDestroy修饰的方法doPreDestroy就会被执行

  • 接下来是使用拦截器TrackLifeCycle了,用于演示的bean如下,用TrackLifeCycle修饰,有构造方法和简单的helloWorld方法

@ApplicationScoped
@TrackLifeCycle
public class Hello {public Hello() {Log.info(this.getClass().getSimpleName() + " at instance");}public void helloWorld() {Log.info("Hello world!");}
}
  • 最后再写个单元测试类验证
@QuarkusTest
public class LifeCycleTest {@InjectHello hello;@Testpublic void testLifyCycle() {hello.helloWorld();}
}

自定义模式

  • 刚才的拦截器模式有个明显问题:如果不同bean的生命周期回调有不同业务需求,该如何是好?为每个bean做一个拦截器吗?随着bean的增加会有大量拦截器,似乎不是个好的方案

  • 如果您熟悉spring,对下面的代码要改不陌生,这是来自spring官网的内容,直接在bean的方法上用PostConstruct和PreDestroy修饰,即可在bean的创建完成和销毁前被调用

public class CachingMovieLister {@PostConstructpublic void populateMovieCache() {// populates the movie cache upon initialization...}@PreDestroypublic void clearMovieCache() {// clears the movie cache upon destruction...}
}
  • 实际上,quarkus也支持上述方式,不过和拦截器相比有两个差异:
  1. 在bean的内部,只能用PostConstruct和PreDestroy,不能用AroundConstruct,只有拦截器才能用AroundConstruct
  2. 在拦截器中,PostConstruct和PreDestroy 修饰的方法必须要有InvocationContext类型的入参,但是在bean内部则没有此要求
  • 咱们来改造Hello.java的源码,修改后如下,增加了两个方法,分别被PostConstruct和PreDestroy修饰
@ApplicationScoped
@TrackLifeCycle
public class Hello {public Hello() {Log.info(this.getClass().getSimpleName() + " at instance");}@PostConstructpublic void doPostConstruct() {Log.info("at doPostConstruct");}@PreDestroypublic void doPreDestroy() {Log.info("at PreDestroy");}public void helloWorld() {Log.info("Hello world!");}
}

dispose注解:实现销毁前自定义操作,dispose是另一种可选方案

  • 试想这样的场景:我的bean在销毁前要做自定义操作,但是如果用之前的两种方案,可能面临以下问题:
  1. 不适合修改bean的代码,bean的类可能是第三方库
  2. 也不适合修改生命周期拦截器代码,拦截器可能也是第三方库,也可能是多个bean共用,若修改会影响其他bean
  • 好在quarkus为我们提供了另一个方案,不用修改bean和拦截器的代码,用注解dispose修饰指定方法即可,接下来编码验证
  • 增加一个普通类ResourceManager.java,假设这是业务中的资源管理服务,可以打开和关闭业务资源,稍后会在配置类中将其指定为bean
package com.bolingcavalry.service.impl;import io.quarkus.logging.Log;/*** @author zq2599@gmail.com* @Title: 资源管理类* @Package* @Description:* @date 4/10/22 10:20 AM*/
public class ResourceManager {public ResourceManager () {Log.info("create instance, " + this.getClass().getSimpleName());}/*** 假设再次方法中打开资源,如网络、文件、数据库等*/public void open() {Log.info("open resource here");}/*** 假设在此方法中关闭所有已打开的资源*/public void closeAll() {Log.info("close all resource here");}
}
  • 配置类SelectBeanConfiguration.java,指定了ResourceManager的生命周期是每次http请求
package com.bolingcavalry.config;import com.bolingcavalry.service.impl.ResourceManager;
import javax.enterprise.context.RequestScoped;public class SelectBeanConfiguration {@RequestScopedpublic ResourceManager getResourceManager() {return new ResourceManager();}  
}
  • 再写一个web服务类ResourceManagerController.java,这里面使用了ResourceManager
package com.bolingcavalry;import com.bolingcavalry.service.impl.ResourceManager;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;@Path("/resourcemanager")
public class ResourceManagerController {@InjectResourceManager resourceManager;@GET@Produces(MediaType.TEXT_PLAIN)public String get() {resourceManager.open();return "success";}
}

由于ResourceManager的生命周期是RequestScoped,因此每次请求/resourcemanager都会实例化一个ResourceManager,请求结束后再将其销毁

现在,业务需求是每个ResourceManager的bean在销毁前,都要求其closeAll方法被执行

重点来了,在SelectBeanConfiguration.java中新增一个方法,入参是bean,而且要用Disposes注解修饰,如此,ResourceManager类型的bean在销毁前此方法都会被执行

/*** 使用了Disposes注解后,ResourceManager类型的bean在销毁前,此方法都会执行* @param resourceManager*/
public void closeResource(@Disposes ResourceManager resourceManager) {// 在这里可以做一些额外的操作,不需要bean参与Log.info("do other things that bean do not care");// 也可以执行bean的方法resourceManager.closeAll();
}
  • 最后是单元测试类DisposeTest.java,这里用了注解RepeatedTest表示重复执行,属性值为3,表示重复执行3次
@QuarkusTest
public class DisposeTest {@RepeatedTest(3)public void test() {given().when().get("/resourcemanager").then().statusCode(200)// 检查body内容.body(is("success"));}
}

装饰器(Decorator)

掌握quarkus实现的一个CDI特性:装饰器(Decorator)

  • 实战功能说明

一杯意式浓缩咖啡(Espresso)价格3美元

拿铁(Latte)由意式浓缩+牛奶组成,价格是意式浓缩和牛奶之和,即5美元

焦糖玛奇朵(CaramelMacchiato)由拿铁+焦糖组成,价格比拿铁多了焦糖的1美元,即6美元

每种咖啡都是一种对象,价格由getPrice方法返回

  • 编码实践

    public interface Coffee {/*** 咖啡名称* @return*/String name();/*** 当前咖啡的价格* @return*/int getPrice();
    }
    /*** 意式浓缩咖啡,价格3美元*/
    @ApplicationScoped
    public class Espresso implements Coffee {@Overridepublic String name() {return "Espresso";}@Overridepublic int getPrice() {return 3;}
    }
    @Decorator
    @Priority(11)
    public class Latte implements Coffee {/*** 牛奶价格:2美元*/private static final int MILK_PRICE = 2;/*** 使用quarkus的装饰器功能时,有两件事必须要做:装饰类要用注解Decorator修饰,被装饰类要用注解						Delegate修饰* 因此,Latte被注解Decorator修饰,Latte的成员变量delegate是被装饰类,要用注解Delegate修饰,* Latte的成员变量delegate并未指明是Espresso,quarkus会选择Espresso的bean注入到这里*/@Delegate@InjectCoffee delegate;@Overridepublic String name() {return "Latte";}@Overridepublic int getPrice() {// 将Latte的代理类打印出来,看quarkus注入的是否正确Log.info("Latte's delegate type : " + this.delegate.name());return delegate.getPrice() + MILK_PRICE;}
    }
  • 接下来是CaramelMacchiato类(焦糖玛奇朵),有几处要注意的地方稍后会说明

/*** 焦糖玛奇朵:拿铁+焦糖*/
@Decorator
@Priority(10)
public class CaramelMacchiato implements Coffee {/*** 焦糖价格:1美元*/private static final int CARAMEL_PRICE = 1;@Delegate@InjectCoffee delegate;@Overridepublic String name() {return "CaramelMacchiato";}@Overridepublic int getPrice() {// 将CaramelMacchiato的代理类打印出来,看quarkus注入的是否正确Log.infov("CaramelMacchiato's delegate type : " + this.delegate.name());return delegate.getPrice() + CARAMEL_PRICE;}
}

重要知识点

看到这里,相信您也发现了问题所在:CaramelMacchiato和Latte都有成员变量delegate,其注解和类型声明都一模一样,那么,如何才能保证Latte的delegate注入的是Espresso,而CaramelMacchiato的delegate注入的是Latte呢?

此刻就是注解Priority在发挥作用了,CaramelMacchiato和Latte都有注解Priority修饰,属性值却不同,属性值越大越接近原始类Espresso,如下图,所以,Latte装饰的就是Espresso,CaramelMacchiato装饰的是Latte

流程图 - 2022-04-09T203421.135

@QuarkusTest
public class DecoratorTest {@InjectCoffee coffee;@Testpublic void testDecoratorPrice() {Assertions.assertEquals(6, coffee.getPrice());}
}

猜猜这里注入的谁,很神奇,先放这吧,不明实际应用场景

bean读写锁

  1. 关于多线程同步问题
  2. 代码复现多线程同步问题
  3. quarkus的bean读写锁

直接结论

image-20220417113205821

在deposit和deduct都没有被调用时,get方法可以被调用,而且可以多线程同时调用,因为每个线程都能顺利拿到读锁

一旦deposit或者deduct被调用,其他线程在调用deposit、deduct、get方法时都被阻塞了,因为此刻不论读锁还是写锁都拿不到,必须等deposit执行完毕,它们才重新去抢锁

有了上述逻辑,再也不会出现deposit和deduct同时修改余额的情况了,预测单元测试应该能通过

这种读写锁的方法虽然可以确保逻辑正确,但是代价不小(一个线程执行,其他线程等待),所以在并发性能要求较高的场景下要慎用,可以考虑乐观锁、AtomicInteger这些方式来降低等待代价

学习和改变bean懒加载规则

关于懒加载(Lazy Instantiation

  • CDI规范下的懒加载规则:
  1. 常规作用域的bean(例如ApplicationScoped、RequestScoped),在注入时,实例化的是其代理类,而真实类的实例化发生在bean方法被首次调用的时候
  2. 伪作用域的bean(Dependent和Singleton),在注入时就会实例化
  • quarkus也遵循此规则,接下来编码验证

编码验证懒加载

@ApplicationScoped
public class NormalApplicationScoped {public NormalApplicationScoped() {Log.info("Construction from " + this.getClass().getSimpleName());}public String ping() {return "ping from NormalApplicationScoped";}
}
@Singleton
public class NormalSingleton {public NormalSingleton() {Log.info("Construction from " + this.getClass().getSimpleName());}public String ping() {return "ping from NormalSingleton";}
}
@QuarkusTest
class ChangeLazyLogicTest {@InjectNormalSingleton normalSingleton;@InjectNormalApplicationScoped normalApplicationScoped;@Testvoid ping() {Log.info("start invoke normalSingleton.ping");normalSingleton.ping();Log.info("start invoke normalApplicationScoped.ping");normalApplicationScoped.ping();}
}

改变懒加载规则的第一种手段

让bean尽早实例化的第一种手段,是让bean消费StartupEvent事件,这是quarkus框架启动成功后发出的事件,从时间上来看,此事件的时间比注入bean的时间还要早,这样消费事件的bean就会实例化

咱们给NormalApplicationScoped增加下图红框中的代码,让它消费StartupEvent事件

image-20220501093358565

改变懒加载规则的第二种手段(居然和官方资料有出入)

image-20220501101416574

image-20220501102631368

官方都这么说了,我岂敢不信,不过流程还是要完成的,把修改后的代码再运行一遍,截个图贴到文中,走走过场…

然而,这次运行的结果,却让人精神一振,StartupEvent和Startup效果是不一样的!!!

运行结果如下图,最先实例化的居然不是被Startup注解修饰的NormalApplicationScoped,而是它的代理类!

image-20220501102150488

  • 由此可见,Startup可以将bean的实例化提前,而且是连带bean的代理类的实例化也提前了
  • 回想一下,虽然结果与预期不符合,而预期来自官方注释,但这并不代表官方注释有错,人家只说了句functionally equivalent,从字面上看并不涉及代理类的实例化
  • 另外Startup也有自己的独特之处,一共有以下两点
  1. Startup注解的value属性值,是bean的优先级,这样,多个bean都使用Startup的时候,可以通过value值设置优先级,以此控制实例化顺序(实际上控制的是事件observer的创建顺序)

  2. 如果一个类只有Startup注解修饰,而没有设置作用域的时候,quarkus自动将其作用域设置为ApplicationScoped,也就是说,下面这段代码中,ApplicationScoped注解写不写都一样

    @ApplicationScoped
    @Startup
    public class NormalApplicationScoped {
    

总结

流程图 (3)

拦截器高级特性(属性设置和重复使用)

先定义三个bean

public interface SayHello {void hello();
}
@ApplicationScoped
@Named("A")
public class SayHelloA implements SayHello {@SendMessage@Overridepublic void hello() {Log.info("hello from A");}
}

@ApplicationScoped
@Named("B")
public class SayHelloB implements SayHello {@SendMessage(sendType = "email")@Overridepublic void hello() {Log.info("hello from B");}
}
@ApplicationScoped
@Named("C")
public class SayHelloC implements SayHello {@SendMessage@SendMessage(sendType = "email")@Overridepublic void hello() {Log.info("hello from C");}
}

需求:

要求设计一个拦截器,名为SendMessage,功能是对外发送通知,通知的方式有短信和邮件两种,具体用哪种是可以设置的

用SendMessage拦截器拦截SayHelloA,通知类型是短信

用SendMessage拦截器拦截SayHelloB,通知类型是邮件

用SendMessage拦截器拦截SayHelloC,通知类型是短信和邮件都发送

定义拦截器

@InterceptorBinding
@Repeatable(SendMessage.SendMessageList.class)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SendMessage {/*** 消息类型 : "sms"表示短信,"email"表示邮件* @return*/@NonbindingString sendType() default "sms";@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@interface SendMessageList {SendMessage[] value();}
}
  1. 允许在同一位置重复使用同一个注解,这是java注解的通用功能,并非quarkus独有
  2. 重复使用注解时,必须定义注解容器,用来放置重复的注解,这里的容器是SendMessageList
  3. 使用Repeatable修饰SendMessage,这样就能在同一位置重复使用SendMessage注解了,注意Repeatable的属性值是容器SendMessageList
  4. sendType是注解属性,用来保存通知类型,任何使用SendMessage注解的地方都能通过设置sendType来指定通知类型,如果不指定则使用默认值sms
  5. 要注意sendType的注解Nonbinding,此注解非常重要,如果不添加此注解,在使用SendMessage的时候,设置sendType为email时拦截器不会生效

quarkus对重复使用同一拦截器注解的限制

  • 虽然可以在同一位置重复使用SendMessage拦截器,但是要注意quarkus的限制
  1. 可以作用在方法上
  2. 不能作用在类上
  3. 不能作用在stereotypes上
  • 关于2和3,官方的说法是将来会解决(This might be added in the future)
@SendMessage
@Interceptor
public class SendMessageInterceptor {@AroundInvokeObject execute(InvocationContext context) throws Exception {// 先执行被拦截的方法Object rlt = context.proceed();// 获取被拦截方法的类名String interceptedClass = context.getTarget().getClass().getSimpleName();// 代码能走到这里,表示被拦截的方法已执行成功,未出现异常// 从context中获取通知类型,由于允许重复注解,因此通知类型可能有多个List<String> allTypes = getAllTypes(context);// 将所有消息类型打印出来Log.infov("{0} messageTypes : {1}", interceptedClass, allTypes);// 遍历所有消息类型,调用对应的方法处理for (String type : allTypes) {switch (type) {// 短信case "sms":sendSms();break;// 邮件case "email":sendEmail();break;}}// 最后再返回方法执行结果return rlt;}/*** 从InvocationContext中取出所有注解,过滤出SendMessage类型的,将它们的type属性放入List中返回* @param invocationContext* @return*/private List<String> getAllTypes(InvocationContext invocationContext) {// 取出所有注解Set<Annotation> bindings = InterceptorBindings.getInterceptorBindings(invocationContext);List<String> allTypes = new ArrayList<>();// 遍历所有注解,过滤出SendMessage类型的for (Annotation binding : bindings) {if (binding instanceof SendMessage) {allTypes.add(((SendMessage) binding).sendType());}}return allTypes;}/*** 模拟发送短信*/private void sendSms() {Log.info("operating success, from sms");}/*** 模拟发送邮件*/private void sendEmail() {Log.info("operating success, from email");}
}
@QuarkusTest
public class SendMessageTest {@Named("A")SayHello sayHelloA;@Named("B")SayHello sayHelloB;@Named("C")SayHello sayHelloC;@Testpublic void testSendMessage() {sayHelloA.hello();sayHelloB.hello();sayHelloC.hello();}
}

禁用类级别拦截器

类拦截器和方法拦截器的叠加效果

  • 接下来进行编码,看看作用在类上和方法上的两个拦截器的叠加效果,要新建的文件清单如下
  1. TrackClass.java:定义类级别的拦截器
  2. TrackClassInterceptor.java:拦截器TrackClass的功能实现
  3. TrackMethod.java:方法级别的拦截器
  4. TrackMethodInterceptor.java:拦截器TrackMethod的功能实现
  5. ExcludeInterceptorDemo.java:普通的bean,用TrackClass修饰其类,用TrackMethod修饰其test1方法
  6. ExcludeInterceptorTest.java:单元测试类,运行ExcludeInterceptorDemo的方法,观察拦截效果
@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackClass {
}
@TrackClass
@Interceptor
public class TrackClassInterceptor {@AroundInvokeObject execute(InvocationContext context) throws Exception {Log.info("from TrackClass");return context.proceed();}
}
@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackMethod {
}
@TrackMethod
@Interceptor
public class TrackMethodInterceptor {@AroundInvokeObject execute(InvocationContext context) throws Exception {Log.info("from TrackMethod");return context.proceed();}
}
@ApplicationScoped
@TrackClass
public class ExcludeInterceptorDemo {public void test0() {Log.info("from test0");}@TrackMethodpublic void test1() {Log.info("from test1");}
}
  • 这两种拦截器,在定义上没有任何区别,不过就是该注解可以加在类上(该类的所有方法都将会被拦截),也可以加在方法上(只拦截该方法)

测试

@QuarkusTest
public class ExcludeInterceptorTest {@InjectExcludeInterceptorDemo excludeInterceptorDemo;@Testpublic void test() {excludeInterceptorDemo.test0();Log.info("*****************************");excludeInterceptorDemo.test1();}
}

image-20220502180545876

用注解NoClassInterceptors使类拦截器失效

假设遇到了某些冲突(例如和数据库、IO相关等),导致TrackClassInterceptor和TrackMethodInterceptor两个拦截器不能同时对test1方法进行拦截,只能保留TrackMethodInterceptor

此时,可以用注解NoClassInterceptors修饰test1方法,如下图红框所示,这样类拦截器TrackClassInterceptor就会失效,只剩下TrackMethodInterceptor可以正常工作

image-20220502192040472

NoClassInterceptors的影响范围

  • 回顾类拦截器TrackClassInterceptor,如下图红框,可见其拦截方法有注解AroundInvoke修饰

image-20220502193918403

而NoClassInterceptors的作用,就是针对有注解AroundInvoke修饰的方法,使他们失效

除了AroundInvoke,NoClassInterceptors还针对AroundConstruct修饰的方法,使他们失效

至此,拦截器的高级特性已经全部学习和实践完成,希望能给您提供一些参考,助您设计出更完善的拦截器

其他重要知识点大串讲

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

简化之一:bean注入

  • quarkus在CDI规范的基础上做了简化,可以让我们少写几行代码
  • 将配置文件中名为aaa.name的配置项注入到bean的成员变量greetingMsg中,按照CDI规范的写法如下
@ApplicationScoped
public class ConfigBean {@ConfigProperty(name = "aaa.name")String greetingMsg;public String getGreetingMsg() {return greetingMsg;}
}

简化之二:bean构造方法

  • 关于bean的构造方法,CDI有两个规定:首先,必须要有无参构造方法,其次,有参数的构造方法需要@Inject注解修饰,实例代码如下所示
@ApplicationScoped
public class MyCoolService {private SimpleProcessor processor;MyCoolService() { // dummy constructor needed}@Inject // constructor injectionMyCoolService(SimpleProcessor processor) {this.processor = processor;}
}
  • 但是,在quarkus框架下,无参构造方法可不写,有参数的构造方法也可以略去@Inject,写成下面这样的效果和上面的代码一模一样
@ApplicationScoped
public class MyCoolService {private SimpleProcessor processor;MyCoolService(SimpleProcessor processor) {this.processor = processor;}
}

简化之三:bean生产方法

  • 在CDI规范中,通过方法生产bean的语法如下,可见要同时使用Produces和ApplicationScoped注解修饰返回bean的方法
class Producers {@Produces@ApplicationScopedMyService produceService() {return new MyService(coolProperty);}
}
  • 在quarkus框架下可以略去@Produces,写成下面这样的效果和上面的代码一模一样
class Producers {@ApplicationScopedMyService produceService() {return new MyService(coolProperty);}
}

WithCaching注解:避免不必要的多次实例化

  • 在介绍WithCaching注解之前,先来看一个普通场景
  • 下面是一段单元测试代码,HelloDependent类型的bean通过Instance的方式被注入,再用Instance#get来获取此bean
@Dependent
public class HelloDependent {public HelloDependent(InjectionPoint injectionPoint) {Log.info("injecting from bean "+ injectionPoint.getMember().getDeclaringClass());}public String hello() {return this.getClass().getSimpleName();}
}
@QuarkusTest
public class WithCachingTest {@InjectInstance<HelloDependent> instance;@Testpublic void test() {// 第一次调用Instance#get方法HelloDependent helloDependent = instance.get();helloDependent.hello();// 第二次调用Instance#get方法helloDependent = instance.get();helloDependent.hello();}
}
  • 如果HelloDependent的作用域是ApplicationScoped,上述代码一切正常,但是,如果作用域是Dependent呢?代码中执行了两次Instance#get,得到的HelloDependent实例是同一个吗?Dependent的特性是每次注入都实例化一次,这里的Instance#get又算几次注入呢?

  • 最简单的方法就是运行上述代码看实际效果,这里先回顾HelloDependent.java的源码,如下所示,构造方法中会打印日志,这下好办了,只要看日志出现几次,就知道实例化几次了

image-20220427083442714

  • 现在问题来了:如果bean的作用域必须是Dependent,又希望多次Instance#get返回的是同一个bean实例,这样的要求可以做到吗?

  • 答案是可以,用WithCaching注解修饰Instance即可,改动如下图红框1,改好后再次运行,红框2显示HelloDependent只实例化了一次

image-20220427084522435

拦截静态方法

  • 仅支持方法级别的拦截(即拦截器修饰的是方法)

  • private型的静态方法不会被拦截

  • 下图是拦截器实现的常见代码,通过入参InvocationContext的getTarget方法,可以得到被拦截的对象,然而,在拦截静态方法时,getTarget方法的返回值是null,这一点尤其要注意,例如下图红框中的代码,在拦截静态方法是就会抛出空指针异常

image-20220501162427008

All更加直观的注入

public interface SayHello {void hello();
}
  • 现在有三个bean都实现了SayHello接口,如果想要调用这三个bean的hello方法,应该怎么做呢?
  • 按照CDI的规范,应该用Instance注入,然后使用Instance中的迭代器即可获取所有bean,代码如下
public class InjectAllTest {/*** 用Instance接收注入,得到所有SayHello类型的bean*/@InjectInstance<SayHello> instance;@Testpublic void testInstance() {// instance中有迭代器,可以用遍历的方式得到所有beanfor (SayHello sayHello : instance) {sayHello.hello();}}
}
  • quarkus提供了另一种方式,借助注解io.quarkus.arc.All,可以将所有SayHello类型的bean注入到List中,如下所示
@QuarkusTest
public class InjectAllTest {/*** 用All注解可以将SayHello类型的bean全部注入到list中,* 这样更加直观*/@AllList<SayHello> list;@Testpublic void testAll() {for (SayHello sayHello : list) {sayHello.hello();}}
}
  • 和CDI规范相比,使用All注解可以让代码显得更为直观,另外还有以下三个特点
  1. 此list是immutable的(内容不可变)
  2. list中的bean是按照priority排序的
  3. 如果您需要的不仅仅是注入bean,还需要bean的元数据信息(例如bean的scope),可以将List中的类型从SayHello改为InstanceHandle,这样即可以得到注入bean,也能得到注入bean的元数据(在InjectableBean中),参考代码如下
@QuarkusTest
public class InjectAllTest {@AllList<InstanceHandle<SayHello>> list;@Testpublic void testQuarkusAllAnnonation() {for (InstanceHandle<SayHello> instanceHandle : list) {// InstanceHandle#get可以得到注入beanSayHello sayHello = instanceHandle.get();// InjectableBean封装了注入bean的元数据信息InjectableBean<SayHello> injectableBean = instanceHandle.getBean();// 例如bean的作用域就能从InjectableBean中取得Class clazz = injectableBean.getScope();// 打印出来验证Log.infov("bean [{0}], scope [{1}]", sayHello.getClass().getSimpleName(), clazz.getSimpleName() );}}
}

统一处理异步事件的异常

  • 需要提前说一下,本段落涉及的知识点和AsyncObserverExceptionHandler类有关,而《quarkus依赖注入》系列所用的quarkus-2.7.3.Final版本中并没有AsyncObserverExceptionHandler类,后来将quarkus版本更新为2.8.2.Final,就可以正常使用AsyncObserverExceptionHandler类了

  • 本段落的知识点和异步事件有关:如果消费异步事件的过程中发生异常,而开发者有没有专门写代码处理异步消费结果,那么此异常就默默无闻的被忽略了,我们也可能因此错失了及时发现和处理问题的时机

来写一段代码复现上述问题,首先是事件定义TestEvent.java,就是个普通类,啥都没有

public class TestEvent {
}
@ApplicationScoped
public class TestEventProducer {@InjectEvent<TestEvent> event;/*** 发送异步事件*/public void asyncProduce() {event.fireAsync(new TestEvent());}
}
  • 事件的消费者TestEventConsumer.java,这里在消费TestEvent事件的时候,故意抛出了异常
@ApplicationScoped
public class TestEventConsumer {/*** 消费异步事件,这里故意抛出异常*/public void aSyncConsume(@ObservesAsync TestEvent testEvent) throws Exception {throw new Exception("exception from aSyncConsume");}
}
@QuarkusTest
public class EventExceptionHandlerTest {@InjectTestEventProducer testEventProducer;@Testpublic void testAsync() throws InterruptedException {testEventProducer.asyncProduce();}
}
  • 运行EventExceptionHandlerTest,结果如下图,DefaultAsyncObserverExceptionHandler处理了这个异常,这是quarkus框架的默认处理逻辑

image-20220502205725214

  • DefaultAsyncObserverExceptionHandler只是输出了日志,这样的处理对于真实业务是不够的(可能需要记录到特定地方,调用其他告警服务等),所以,我们需要自定义默认的异步事件异常处理器
  • 自定义的全局异步事件异常处理器如下
@ApplicationScoped
public class NoopAsyncObserverExceptionHandler implements AsyncObserverExceptionHandler {@Overridepublic void handle(Throwable throwable, ObserverMethod<?> observerMethod, EventContext<?> eventContext) {// 异常信息Log.info("exception is - " + throwable);// 事件信息Log.info("observer type is - " + observerMethod.getObservedType().getTypeName());}
}
  • 此刻,咱们再执行一次单元测试,如下图所示,异常已经被NoopAsyncObserverExceptionHandler#handler处理,异常和事件相关的信息都能拿到,您可以按照实际的业务需求来进行定制了

image-20220502210222786

  • 另外还要说明一下,自定义的全局异步事件异常处理器,其作用域只能是ApplicationScoped或者Singleton

小点回顾补充

CDI

官方提醒

在使用依赖注入的时候,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规范的内容(请原谅欣宸的英语水平):
  1. 该规范定义了一组强大的补充服务,有助于改进应用程序代码的结构
  2. 给有状态对象定义了生命周期,这些对象会绑定到上下文,上下文是可扩展的
  3. 复杂的、安全的依赖注入机制,还有开发和部署阶段选择依赖的能力
  4. 与Expression Language (EL)集成
  5. 装饰注入对象的能力(个人想到了AOP,你拿到的对象其实是个代理)
  6. 拦截器与对象关联的能力
  7. 事件通知模型
  8. web会话上下文
  9. 一个SPI:允许便携式扩展与容器的集成(integrate cleanly )

关于CDI的bean

  • CDI的实现(如quarkus),允许对象做这些事情:
  1. 绑定到生命周期上下文
  2. 注入
  3. 与拦截器和装饰器关联
  4. 通过触发和观察事件,以松散耦合的方式交互
  • 上述场景的对象统称为bean,上下文中的 bean 实例称为上下文实例,上下文实例可以通过依赖注入服务注入到其他对象中

创建bean实例:注解修饰在类上

@ApplicationScoped
public class ClassAnnotationBean {public String hello() {return "from " + this.getClass().getSimpleName();}
}
  • 这种注解修饰在类上的bean,被quarkus官方成为class-based beans
  • 使用bean也很简单,如下,用注解Inject修饰ClassAnnotationBean类型的成员变量即可
@Path("/classannotataionbean")
public class ClassAnnotationController {@InjectClassAnnotationBean classAnnotationBean;@GET@Produces(MediaType.TEXT_PLAIN)public String hello() {return String.format("Hello RESTEasy, %s, %s",LocalDateTime.now(),classAnnotationBean.hello());}
}

创建bean实例:注解修饰在方法上

public interface HelloService {String hello();
}
public class HelloServiceImpl implements HelloService {@Overridepublic String hello() {return "from " + this.getClass().getSimpleName();}
}
public class MethodAnnonationBean {@Produces  // 可省略@ApplicationScopedpublic HelloService getHelloService() {return new HelloServiceImpl();}
}
  • 这种用于创建bean的方法,被quarkus称为producer method

  • 看过上述代码,相信聪明的您应该明白了用这种方式创建bean的优点:在创建HelloService接口的实例时,可以控制所有细节(构造方法的参数、或者从多个HelloService实现类中选择一个),没错,在SpringBoot的Configuration类中咱们也是这样做的

  • 前面的getHelloService方法的返回值,可以直接在业务代码中依赖注入,如下所示

@Path("/methodannotataionbean")
public class MethodAnnotationController {@InjectHelloService helloService;@GET@Produces(MediaType.TEXT_PLAIN)public String get() {return String.format("Hello RESTEasy, %s, %s",LocalDateTime.now(),helloService.hello());}
}
  • producer method有个特性需要重点关注:如果刚才生产bean的getHelloService方法有个入参,如下所示,入参是OtherService对象,那么,这个OtherService对象也必须是个bean实例(这就像你用@Inject注入一个bean的时候,这个bean必须存在一样),如果OtherService不是个bean,那么应用初始化的时候会报错,(其实这个特性SpringBoot中也有,相信经验丰富的您在使用Configuration类的时候应该用到过)
public class MethodAnnonationBean {@Produces@ApplicationScopedpublic HelloService getHelloService(OtherService otherService) {return new HelloServiceImpl();}
}

创建bean实例:注解修饰在成员变量上

public class OtherServiceImpl {public String hello() {return "from " + this.getClass().getSimpleName();}
}
public class FieldAnnonationBean {@Produces@ApplicationScopedOtherServiceImpl otherServiceImpl = new OtherServiceImpl();
}
  • 种用于创建bean的成员变量(如上面的otherServiceImpl),被quarkus称为producer field
  • 上述bean的使用方法如下,可见与前面的使用并无区别,都是从quarkus的依赖注入

关于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的作用域

关于bean的作用域(scope)

  • 作为《quarkus依赖注入》系列的第二篇,继续学习一个重要的知识点:bean的作用域(scope),每个bean的作用域是唯一的,不同类型的作用域,决定了各个bean实例的生命周期,例如:何时何处创建,又何时何处销毁

  • bean的作用域在代码中是什么样的?回顾前文的代码,如下,ApplicationScoped就是作用域,表明bean实例以单例模式一直存活(只要应用还存活着),这是业务开发中常用的作用域类型:

@ApplicationScoped
public class ClassAnnotationBean {public String hello() {return "from " + this.getClass().getSimpleName();}
}

请添加图片描述

内置

常规作用域和伪作用域

常规作用域,quarkus官方称之为normal scope,包括:ApplicationScoped、RequestScoped、SessionScoped三种

伪作用域称之为pseudo scope,包括:Singleton、 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 {@InjectClassAnnotationBean 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.执行常规业务代码执行常规代码
  • 至此,您应该明白两种作用域的区别了:伪作用域的bean,在注入的时候实例化,常规作用域的bean,在注入的时候并未实例化,只有它的方法首次执行的时候才会实例化,如下图

RequestScoped

image-20220313094309886

SessionScoped

  1. ApplicationScoped

    • ApplicationScoped算是最常用的作用域了,它修饰的bean,在整个应用中只有一个实例
  2. RequestScoped

    • 这是与当前http请求绑定的作用域,它修饰的bean,在每次http请求时都有一个全新实例,来写一段代码验证
    • 首先是bean类RequestScopeBean.java,注意作用域是RequestScoped,如下,在构造方法中打印日志,这样可以通过日志行数知道实例化次数
    @RequestScoped
    public class RequestScopeBean {/*** 在构造方法中打印日志,通过日志出现次数对应着实例化次数*/public RequestScopeBean() {Log.info("Instance of " + this.getClass().getSimpleName());}public String hello() {return "from " + this.getClass().getSimpleName();}
    }
    • 然后是使用bean的代码,是个普通的web服务类
    @Path("/requestscope")
    public class RequestScopeController {@InjectRequestScopeBean 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次
    @QuarkusTest
    class RequestScopeControllerTest {@RepeatedTest(10)public void testGetEndpoint() {given().when().get("/requestscope").then().statusCode(200)// 检查body内容,是否含有ClassAnnotationBean.hello方法返回的字符串.body(containsString("from " + RequestScopeBean.class.getSimpleName()));}
    }

image-20220313103932524

另外,请重点关注蓝框和蓝色注释文字,这是意外收获,居然看到了代理类的日志,看样子代理类是继承了RequestScopeBean类,于是父类构造方法中的日志代码也执行了,还把代理类的类名打印出来了

从日志可以看出:10次http请求,bean的构造方法执行了10次,代理类的构造方法只执行了一次,这是个重要结论:bean类被多次实例化的时候,代理类不会多次实例化

  1. SessionScoped

    • SessionScoped与RequestScoped类似,区别是范围,RequestScoped是每次http请求做一次实例化,SessionScoped是每个http会话,以下场景都在session范围内,共享同一个bean实例:
    1. servlet的service方法
    2. servlet filter的doFileter方法
    3. web容器调用HttpSessionListener、AsyncListener、ServletRequestListener等监听器
  2. 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,会空指针异常的

    image-20220313094309886

  3. Dependent

    • Dependent是个伪作用域,它的特点是:每个依赖注入点的对象实例都不同

    • 假设DependentClinetA和DependentClinetB都用@Inject注解注入了HelloDependent,那么DependentClinetA引用的HelloDependent对象,DependentClinetB引用的HelloDependent对象,是两个实例,如下图,两个hello是不同的实例

流程图 (18)

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 {@InjectHelloDependent hello;public String doHello() {return hello.hello();}
}
  • DependentClientB的代码和DependentClientA一模一样,就不贴出来了
@QuarkusTest
public class DependentTest {@InjectDependentClientA dependentClientA;@InjectDependentClientB dependentClientB;@Testpublic void testSelectHelloInstanceA() {Class<HelloDependent> clazz = HelloDependent.class;Assertions.assertEquals(clazz.getSimpleName(), dependentClientA.doHello());Assertions.assertEquals(clazz.getSimpleName(), dependentClientB.doHello());}
}

image-20220326172853871

用注解选择注入bean

LookupIfProperty,配置项的值符合要求才能使用bean

LookupUnlessProperty,配置项的值不符合要求才能使用bean

IfBuildProfile,如果是指定的profile才能使用bean

UnlessBuildProfile,如果不是指定的profile才能使用bean

IfBuildProperty,如果构建属性匹配才能使用bean

LookupIfProperty,配置项的值符合要求才能使用bean

注解LookupIfProperty的作用是检查指定配置项,如果存在且符合要求,才能通过代码获取到此bean,

有个关键点请注意:下图是官方定义,可见LookupIfProperty并没有决定是否实例化beam,它决定的是能否通过代码取到bean,这个代码就是Instance来注入,并且用Instance.get方法来获取

  • 定义一个接口TryLookupIfProperty.java
public interface TryLookupIfProperty {String hello();
}
  • 以及两个实现类

    public class TryLookupIfPropertyAlpha implements TryLookupIfProperty {@Overridepublic String hello() {return "from " + this.getClass().getSimpleName();}
    }
    public class TryLookupIfPropertyBeta implements TryLookupIfProperty {@Overridepublic String hello() {return "from " + this.getClass().getSimpleName();}
    }
  • 然后就是注解LookupIfProperty的用法了,如下所示,SelectBeanConfiguration是个配置类,里面有两个方法用来生产bean,都用注解LookupIfProperty修饰,如果配置项service.alpha.enabled的值等于true,就会执行tryLookupIfPropertyAlpah方法,如果配置项service.beta.enabled的值等于true,就会执行tryLookupIfPropertyBeta方法

    public class SelectBeanConfiguration {@LookupIfProperty(name = "service.alpha.enabled", stringValue = "true")@ApplicationScopedpublic TryLookupIfProperty tryLookupIfPropertyAlpha() {return new TryLookupIfPropertyAlpha();}@LookupIfProperty(name = "service.beta.enabled", stringValue = "true")@ApplicationScopedpublic TryLookupIfProperty tryLookupIfPropertyBeta() {return new TryLookupIfPropertyBeta();}
    }
    @QuarkusTest
    public class BeanInstanceSwitchTest {@BeforeAllpublic static void setUp() {System.setProperty("service.alpha.enabled", "true");}// 注意,前面的LookupIfProperty不能决定注入bean是否实力话,只能决定Instance.get是否能取到,//所以此处要注入的是Instance,而不是TryLookupIfProperty本身@InjectInstance<TryLookupIfProperty> service;@Testpublic void testTryLookupIfProperty() {Assertions.assertEquals("from " + tryLookupIfPropertyAlpha.class.getSimpleName(),service.get().hello());}
    }
    • 上述代码有以下两点要注意
    1. 注意TryLookupIfProperty的注入方式,对这种运行时才能确定具体实现类的bean,要用Instance的方式注入,使用时要用Instance.get方法取得bean
    2. 单元测试的BeforeAll注解用于指定测试前要做的事情,这里用System.setProperty设置配置项service.alpha.enabled,所以,理论上SelectBeanConfiguration.tryLookupIfPropertyAlpha方法应该会执行,也就是说注入的TryLookupIfProperty应该是TryLookupIfPropertyAlpha实例,所以testTryLookupIfProperty中用assertEquals断言预测:TryLookupIfProperty.hello的值来自TryLookupIfPropertyAlpha

    image-20220316090323717

LookupUnlessProperty,配置项的值不符合要求才能使用bean

LookupIfProperty和LookupUnlessProperty都有名为lookupIfMissing的属性,意思都一样:指定配置项不存在的时候,就执行注解所修饰的方法,修改SelectBeanConfiguration.java,如下图黄框所示,增加lookupIfMissing属性,指定值为true(没有指定的时候,默认值是false)

image-20220316231842895

IfBuildProfile,如果是指定的profile才能使用bean

应用在运行时,其profile是固定的,IfBuildProfile检查当前profile是否是指定值,如果是,其修饰的bean就能被业务代码使用

对比官方对LookupIfProperty和IfBuildProfile描述的差别,LookupIfProperty决定了是否能被选择,IfBuildProfile决定了是否在容器中

public interface TryIfBuildProfile {String hello();
}
public class TryIfBuildProfileProd implements TryIfBuildProfile {@Overridepublic String hello() {return "from " + this.getClass().getSimpleName();}
}
public class TryIfBuildProfileDefault implements TryIfBuildProfile {@Overridepublic String hello() {return "from " + this.getClass().getSimpleName();}
}
  • 再来看IfBuildProfile的用法,在刚才的SelectBeanConfiguration.java中新增两个方法,如下所示,应用运行时,如果profile是test,那么tryIfBuildProfileProd方法会被执行,还要注意的是注解DefaultBean的用法,如果profile不是test,那么quarkus的bean容器中就没有TryIfBuildProfile类型的bean了,此时DefaultBean修饰的tryIfBuildProfileDefault方法就会被执行,导致TryIfBuildProfileDefault的实例注册在quarkus容器中

    // 两者选择其一个执行
    @Produces
    @IfBuildProfile("test")
    public TryIfBuildProfile tryIfBuildProfileProd() {return new TryIfBuildProfileProd();
    }@Produces
    @DefaultBean
    public TryIfBuildProfile tryIfBuildProfileDefault() {return new TryIfBuildProfileDefault();
    }
  • 单元测试代码写在刚才的BeanInstanceSwitchTest.java中,运行单元测试是profile被设置为test,所以tryIfBuildProfile的预期是TryIfBuildProfileProd实例,注意,这里和前面LookupIfProperty不一样的是:这里的TryIfBuildProfile直接注入就好,不需要Instance来注入

    @Inject
    TryIfBuildProfile tryIfBuildProfile;@Test
    public void testTryLookupIfProperty() {Assertions.assertEquals("from " + TryLookupIfPropertyAlpha.class.getSimpleName(),service.get().hello());
    }@Test
    public void tryIfBuildProfile() {Assertions.assertEquals("from " + TryIfBuildProfileProd.class.getSimpleName(),tryIfBuildProfile.hello());
    }

image-20220320101229556

image-20220318230726955

UnlessBuildProfile,如果不是指定的profile才能使用bean

IfBuildProperty,如果构建属性匹配才能使用bean

  • 最后要提到注解是IfBuildProperty是,此注解与LookupIfProperty类似,下面是两个注解的官方描述对比,可见IfBuildProperty作用的熟悉主要是构建属性(前面的文章中提到过构建属性,它们的特点是运行期间只读,值固定不变)

  • 限于篇幅,就不写代码验证了,来看看官方demo,用法上与LookupIfProperty类似,可以用DefaultBean来兜底,适配匹配失败的场景

    @Dependent
    public class TracerConfiguration {@Produces@IfBuildProperty(name = "some.tracer.enabled", stringValue = "true")public Tracer realTracer(Reporter reporter, Configuration configuration) {return new RealTracer(reporter, configuration);}@Produces@DefaultBeanpublic Tracer noopTracer() {return new NoopTracer();}
    }

配置

请添加图片描述

@Path("/actions")
public class HobbyResource {@ConfigProperty(name = "greeting.message")String message;@GET@Produces(MediaType.TEXT_PLAIN)public String hello() {return "Hello RESTEasy, " + LocalDateTime.now() + " [" + message + "]";}
}

配置方式一览

config sources

方式一:System properties

java -Dgreeting.message="from system properties" -jar hello-quarkus-1.0-SNAPSHOT-runner.jar

方式二:Environment variables

  • 在设置环境变量时,要注意转换规则:全大写、点号变下划线,因此greeting.message在环境变量中应该写成GREETING_MESSAGE

  • 打开控制台,执行以下命令,即可在当前会话中设置环境变量:

export GREETING_MESSAGE="from Environment variables"

方式三:.env file

  • 为了避免之前的操作带来的影响,请重新打开一个控制台
  • 在pom.xml文件所在目录新建文件.env,内容如下:
GREETING_MESSAGE=from .env file
  • 这种配置方式有个问题要注意:.env中的配置,在代码中使用System.getenv(String)无法取得
  • 官方建议不要将.env文件提交到git、svn等版本控制工具中

方式四:config目录下的application.properties

为了避免之前的操作带来的影响,请删除刚才创建的.env文件

于hello-quarkus-1.0-SNAPSHOT-runner.jar文件所在目录,新建文件夹config

在config文件夹下新建文件application.properties,内容如下:

greeting.message=from config/application.properties

方式五:src/main/resources目录下的application.properties

  • 了避免之前的操作带来的影响,请删除刚才创建的config文件夹(里面的文件也删除)
  • src/main/resources目录下的application.properties,这个配置相信您应该很熟悉,SpringBoot也是这样配置的

方式六:MicroProfile Config configuration file

为了避免之前的操作带来的影响,请将src/main/resources/application.properties文件中的greeting.message配置项删除

MicroProfile是一个 Java 微服务开发的基础编程模型,它致力于定义企业 Java 微服务规范,其中的配置规范有如下描述:

image-20220306100652974

图红框指出了MicroProfile规定的配置文件位置,咱们来试试在此位置放置配置文件是否能生效

如下图红框,在工程的src/main/resources/META-INF目录下新建文件microprofile-config.properties,内容如黄框所示

image-20220306102242975

注意:microprofile-config.properties文件所在目录是src/main/resources/META-INF,不是src/main/resources/META-INF/resources

至此,六种配置方式及其实例验证都完成了,您可以按照自己的实际情况灵活选择

配置内容:常规

  • 现在我们知道了通过何种途径将配置信息传给应用,接下来要看的是配置信息本身:我们可以在配置文件中输入哪些内容呢?
  • 最常用的当然是字符串类型的键值对了,如下所示,刚才一直在用的,就不赘述了:
greeting.message=from config/application.properties

配置内容:引用其他配置

  • 配置项的值可以引用其他配置项,如下所示,greeting.message的值由两部分拼接而成:固定的hello, 、以及配置项greeting.name的值,表达式的格式是**${配置项名称:配置项找不到时的默认值}**,:xxxxxx的意思是如果找不到配置项greeting.name,就用字符串xxxxxx代替
greeting.name=Will
greeting.message=hello, ${greeting.name:xxxxxx}

配置内容:UUID

  • 当同一个应用同时在多个机器上运行时,如何让每个进程有个独立的身份?
  • quarkus提供了一个生成UUID的方式,可以低成本解决上述问题,如下所示,应用启动时,${quarkus.uuid}会生成一个UUID,此时的greeting.message的值也是唯一的
greeting.message=hello, ${quarkus.uuid}
  • 多刷几次浏览器,UUID始终不变,看来此UUID在整个进程存活期间都不会改变
  • 重启应用,再用浏览器访问,如下图,UUID已更新,看来进程身份的唯一性可以通过此配置来保证

配置内容:集合

  • 集合类型的配置也是常见需求,下面是常规的集合配置
my.collection=dog,cat,turtle
  • 对应的代码如下,可见只要被ConfigProperty修饰的成员变量是集合类型就行
@Path("/actions")
public class HobbyResource {@ConfigProperty(name = "my.collection")List<String> message;@GET@Produces(MediaType.TEXT_PLAIN)public String hello() {return "Hello RESTEasy, " + LocalDateTime.now() + ", " + message + "";}
}
  • 还可以将集合中的每个元素分开写,如下所示,代码不变,效果和前面的配置一样
my.collection[0]=dog
my.collection[1]=cat,turtle
my.collection[2]=turtle

使用配置

整篇文章由以下内容构成:

  1. 创建工程,作为演示使用配置项操作的代码
  2. 演示最基本的使用配置项操作
  3. 展示配置项不存时会导致什么问题
  4. 演示如何设置默认值,这样配置项不存在也不会出错
  5. 默认值是字符串,而实际的变量可以是多种类型,它们之间的关系
  6. Optional类型的配置注入
  7. 不用注解注入,也可以写代码获取配置
  8. 针对相同前缀的配置项,使用配置接口简化代码
  9. 使用配置接口嵌套,简化多级的相同前缀配置项
  10. 用map接受配置信息(减少配置项相关代码量)
  11. quarkus及其扩展组件的内置配置项

最基本的配置

greeting.message = hello from application.properties
@ConfigProperty(name = "greeting.message") String message;

配置项不存在导致的异常

image-20220306112030133

带默认值的配置

  • 对于上面演示的配置项不存在导致启动失败问题,可以给ConfigProperty注解设置默认值,这样一旦找不到配置项,就使用默认值注入,可以避免启动失败了
  • HobbyResource.java的源码如下,成员变量notExistsConfig的注解了增加属性defaultValue
@Path("/actions")
public class HobbyResource {// 配置文件中不存在名为not.exists.config的配置项@ConfigProperty(name = "not.exists.config", defaultValue = "112233")String notExistsConfig;@GET@Produces(MediaType.TEXT_PLAIN)public String hello() {return "Hello RESTEasy, " + LocalDateTime.now() + ", [" + notExistsConfig + "]";}
}

defaultValue属性的自动转换

对于ConfigProperty注解的defaultValue属性还有一点要注意,来看ConfigProperty的源码,如下图,红框显示defaultValue的类型是String

在这里插入图片描述

上图中,defaultValue的注释有说明:如果ConfigProperty注解修饰的变量并非String型,那么defaultValue的字符串就会被自动quarkus字符转换

例如修饰的变量是int型,那么defaultValue的String类型的值会被转为int型再赋给变量,如下所示,notExistsConfig是int型,defaultValue的字符串可以被转为int:

// 配置文件中不存在名为not.exists.config的配置项
@ConfigProperty(name = "not.exists.config", defaultValue = "123")
int notExistsConfig;
  • 除了上面试过的int,还有很多种类型都支持从defaultValue的字符串值被自动转换,它们是:
  1. 基础类型:如boolean, byte, short
  2. 装箱类型:如java.lang.Boolean, java.lang.Byte, java.lang.Short
  3. Optional类型:java.util.Optional, java.util.OptionalInt, java.util.OptionalLong, and java.util.OptionalDouble
  4. java枚举
  5. java.time.Duration
  6. JDK网络对象:如java.net.SocketAddress, java.net.InetAddress
@ConfigProperty(name = "server.address", defaultValue = "192.168.1.1")
InetAddress serverAddress;
  • 如果ConfigProperty修饰的变量是boolean型,或者Boolean型,则defaultValue值的自动转换逻辑有些特别: “true”, “1”, “YES”, “Y” "ON"这些都会被转为true(而且不区分大小写,"on"也被转为true),其他值会被转为false

  • 还有一处要注意的:defaultValue的值如果是空字符串,就相当于没有设置defaultValue,此时如果在配置文件中没有该配置项,启动应用会报错

支持Optional

  • 支持Optional这个特性很赞,首先Optional类型的成员变量可直接用于函数式编程,其次配置项不存在时又能避免启动失败
  • 接下来试试用ConfigProperty注解修饰Optional类型的成员变量
  • HobbyResource.java的源码如下,optionalMessage是Optional类型的成员变量,配置项optional.message就算不存在,应用也能正常启动,并且optionalMessage直接用于函数式编程中(optionalMessage.ifPresent)
@Path("/actions")
public class HobbyResource {// 配置文件中存在名为greeting.message的配置项@ConfigProperty(name = "greeting.message")String message;// 配置文件中,不论是否存在名为optional.message的配置项,应用都不会抛出异常@ConfigProperty(name = "optional.message")Optional<String> optionalMessage;@GET@Produces(MediaType.TEXT_PLAIN)public String hello() {List<String> list = new ArrayList<>();list.add(message);// 只有配置项optional.message存在的时候,才会执行list.add方法optionalMessage.ifPresent(list::add);return "Hello RESTEasy, " + LocalDateTime.now() + ", " + list;}
}

编码获取配置项

  • 除了用ConfigProperty注解来获取配置项的值,还可以用写代码的方式获取
  • 下面的代码展示了通过API获取配置项的操作,请注意代码中的注释
@Path("/actions")
public class HobbyResource {@GET@Produces(MediaType.TEXT_PLAIN)public String hello() {List<String> list = new ArrayList<>();// 可以用静态方法取得Config实例Config config = ConfigProvider.getConfig();// getValue可取得指定配置项的指定类型值String greet = config.getValue("greeting.message", String.class);list.add(greet);// getOptionalValue可以将配置项的值包状为Optional对象,如果配置项不存在,也不会报错Optional<String> optional = config.getOptionalValue("not.exists.config", String.class);// 函数式编程:只用optional中有对象时,才会执行list.add方法optional.ifPresent(list::add);return "Hello RESTEasy, " + LocalDateTime.now() + ", " + list;}
}

另外,官方建议不要使用System.getProperty(String) 和 System.getEnv(String)去获取配置项了,它们并非quarkus的API,因此quarkus配置相关的功能与它们并无关系(例如感知配置变化、自动转换类型等)

配置接口

  • 假设配置项如下,都是相同的前缀student
student.name=Tom
student.age=11
student.description=He is a good boy
  • 针对上述配置项,可以用注解ConfigMapping将这些它们集中在一个接口类中获取,接口类StudentConfiguration.java如下
@ConfigMapping(prefix = "student")
public interface StudentConfiguration {/*** 名字与配置项一致* @return*/String name();/*** 名字与配置项一致,自动转为int型* @return*/int age();/*** 名字与配置项不一致时,用WithName注解指定配置项* @return*/@WithName("description")String desc();/*** 用WithDefault注解设置默认值,如果配置项"student.favorite"不存在,则默认值生效* @return*/@WithDefault("default from code")String favorite();
}/// 这个好强

配置项是多个单词时,如何对应配置接口的方法?

  • 首先要看您的匹配项的命名风格,对多个单词是如何分隔的,一般有这三种:
  1. 减号分隔:student-number
  2. 下划线分隔:student_number
  3. 驼峰命名:studentNumber
  • ConfigMapping注解提供了namingStrategy的属性,其值有三种,分别对应上述三种命名风格,您根据自身情况选用即可
  1. KEBAB_CASE(默认值):减号分隔的配置项转为驼峰命令的方法,配置项student-number对应的方法是studentNumber
  2. SNAKE_CASE:下划线分隔的配置项转为驼峰命令的方法,配置项student_number对应的方法是studentNumber
  3. VERBATIM:完全对应,不做任何转换,配置项student_number对应的方法是student_number
@ConfigMapping(prefix = "student", namingStrategy = ConfigMapping.NamingStrategy.SNAKE_CASE)
public interface StudentConfiguration {/*** 名字与配置项一致* @return*/String name();...

配置接口嵌套

  • 再来看下面的配置,有两个配置项的前缀都是student.address,给人的感觉像是student对象里面有个成员变量是address类型的,而address有两个字段:province和city
student.name=Tom
student.age=11
student.description=He is a good boystudent.address.province=guangdong
student.address.city=shenzhen
  • 针对上述配置,quarkus支持用接口嵌套来导入,具体做法分为两步,首先新增一个接口Address.java,源码如下
public interface Address {String province();String city();
}
  • 在配置接口StudentConfiguration.java中,增加下图红框中的一行代码(接口中返回接口,形成接口嵌套)

image-20220310083629464

  • 最后,修改HobbyResource.java代码,增加下图红框中的两行,验证能否正常取得address前缀的配置项目

image-20220310083947856

配置项转为map

  • 前面的接口嵌套,虽然将多层级的配置以对象的形式清晰的表达出来,但也引出一个问题:配置越多,接口定义或者接口方法就越多,代码随之增加

  • 如果配置项的层级简单,还有种简单的方式将其映射到配置接口中:转为map

    student.address.province=guangdong
    student.address.city=shenzhen
    

    对应的代码改动如下图,只要把address方法的返回值从Address改为Map<String, String>即可,这样修改后,address层级下面再增加配置项,也不用修改配置项有关的代码了:

image-20220311080316711

image-20220311080529522

内置配置项

  • quarkus有很多内置的配置项,例如web服务的端口quarkus.http.port就是其中一个,如果您熟悉SpringBoot的话,对这些内置配置项应该很好理解,数据库、消息、缓存,都有对应配置项

    篇幅所限就不在此讲解quarkus内置的配置项了,您可以参考这份官方提供的配置项列表,里面有详细说明:quarkus.io/guides/all-…

    上述文档中,有很多配置项带有加锁的图标,如下图红框所示,有这个图标的配置项,其值在应用构建的时候已经固定了,在应用运行期间始终保持只读状态 在这里插入图片描述

    这种带有加锁图标的配置项的值,在应用运行期间真的不能改变了吗?其实还是有办法的,官方文档指明,如果业务的情况特殊,一定要变,就走热部署的途径,您可以参考《quarkus实战之四:远程热部署》

    官方对开发者的建议:在开发quarkus应用的时候,不要使用quarkus作为配置项的前缀,因为目前quarkus框架及其插件们的配置项的前缀都是quarkus,应用开发应该避免和框架使用相同的配置项前缀,以免冲突

profile

设定profile

  • profile自己是个普通的配置项,例如在application.properties文件中,是这样设置profile的
# 这个配置信息在各个环境中都是相同的
quarkus.profile=dev# 如果不指定profile,就使用此配置
quarkus.http.port=8080
  • 也可以在System properties中设置,如下所示,如此以来,不同环境只有启动命令不同,配置文件可以完全不用修改:
java -Dquarkus.profile="dev" -jar hello-quarkus-1.0-SNAPSHOT-runner.jar

同一个配置项在不同profile时的值

  • profile的格式是%{profile-name}.config.name
  • 以刚才的配置为例,quarkus.http.port配置项共出现三次,前两次带有前缀,格式是百分号+profile名称+点号,如下所示
# 指定当前profile
quarkus.profile=dev# 这个配置信息在各个环境中都是相同的
greeting.message=hello# 如果profile为dev,就是用此配置
%dev.quarkus.http.port=8081
# 如果profile为production,就是用此配置
%production.quarkus.http.port=8082
# 如果不指定profile,或者profile既不是dev也不是production,就使用此配置
quarkus.http.port=8080

需要大写的场景

  • 在《quarkus实战之六:配置》一文中,曾提到过配置方式有六种,有几种要求配置项大写,例如在.env中的配置,此时格式变成了_{PROFILE}_CONFIG_KEY=value,举例如下
# 这个配置信息在各个环境中都是相同的
GREETING_MESSAGE=hello# 如果profile为dev,就是用此配置
_DEV_QUARKUS_HTTP_PORT=8081# 如果profile为production,就是用此配置
_PRODUCTION_QUARKUS_HTTP_PORT=8082# 如果不指定profile,就使用此配置
QUARKUS_HTTP_PORT=8080
  • 注意,实测发现在.env中配置QUARKUS_PROFILE=dev无效,也就是说不能在.env中指定profile,此时应该在启动命令中指定profile,例如:
java -Dquarkus.profile=dev -jar hello-quarkus-1.0-SNAPSHOT-runner.jar

不指定profile时的默认值

  • 不指定profile的时候,quarkus会给profile设置默认值,有三种可能:dev、test、prod,具体逻辑如下:

      如果启动命令是mvn quarkus:dev,profile等于dev,如下图,大家应该见过多次了:
    

在这里插入图片描述

  • 单元测试期间,例如执行命令mvn test,profile等于test

在这里插入图片描述

  • 以上两种场景之外,profile等于prod,例如用命令java -jar hello-quarkus-1.0-SNAPSHOT-runner.jar启动应用

在这里插入图片描述

每个profile对应一个配置文件

  • 如果您希望每个profile都有自己的配置文件,quarkus也支持,如下所示,src/main/resources/目录下同时存在两个配置文件:application.properties和application-staging.properties
resources
├── META-INF
│   └── resources
│       └── index.html
├── application-staging.properties
└── application.properties
  • application.properties内容如下
shell
复制代码greeting.message=hello
quarkus.http.port=8080
  • application-staging.properties内容如下
shell
复制代码greeting.message=hello
quarkus.http.port=8081
  • 如果启动命令指定了profile,如mvn quarkus:dev -Dquarkus.profile=staging,此时只有application-staging.properties文件生效,如下图

image-20220309081432688

  • 还要注意一点:此时如果指定一个不存在的profile,例如mvn quarkus:dev -Dquarkus.profile=xxxxxxx,此时生效的是application.properties文件生效,如下图

image-20220309081901953

Parent Profile

  • parent profile解决的问题是:假设当前profile是aaa,那么配置项xxx对应的配置名应该是%dev.aaa,如果找不到%dev.aaa,就去找它的parent profile对应的配置项,来看个例子就清楚了,假设配置信息如下:
# 指定profile的名字
quarkus.profile=dev
# 指定parent的名字
quarkus.config.profile.parent=common%common.quarkus.http.port=9090
%dev.quarkus.http.ssl-port=9443quarkus.http.port=8080
quarkus.http.ssl-port=8443

当前profile已经指定为dev

parent profile已经指定为common

对于配置项quarkus.http.port,由于没找到%dev.quarkus.http.port,就去找parent profile的配置,于是找到了%common.quarkus.http.port,所以值为9090

对于配置项quarkus.http.ssl-port,由于找到了%dev.quarkus.http.ssl-port,所以值为9443

对于配置项quarkus.http.port,如果%dev.quarkus.http.port和%common.quarkus.http.port都不存在,会用quarkus.http.port,值为8080

修改默认profile

  • 前面曾说到,启动的时候如果不指定profile,quarkus会指定默认的profile:将应用制作成jar,以java -jar命令启动时,profile会被设置为prod
  • 如果您想让默认值从prod变为其他值,可以在构建的时候用-Dquarkus.profile去改变它,例如下面这个命令,jar包生成后,启动的时候默认profile是prod-aws
mvn clean package -U -Dquarkus.package.type=uber-jar -Dquarkus.profile=prod-aws
  • 启动jar的时候不指定profile,如下图,profile已被设定为prod-aws

image-20220309085425879

三个关键注意事项(重要)

  • quarkus官方给出了三个重点注意事项
  1. 应用在运行时,只会有一种profile生效
  2. 如果想在代码获取当前的profile,可以用此API
io.quarkus.runtime.configuration.ProfileManager#getActiveProfile
  1. 用注解的方式获取profile是无效的,下面这段代码无法得到当前的profile
@ConfigProperty("quarkus.profile")String profile;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/15574.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

论文笔记--GloVe: Global Vectors for Word Representation

论文笔记--GloVe: Global Vectors for Word Representation 1. 文章简介2. 文章概括3 文章重点技术3.1 两种常用的单词向量训练方法3.2 GloVe3.3 模型的复杂度 4. 文章亮点5. 原文传送门6. References 1. 文章简介 标题&#xff1a;GloVe: Global Vectors for Word Representa…

自己实现MyBatis 底层机制--抽丝剥茧(上)

&#x1f600;前言 本篇博文是学习过程中的笔记和对于MyBatis底层机制的分析思路&#xff0c;希望能够给您带来帮助&#x1f60a; &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以帮助到…

(树) 剑指 Offer 27. 二叉树的镜像 ——【Leetcode每日一题】

❓剑指 Offer 27. 二叉树的镜像 难度&#xff1a;简单 请完成一个函数&#xff0c;输入一个二叉树&#xff0c;该函数输出它的镜像。 例如输入&#xff1a; 4/ \2 7/ \ / \1 3 6 9镜像输出&#xff1a; 4/ \7 2/ \ / \9 6 3 1示例 1&#xff1a; 输…

28_计算机网络(Computer Networks)基础

本篇介绍计算机网络的基础知识。 文章目录 1. 计算机网络历史2. 以太网" (Ethernet)2.1 以太网" (Ethernet)的简单形式及概念2.2 指数退避解决冲突问题2.3 利用交换机减少同一载体中设备2.4 互联网&#xff08;The Internet&#xff09;2.5 路由(routing)2.6 数据包…

基于峰谷分时电价引导下的电动汽车充电负荷优化(matlab代码)

目录 1 主要内容 峰谷电价优化 电动汽车充电负荷变化 2 部分代码 3 程序结果 1 主要内容 该程序基本复现《基于峰谷分时电价引导下的电动汽车充电负荷优化》&#xff0c;代码主要做的是基于NSGA-II的电动汽车充电负荷优化&#xff0c;首先&#xff0c;在研究电动汽车用户充…

重生之我要学C++第五天

这篇文章主要内容是构造函数的初始化列表以及运算符重载在顺序表中的简单应用&#xff0c;运算符重载实现自定义类型的流插入流提取。希望对大家有所帮助&#xff0c;点赞收藏评论&#xff0c;支持一下吧&#xff01; 目录 构造函数进阶理解 1.内置类型成员在参数列表中的定义 …

【云原生】Docker容器命令监控+Prometheus监控平台

目录 1.常用命令监控 docker ps docker top docker stats 2.weave scope 1.下载 2.安装 3.访问查询即可 3.Prometheus监控平台 1.部署数据收集器cadvisor 2.部署Prometheus 3.部署可视化平台Gragana 4.进入后台控制台 1.常用命令监控 docker ps [rootlocalhost ~…

重新审视MHA与Transformer

本文将基于PyTorch源码重新审视MultiheadAttention与Transformer。事实上&#xff0c;早在一年前博主就已经分别介绍了两者&#xff1a;各种注意力机制的PyTorch实现、从零开始手写一个Transformer&#xff0c;但当时的实现大部分是基于d2l教程的&#xff0c;这次将基于PyTorch…

opencv顺时针,逆时针旋转视频并保存视频

原视频 代码 import cv2# 打开视频文件 video cv2.VideoCapture(inference/video/lianzhang.mp4)# 获取原视频的宽度和高度 width int(video.get(cv2.CAP_PROP_FRAME_WIDTH)) height int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))# 创建视频编写器并设置输出视频参数 fourcc …

【C++】类和对象(下)

1、初始化列表 初始化列表&#xff1a;以一个冒号开始&#xff0c;接着是一个以逗号分隔的数据成员列表&#xff0c;每个"成员变量"后面跟一个放在括号中的初始值或表达式。 class Date { public:Date(int year, int month, int day): _year(year), _month(month), _…

OSPF协议RIP协议+OSPF实验(eNSP)

本篇博客主要讲解单区域的ospf&#xff0c;多区域的仅作了解。 目录 一、OSPF路由协议概述 1.内部网关协议和外部网关协议 二、OSPF的应用环境 1.从以下几方面考虑OSPF的使用 2.OSPF的特点 三、OSPF重要基本概念 3.1&#xff0c;辨析邻居和邻接关系以及七种邻居状态 3…

【MySQL】索引与B+树

【MySQL】索引与B树 索引概念前导硬件软件方面 索引的理解单个page多个page引入B树B树的特征为什么B树做索引优于其他数据结构&#xff1f;聚簇索引与非聚簇索引辅助索引 索引的创建主键索引的创建和查看唯一键索引的创建和查看普通索引的创建和查看复合索引全文索引索引的其他…

js全端支持的深拷贝structuredClone

Jul 7, 2023 经过一年半的试用&#xff0c;structuredClone转正了&#xff0c;全端可以正式使用。 https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

OpenHarmony开源鸿蒙学习入门 - 基于3.2Release 应用开发环境安装

OpenHarmony开源鸿蒙学习入门 - 基于3.2Release 应用开发环境安装 基于目前官方master主支&#xff0c;最新文档版本3.2Release&#xff0c;更新应用开发环境安装文档。 一、安装IDE&#xff1a; 1.IDE安装的系统要求 2.IDE下载官网链接&#xff08;IDE下载链接&#xff09; …

Modbus tcp转ETHERCAT在Modbus软件中的配置方法

Modbus tcp和ETHERCAT是两种不同的协议&#xff0c;这给工业生产带来了很大的麻烦&#xff0c;因为这两种设备之间无法通讯。但是&#xff0c;捷米JM-ECT-TCP网关的出现&#xff0c;却为这个难题提供了解决方案。 JM-ECT-TCP网关能够连接到Modbus tcp总线和ETHERCAT总线中&…

网络面试合集

传输层的数据结构是什么&#xff1f; 就是在问他的协议格式&#xff1a;UDP&TCP 2.1.1三次握手 通信前&#xff0c;要先建立连接&#xff0c;确保双方都是在线&#xff0c;具有数据收发的能力。 2.1.2四次挥手 通信结束后&#xff0c;会有一个断开连接的过程&#xff0…

Qsys介绍

文章目录 前言一、为什么需要Qsys1、简化了系统的设计流程2、Qsys涉及的技术 二、Qsys真身1、一种系统集成工具2、何为Nios II1、内核架构2、Nios II选型 三、Qsys设计涉及到的软件&工具四、总结五、参考资料 前言 Qsys是Altera下的一个系统集成工具&#xff0c;可用于搭建…

APP自动化测试-Python+Appium+Pytest+Allure框架实战封装(详细)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 pytest只是单独的…

JVM入门篇-JVM的概念与学习路线

JVM入门篇-JVM的概念与学习路线 什么是 JVM 定义 Java Virtual Machine - java 程序的运行环境&#xff08;java 二进制字节码的运行环境&#xff09; 好处 一次编写&#xff0c;到处运行自动内存管理&#xff0c;垃圾回收功能数组下标越界检查多态 比较 jvm jre jdk 常…

单片机第一季:零基础12——I2C和EEPROM

目录 1&#xff0c;EEPROM 2&#xff0c;I2C 2.1&#xff0c;I2C物理层 2.2&#xff0c;I2C协议层 3&#xff0c;AT24C02介绍 4&#xff0c;代码 1&#xff0c;EEPROM 为什么需要EEPROM&#xff1f; 单片机内部的ROM只能在程序下载时进行擦除和改写&#xff0c;但是…