此篇只做总结,有大佬做的更详细
大佬quarkus笔记
在应用中,一个接口有多个实现是很常见的,那么依赖注入时,如果类型是接口,如何准确选择实现呢?
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface MyQualifier {
@Nonbinding String value();
}
修饰符匹配要注意的地方
修饰符匹配的逻辑非常简单:bean定义和bean注入的地方用一个修饰符即可,使用中有三个笛梵要注意
@QuarkusTest
public class InstanceTest {
@Inject
Instance<HelloInstance> instance;
@Test
public void testSelectHelloInstanceA() {
Class<HelloInstanceA> clazz = HelloInstanceA.class;
Assertions.assertEquals(clazz.getSimpleName(),
instance.select(clazz).get().hello());
}
@Test
public void testSelectHelloInstanceB() {
Class<HelloInstanceB> clazz = HelloInstanceB.class;
Assertions.assertEquals(clazz.getSimpleName(),
instance.select(clazz).get().hello());
}
}
定义和使用拦截器一共需要做三件事
@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
*/
@AroundInvoke
Object 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 {
@Inject
HandleErrorDemo handleErrorDemo;
@Test
public 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 {
@AroundConstruct
void execute(InvocationContext context) throws Exception {
// 执行业务逻辑可以在此
Log.infov("start construction interceptor");
// 执行bean的构造方法
context.proceed();
// 注意,对于context.getTarget()的返回值,此时不是null,如果在context.proceed()之前,则是null
Log.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 {
@Inject
HandleonstructionDemo handleonstructionDemo;
@Test
public 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 {
@AroundInvoke
Object 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 {
@Inject
TrackParamsDemo trackParamsDemo;
@Test
public 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 {
// 取出保存拦截器间共享数据的map
Map<String, Object> map = context.getContextData();
List<String> list;
String instanceClassName = this.getClass().getSimpleName();
// 根据指定key从map中获取一个list
if (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 {
@AroundInvoke
Object execute(InvocationContext context) throws Exception {
return super.execute(context);
}
}
@ContextData
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 2)
public class ContextDataInterceptorB extends BaseContextDataInterceptor {
@AroundInvoke
Object 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 {
@Inject
ContextDataDemo contextDataDemo;
@Test
public void testContextData() {
contextDataDemo.hello();
}
}
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();
}
@Override
public String toString() {
return "MyEvent{" +
"source='" + source + '\'' +
", consumeNum=" + getNum() +
'}';
}
}
@ApplicationScoped
public class MyProducer {
@Inject
Event<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 {
@Inject
MyProducer myProducer;
@Inject
MyConsumer myConsumer;
@Test
public void testSync() {
Assertions.assertEquals(1, myProducer.syncProduce("testSync"));
}
}
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方法,将响应逻辑(对事件消费结果的处理)传入,这段响应逻辑会在事件消费结束后被执行,上述代码中的响应逻辑是检查异常,若有就打印
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();
}
@Test
public void testAsync() throws InterruptedException {
Assertions.assertEquals(0, myProducer.asyncProduce("testAsync"));
// 如果不等待的话,主线程结束的时候会中断正在消费事件的子线程,导致子线程报错
Thread.sleep(150);
}
从技术上分析,实现上述功能的关键点是:消息的消费者要精确过滤掉不该自己消费的消息
此刻,您是否回忆起前面文章中的一个场景:依赖注入时,如何从多个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();
}
@Override
public 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
@Admin
Event<TwoChannelEvent> adminEvent;
@Inject
@Normal
Event<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();
}
}
注解修饰,相当于为它们添加了不同的标签,在消费的时候也可以用这两个注解来过滤
@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 {
@Inject
TwoChannelWithTwoEvent twoChannelWithTwoEvent;
@Test
public void testTwoChnnelWithTwoEvent() {
// 对管理员来说,
// TwoChannelConsumer.adminEvent消费时计数加2,
// TwoChannelConsumer.allEvent消费时计数加1,
// 所以最终计数是3
Assertions.assertEquals(3, twoChannelWithTwoEvent.produceAdmin("admin"));
// 对普通人员来说,
// TwoChannelConsumer.normalEvent消费时计数加1,
// TwoChannelConsumer.allEvent消费时计数加1,
// 所以最终计数是2
Assertions.assertEquals(2, twoChannelWithTwoEvent.produceNormal("normal"));
}
}
/**
* @author will
* @email [email protected]
* @date 2022/4/3 10:16
* @description 用同一个事件结构体TwoChannelEvent,分别发送不同业务类型的事件
*/
@ApplicationScoped
public class TwoChannelWithSingleEvent {
@Inject
Event<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();
}
}
@QuarkusTest
public class EventTest {
@Inject
TwoChannelWithSingleEvent twoChannelWithSingleEvent;
@Test
public void testTwoChnnelWithSingleEvent() {
// 对管理员来说,
// TwoChannelConsumer.adminEvent消费时计数加2,
// TwoChannelConsumer.allEvent消费时计数加1,
// 所以最终计数是3
Assertions.assertEquals(3, twoChannelWithSingleEvent.produceAdmin("admin"));
// 对普通人员来说,
// TwoChannelConsumer.normalEvent消费时计数加1,
// TwoChannelConsumer.allEvent消费时计数加1,
// 所以最终计数是2
Assertions.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();
}
上述代码中,以下几处需要注意
@InterceptorBinding
@Target({TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackLifeCycle {
}
然后是实现拦截器的功能,有几处要注意的地方稍后会提到
@TrackLifeCycle
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 1)
public class LifeCycleInterceptor {
@AroundConstruct
void execute(InvocationContext context) throws Exception {
Log.info("start AroundConstruct");
try {
context.proceed();
} catch (Exception e) {
e.printStackTrace();
}
Log.info("end AroundConstruct");
}
@PostConstruct
public void doPostConstruct(InvocationContext ctx) {
Log.info("life cycle PostConstruct");
}
@PreDestroy
public 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 {
@Inject
Hello hello;
@Test
public void testLifyCycle() {
hello.helloWorld();
}
}
刚才的拦截器模式有个明显问题:如果不同bean的生命周期回调有不同业务需求,该如何是好?为每个bean做一个拦截器吗?随着bean的增加会有大量拦截器,似乎不是个好的方案
如果您熟悉spring,对下面的代码要改不陌生,这是来自spring官网的内容,直接在bean的方法上用PostConstruct和PreDestroy修饰,即可在bean的创建完成和销毁前被调用
public class CachingMovieLister {
@PostConstruct
public void populateMovieCache() {
// populates the movie cache upon initialization...
}
@PreDestroy
public void clearMovieCache() {
// clears the movie cache upon destruction...
}
}
@ApplicationScoped
@TrackLifeCycle
public class Hello {
public Hello() {
Log.info(this.getClass().getSimpleName() + " at instance");
}
@PostConstruct
public void doPostConstruct() {
Log.info("at doPostConstruct");
}
@PreDestroy
public void doPreDestroy() {
Log.info("at PreDestroy");
}
public void helloWorld() {
Log.info("Hello world!");
}
}
package com.bolingcavalry.service.impl;
import io.quarkus.logging.Log;
/**
* @author [email protected]
* @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");
}
}
package com.bolingcavalry.config;
import com.bolingcavalry.service.impl.ResourceManager;
import javax.enterprise.context.RequestScoped;
public class SelectBeanConfiguration {
@RequestScoped
public ResourceManager getResourceManager() {
return new 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 {
@Inject
ResourceManager 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();
}
@QuarkusTest
public class DisposeTest {
@RepeatedTest(3)
public void test() {
given()
.when().get("/resourcemanager")
.then()
.statusCode(200)
// 检查body内容
.body(is("success"));
}
}
掌握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 {
@Override
public String name() {
return "Espresso";
}
@Override
public 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
@Inject
Coffee delegate;
@Override
public String name() {
return "Latte";
}
@Override
public 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
@Inject
Coffee delegate;
@Override
public String name() {
return "CaramelMacchiato";
}
@Override
public 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
@QuarkusTest
public class DecoratorTest {
@Inject
Coffee coffee;
@Test
public void testDecoratorPrice() {
Assertions.assertEquals(6, coffee.getPrice());
}
}
猜猜这里注入的谁,很神奇,先放这吧,不明实际应用场景
直接结论
在deposit和deduct都没有被调用时,get方法可以被调用,而且可以多线程同时调用,因为每个线程都能顺利拿到读锁
一旦deposit或者deduct被调用,其他线程在调用deposit、deduct、get方法时都被阻塞了,因为此刻不论读锁还是写锁都拿不到,必须等deposit执行完毕,它们才重新去抢锁
有了上述逻辑,再也不会出现deposit和deduct同时修改余额的情况了,预测单元测试应该能通过
这种读写锁的方法虽然可以确保逻辑正确,但是代价不小(一个线程执行,其他线程等待),所以在并发性能要求较高的场景下要慎用,可以考虑乐观锁、AtomicInteger这些方式来降低等待代价
@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 {
@Inject
NormalSingleton normalSingleton;
@Inject
NormalApplicationScoped normalApplicationScoped;
@Test
void 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事件
官方都这么说了,我岂敢不信,不过流程还是要完成的,把修改后的代码再运行一遍,截个图贴到文中,走走过场…
然而,这次运行的结果,却让人精神一振,StartupEvent和Startup效果是不一样的!!!
运行结果如下图,最先实例化的居然不是被Startup注解修饰的NormalApplicationScoped,而是它的代理类!
Startup注解的value属性值,是bean的优先级,这样,多个bean都使用Startup的时候,可以通过value值设置优先级,以此控制实例化顺序(实际上控制的是事件observer的创建顺序)
如果一个类只有Startup注解修饰,而没有设置作用域的时候,quarkus自动将其作用域设置为ApplicationScoped,也就是说,下面这段代码中,ApplicationScoped注解写不写都一样
@ApplicationScoped
@Startup
public class NormalApplicationScoped {
先定义三个bean
public interface SayHello {
void hello();
}
@ApplicationScoped
@Named("A")
public class SayHelloA implements SayHello {
@SendMessage
@Override
public void hello() {
Log.info("hello from A");
}
}
@ApplicationScoped
@Named("B")
public class SayHelloB implements SayHello {
@SendMessage(sendType = "email")
@Override
public void hello() {
Log.info("hello from B");
}
}
@ApplicationScoped
@Named("C")
public class SayHelloC implements SayHello {
@SendMessage
@SendMessage(sendType = "email")
@Override
public 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
*/
@Nonbinding
String sendType() default "sms";
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface SendMessageList {
SendMessage[] value();
}
}
quarkus对重复使用同一拦截器注解的限制
@SendMessage
@Interceptor
public class SendMessageInterceptor {
@AroundInvoke
Object 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;
@Test
public void testSendMessage() {
sayHelloA.hello();
sayHelloB.hello();
sayHelloC.hello();
}
}
@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackClass {
}
@TrackClass
@Interceptor
public class TrackClassInterceptor {
@AroundInvoke
Object 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 {
@AroundInvoke
Object execute(InvocationContext context) throws Exception {
Log.info("from TrackMethod");
return context.proceed();
}
}
@ApplicationScoped
@TrackClass
public class ExcludeInterceptorDemo {
public void test0() {
Log.info("from test0");
}
@TrackMethod
public void test1() {
Log.info("from test1");
}
}
测试
@QuarkusTest
public class ExcludeInterceptorTest {
@Inject
ExcludeInterceptorDemo excludeInterceptorDemo;
@Test
public void test() {
excludeInterceptorDemo.test0();
Log.info("*****************************");
excludeInterceptorDemo.test1();
}
}
假设遇到了某些冲突(例如和数据库、IO相关等),导致TrackClassInterceptor和TrackMethodInterceptor两个拦截器不能同时对test1方法进行拦截,只能保留TrackMethodInterceptor
此时,可以用注解NoClassInterceptors修饰test1方法,如下图红框所示,这样类拦截器TrackClassInterceptor就会失效,只剩下TrackMethodInterceptor可以正常工作
NoClassInterceptors的影响范围
而NoClassInterceptors的作用,就是针对有注解AroundInvoke修饰的方法,使他们失效
除了AroundInvoke,NoClassInterceptors还针对AroundConstruct修饰的方法,使他们失效
至此,拦截器的高级特性已经全部学习和实践完成,希望能给您提供一些参考,助您设计出更完善的拦截器
@ApplicationScoped
public class ConfigBean {
@ConfigProperty(name = "aaa.name")
String greetingMsg;
public String getGreetingMsg() {
return greetingMsg;
}
}
@ApplicationScoped
public class MyCoolService {
private SimpleProcessor processor;
MyCoolService() { // dummy constructor needed
}
@Inject // constructor injection
MyCoolService(SimpleProcessor processor) {
this.processor = processor;
}
}
@ApplicationScoped
public class MyCoolService {
private SimpleProcessor processor;
MyCoolService(SimpleProcessor processor) {
this.processor = processor;
}
}
class Producers {
@Produces
@ApplicationScoped
MyService produceServ
ice() {
return new MyService(coolProperty);
}
}
class Producers {
@ApplicationScoped
MyService produceService() {
return new MyService(coolProperty);
}
}
@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 {
@Inject
Instance<HelloDependent> instance;
@Test
public 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的源码,如下所示,构造方法中会打印日志,这下好办了,只要看日志出现几次,就知道实例化几次了
现在问题来了:如果bean的作用域必须是Dependent,又希望多次Instance#get返回的是同一个bean实例,这样的要求可以做到吗?
答案是可以,用WithCaching注解修饰Instance即可,改动如下图红框1,改好后再次运行,红框2显示HelloDependent只实例化了一次
仅支持方法级别的拦截(即拦截器修饰的是方法)
private型的静态方法不会被拦截
下图是拦截器实现的常见代码,通过入参InvocationContext的getTarget方法,可以得到被拦截的对象,然而,在拦截静态方法时,getTarget方法的返回值是null,这一点尤其要注意,例如下图红框中的代码,在拦截静态方法是就会抛出空指针异常
public interface SayHello {
void hello();
}
public class InjectAllTest {
/**
* 用Instance接收注入,得到所有SayHello类型的bean
*/
@Inject
Instance<SayHello> instance;
@Test
public void testInstance() {
// instance中有迭代器,可以用遍历的方式得到所有bean
for (SayHello sayHello : instance) {
sayHello.hello();
}
}
}
@QuarkusTest
public class InjectAllTest {
/**
* 用All注解可以将SayHello类型的bean全部注入到list中,
* 这样更加直观
*/
@All
List<SayHello> list;
@Test
public void testAll() {
for (SayHello sayHello : list) {
sayHello.hello();
}
}
}
@QuarkusTest
public class InjectAllTest {
@All
List<InstanceHandle<SayHello>> list;
@Test
public void testQuarkusAllAnnonation() {
for (InstanceHandle<SayHello> instanceHandle : list) {
// InstanceHandle#get可以得到注入bean
SayHello 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 {
@Inject
Event<TestEvent> event;
/**
* 发送异步事件
*/
public void asyncProduce() {
event.fireAsync(new TestEvent());
}
}
@ApplicationScoped
public class TestEventConsumer {
/**
* 消费异步事件,这里故意抛出异常
*/
public void aSyncConsume(@ObservesAsync TestEvent testEvent) throws Exception {
throw new Exception("exception from aSyncConsume");
}
}
@QuarkusTest
public class EventExceptionHandlerTest {
@Inject
TestEventProducer testEventProducer;
@Test
public void testAsync() throws InterruptedException {
testEventProducer.asyncProduce();
}
}
@ApplicationScoped
public class NoopAsyncObserverExceptionHandler implements AsyncObserverExceptionHandler {
@Override
public void handle(Throwable throwable, ObserverMethod<?> observerMethod, EventContext<?> eventContext) {
// 异常信息
Log.info("exception is - " + throwable);
// 事件信息
Log.info("observer type is - " + observerMethod.getObservedType().getTypeName());
}
}
官方提醒
在使用依赖注入的时候,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
关于CDI的bean
@ApplicationScoped
public class ClassAnnotationBean {
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
@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());
}
}
public interface HelloService {
String hello();
}
public class HelloServiceImpl implements HelloService {
@Override
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
public class MethodAnnonationBean {
@Produces // 可省略
@ApplicationScoped
public HelloService getHelloService() {
return new HelloServiceImpl();
}
}
这种用于创建bean的方法,被quarkus称为producer method
看过上述代码,相信聪明的您应该明白了用这种方式创建bean的优点:在创建HelloService接口的实例时,可以控制所有细节(构造方法的参数、或者从多个HelloService实现类中选择一个),没错,在SpringBoot的Configuration类中咱们也是这样做的
前面的getHelloService方法的返回值,可以直接在业务代码中依赖注入,如下所示
@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());
}
}
public class MethodAnnonationBean {
@Produces
@ApplicationScoped
public HelloService getHelloService(OtherService otherService) {
return new HelloServiceImpl();
}
}
public class OtherServiceImpl {
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
public class FieldAnnonationBean {
@Produces
@ApplicationScoped
OtherServiceImpl otherServiceImpl = new OtherServiceImpl();
}
还有一种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的作用域(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();
}
}
@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());
}
}
常规作用域
第一种:ClassAnnotationController被实例化的时候,classAnnotationBean会被注入,这时ClassAnnotationBean被实例化
第二种:get方法第一次被调用的时候,classAnnotationBean真正发挥作用,这时ClassAnnotationBean被实例化
所以,一共有两个时间点:注入时和get方法首次执行时,作用域不同,这两个时间点做的事情也不同,下面用表格来解释
时间点 | 常规作用域 | 为作用域 |
---|---|---|
注入的时候 | 注入的是一个代理类,此时ClassAnnotationBean并未实例化 | 触发ClassAnnotationBean的实例化 |
get方法首次执行的时候 | 1. 触发ClassAnnotationBean实例化 2.执行常规业务代码 | 执行常规代码 |
RequestScoped
SessionScoped
@RequestScoped
public class RequestScopeBean {
/**
* 在构造方法中打印日志,通过日志出现次数对应着实例化次数
*/
public RequestScopeBean() {
Log.info("Instance of " + this.getClass().getSimpleName());
}
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
@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());
}
}
@QuarkusTest
class RequestScopeControllerTest {
@RepeatedTest(10)
public void testGetEndpoint() {
given()
.when().get("/requestscope")
.then()
.statusCode(200)
// 检查body内容,是否含有ClassAnnotationBean.hello方法返回的字符串
.body(containsString("from " + RequestScopeBean.class.getSimpleName()));
}
}
另外,请重点关注蓝框和蓝色注释文字,这是意外收获,居然看到了代理类的日志,看样子代理类是继承了RequestScopeBean类,于是父类构造方法中的日志代码也执行了,还把代理类的类名打印出来了
从日志可以看出:10次http请求,bean的构造方法执行了10次,代理类的构造方法只执行了一次,这是个重要结论:bean类被多次实例化的时候,代理类不会多次实例化
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();
}
}
@ApplicationScoped
public class DependentClientA {
@Inject
HelloDependent hello;
public String doHello() {
return hello.hello();
}
}
@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());
}
}
LookupIfProperty,配置项的值符合要求才能使用bean
LookupUnlessProperty,配置项的值不符合要求才能使用bean
IfBuildProfile,如果是指定的profile才能使用bean
UnlessBuildProfile,如果不是指定的profile才能使用bean
IfBuildProperty,如果构建属性匹配才能使用bean
注解LookupIfProperty的作用是检查指定配置项,如果存在且符合要求,才能通过代码获取到此bean,
有个关键点请注意:下图是官方定义,可见LookupIfProperty并没有决定是否实例化beam,它决定的是能否通过代码取到bean,这个代码就是Instance来注入,并且用Instance.get方法来获取
public interface TryLookupIfProperty {
String hello();
}
以及两个实现类
public class TryLookupIfPropertyAlpha implements TryLookupIfProperty {
@Override
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
public class TryLookupIfPropertyBeta implements TryLookupIfProperty {
@Override
public 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")
@ApplicationScoped
public TryLookupIfProperty tryLookupIfPropertyAlpha() {
return new TryLookupIfPropertyAlpha();
}
@LookupIfProperty(name = "service.beta.enabled", stringValue = "true")
@ApplicationScoped
public TryLookupIfProperty tryLookupIfPropertyBeta() {
return new TryLookupIfPropertyBeta();
}
}
@QuarkusTest
public class BeanInstanceSwitchTest {
@BeforeAll
public static void setUp() {
System.setProperty("service.alpha.enabled", "true");
}
// 注意,前面的LookupIfProperty不能决定注入bean是否实力话,只能决定Instance.get是否能取到,
//所以此处要注入的是Instance,而不是TryLookupIfProperty本身
@Inject
Instance<TryLookupIfProperty> service;
@Test
public void testTryLookupIfProperty() {
Assertions.assertEquals("from " + tryLookupIfPropertyAlpha.class.getSimpleName(),
service.get().hello());
}
}
LookupIfProperty和LookupUnlessProperty都有名为lookupIfMissing的属性,意思都一样:指定配置项不存在的时候,就执行注解所修饰的方法,修改SelectBeanConfiguration.java,如下图黄框所示,增加lookupIfMissing属性,指定值为true(没有指定的时候,默认值是false)
应用在运行时,其profile是固定的,IfBuildProfile检查当前profile是否是指定值,如果是,其修饰的bean就能被业务代码使用
对比官方对LookupIfProperty和IfBuildProfile描述的差别,LookupIfProperty决定了是否能被选择,IfBuildProfile决定了是否在容器中
public interface TryIfBuildProfile {
String hello();
}
public class TryIfBuildProfileProd implements TryIfBuildProfile {
@Override
public String hello() {
return "from " + this.getClass().getSimpleName();
}
}
public class TryIfBuildProfileDefault implements TryIfBuildProfile {
@Override
public 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());
}
最后要提到注解是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
@DefaultBean
public 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 + "]";
}
}
java -Dgreeting.message="from system properties" -jar hello-quarkus-1.0-SNAPSHOT-runner.jar
在设置环境变量时,要注意转换规则:全大写、点号变下划线,因此greeting.message在环境变量中应该写成GREETING_MESSAGE
打开控制台,执行以下命令,即可在当前会话中设置环境变量:
export GREETING_MESSAGE="from Environment variables"
GREETING_MESSAGE=from .env file
为了避免之前的操作带来的影响,请删除刚才创建的.env文件
于hello-quarkus-1.0-SNAPSHOT-runner.jar文件所在目录,新建文件夹config
在config文件夹下新建文件application.properties,内容如下:
greeting.message=from config/application.properties
为了避免之前的操作带来的影响,请将src/main/resources/application.properties文件中的greeting.message配置项删除
MicroProfile是一个 Java 微服务开发的基础编程模型,它致力于定义企业 Java 微服务规范,其中的配置规范有如下描述:
图红框指出了MicroProfile规定的配置文件位置,咱们来试试在此位置放置配置文件是否能生效
如下图红框,在工程的src/main/resources/META-INF目录下新建文件microprofile-config.properties,内容如黄框所示
注意:microprofile-config.properties文件所在目录是src/main/resources/META-INF,不是src/main/resources/META-INF/resources
至此,六种配置方式及其实例验证都完成了,您可以按照自己的实际情况灵活选择
greeting.message=from config/application.properties
greeting.name=Will
greeting.message=hello, ${greeting.name:xxxxxx}
greeting.message=hello, ${quarkus.uuid}
my.collection=dog,cat,turtle
@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
整篇文章由以下内容构成:
greeting.message = hello from application.properties
@ConfigProperty(name = "greeting.message")
String message;
@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 + "]";
}
}
对于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;
@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,此时如果在配置文件中没有该配置项,启动应用会报错
@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;
}
}
@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.name=Tom
student.age=11
student.description=He is a good boy
@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();
}
/// 这个好强
@ConfigMapping(prefix = "student", namingStrategy = ConfigMapping.NamingStrategy.SNAKE_CASE)
public interface StudentConfiguration {
/**
* 名字与配置项一致
* @return
*/
String name();
...
student.name=Tom
student.age=11
student.description=He is a good boy
student.address.province=guangdong
student.address.city=shenzhen
public interface Address {
String province();
String city();
}
前面的接口嵌套,虽然将多层级的配置以对象的形式清晰的表达出来,但也引出一个问题:配置越多,接口定义或者接口方法就越多,代码随之增加
如果配置项的层级简单,还有种简单的方式将其映射到配置接口中:转为map
student.address.province=guangdong
student.address.city=shenzhen
对应的代码改动如下图,只要把address方法的返回值从Address改为Map
quarkus有很多内置的配置项,例如web服务的端口quarkus.http.port就是其中一个,如果您熟悉SpringBoot的话,对这些内置配置项应该很好理解,数据库、消息、缓存,都有对应配置项
篇幅所限就不在此讲解quarkus内置的配置项了,您可以参考这份官方提供的配置项列表,里面有详细说明:quarkus.io/guides/all-…
上述文档中,有很多配置项带有加锁的图标,如下图红框所示,有这个图标的配置项,其值在应用构建的时候已经固定了,在应用运行期间始终保持只读状态
这种带有加锁图标的配置项的值,在应用运行期间真的不能改变了吗?其实还是有办法的,官方文档指明,如果业务的情况特殊,一定要变,就走热部署的途径,您可以参考《quarkus实战之四:远程热部署》
官方对开发者的建议:在开发quarkus应用的时候,不要使用quarkus作为配置项的前缀,因为目前quarkus框架及其插件们的配置项的前缀都是quarkus,应用开发应该避免和框架使用相同的配置项前缀,以免冲突
# 这个配置信息在各个环境中都是相同的
quarkus.profile=dev
# 如果不指定profile,就使用此配置
quarkus.http.port=8080
java -Dquarkus.profile="dev" -jar hello-quarkus-1.0-SNAPSHOT-runner.jar
# 指定当前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
# 这个配置信息在各个环境中都是相同的
GREETING_MESSAGE=hello
# 如果profile为dev,就是用此配置
_DEV_QUARKUS_HTTP_PORT=8081
# 如果profile为production,就是用此配置
_PRODUCTION_QUARKUS_HTTP_PORT=8082
# 如果不指定profile,就使用此配置
QUARKUS_HTTP_PORT=8080
java -Dquarkus.profile=dev -jar hello-quarkus-1.0-SNAPSHOT-runner.jar
不指定profile的时候,quarkus会给profile设置默认值,有三种可能:dev、test、prod,具体逻辑如下:
如果启动命令是mvn quarkus:dev,profile等于dev,如下图,大家应该见过多次了:
resources
├── META-INF
│ └── resources
│ └── index.html
├── application-staging.properties
└── application.properties
shell
复制代码greeting.message=hello
quarkus.http.port=8080
shell
复制代码greeting.message=hello
quarkus.http.port=8081
# 指定profile的名字
quarkus.profile=dev
# 指定parent的名字
quarkus.config.profile.parent=common
%common.quarkus.http.port=9090
%dev.quarkus.http.ssl-port=9443
quarkus.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
mvn clean package -U -Dquarkus.package.type=uber-jar -Dquarkus.profile=prod-aws
io.quarkus.runtime.configuration.ProfileManager#getActiveProfile
@ConfigProperty("quarkus.profile")
String profile;