通過編碼實戰瞭解quarkus攔截器的另一個高級特性:禁用類級別攔截器,這樣可以避免類級別和方法級別攔截器的疊加衝突 ...
歡迎訪問我的GitHub
這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos
本篇概覽
- 本篇是《quarkus依賴註入》系列的終篇,前面十二篇已覆蓋quarkus依賴註入的大部分核心內容,但依然漏掉了一些知識點,今天就將剩下的內容彙總,來個一鍋端,輕鬆愉快的結束這個系列
- 總的來說,本篇由以下內容構成,每個段落都是個獨立的知識點
- 幾處可以簡化編碼的地方,如bean註入、構造方法等
- WithCaching:特定場景下,減少bean實例化次數
- 靜態方法是否可以被攔截器攔截?
- All註解,讓多個bean的註入更加直觀
- 統一處理非同步事件的異常
- 咱們從最簡單的看起:表達方式的簡化,一共有三個位置可以簡化:bean的註入、bean構造方法、bean生產方法
簡化之一:bean註入
-
quarkus在CDI規範的基礎上做了簡化,可以讓我們少寫幾行代碼
-
將配置文件中名為greeting.message的配置項註入到bean的成員變數greetingMsg中,按照CDI規範的寫法如下
@Inject
@ConfigProperty(name = "greeting.message")
String greetingMsg;
- 在quarkus框架下可以略去@Inject,寫成下麵這樣的效果和上面的代碼一模一樣
@ConfigProperty(name = "greeting.message")
String greetingMsg;
簡化之二:bean構造方法
- 關於bean的構造方法,CDI有兩個規定:首先,必須要有無參構造方法,其次,有參數的構造方法需要@Inject註解修飾,實例代碼如下所示
@ApplicationScoped
public class MyCoolService {
private SimpleProcessor processor;
MyCoolService() { // dummy constructor needed
}
@Inject // constructor injection
MyCoolService(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
@ApplicationScoped
MyService produceServ
ice() {
return new MyService(coolProperty);
}
}
- 在quarkus框架下可以略去@Produces,寫成下麵這樣的效果和上面的代碼一模一樣
class Producers {
@ApplicationScoped
MyService produceService() {
return new MyService(coolProperty);
}
}
- 好了,熱身結束,接下來看幾個略有深度的技能
WithCaching註解:避免不必要的多次實例化
- 在介紹WithCaching註解之前,先來看一個普通場景
- 下麵是一段單元測試代碼,HelloDependent類型的bean通過Instance的方式被註入,再用Instance#get來獲取此bean
@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();
}
}
-
上述代碼是種常見的bean註入和使用方式,我們的本意是在WithCachingTest實例中多次使用HelloDependent類型的bean,可能是在test方法中使用,也可能在WithCachingTest的其他方法中使用
-
如果HelloDependent的作用域是ApplicationScoped,上述代碼一切正常,但是,如果作用域是Dependent呢?代碼中執行了兩次Instance#get,得到的HelloDependent實例是同一個嗎?Dependent的特性是每次註入都實例化一次,這裡的Instance#get又算幾次註入呢?
-
最簡單的方法就是運行上述代碼看實際效果,這裡先回顧HelloDependent.java的源碼,如下所示,構造方法中會列印日誌,這下好辦了,只要看日誌出現幾次,就知道實例化幾次了
@Dependent
public class HelloDependent {
public HelloDependent(InjectionPoint injectionPoint) {
Log.info("injecting from bean "+ injectionPoint.getMember().getDeclaringClass());
}
public String hello() {
return this.getClass().getSimpleName();
}
}
- 運行單元測試類WithCachingTest,如下圖紅框所示,構造方法中的日誌列印了兩次,所以:每次Instance#get都相當於一次註入,如果bean的作用域是Dependent,就會創建一個新的實例並返回
- 現在問題來了:如果bean的作用域必須是Dependent,又希望多次Instance#get返回的是同一個bean實例,這樣的要求可以做到嗎?
- 答案是可以,用WithCaching註解修飾Instance即可,改動如下圖紅框1,改好後再次運行,紅框2顯示HelloDependent只實例化了一次
攔截靜態方法
- 先回顧一下攔截器的基本知識,定義一個攔截器並用來攔截bean中的方法,總共需要完成以下三步
- 實現攔截器的具體功能時,還要用註解指明攔截器類型,一共有四種類型
- AroundInvoke:攔截bean方法
- PostConstruct:生命周期攔截器,bean創建後執行
- PreDestroy:生命周期攔截器,bean銷毀前執行
- AroundConstruct:生命周期攔截器,攔截bean構造方法
- 現在問題來了:攔截器能攔截靜態方法嗎?
- 答案是可以,但是有限制,具體的限制如下
- 僅支持方法級別的攔截(即攔截器修飾的是方法)
- private型的靜態方法不會被攔截
- 下圖是攔截器實現的常見代碼,通過入參InvocationContext的getTarget方法,可以得到被攔截的對象,然而,在攔截靜態方法時,getTarget方法的返回值是null,這一點尤其要註意,例如下圖紅框中的代碼,在攔截靜態方法是就會拋出空指針異常
All更加直觀的註入
- 假設有個名為SayHello的介面,源碼如下
public interface SayHello {
void hello();
}
-
現在有三個bean都實現了SayHello介面,如果想要調用這三個bean的hello方法,應該怎麼做呢?
-
按照CDI的規範,應該用Instance註入,然後使用Instance中的迭代器即可獲取所有bean,代碼如下
public class InjectAllTest {
/**
* 用Instance接收註入,得到所有SayHello類型的bean
*/
@Inject
Instance<SayHello> instance;
@Test
public void testInstance() {
// instance中有迭代器,可以用遍歷的方式得到所有bean
for (SayHello sayHello : instance) {
sayHello.hello();
}
}
}
- quarkus提供了另一種方式,藉助註解io.quarkus.arc.All,可以將所有SayHello類型的bean註入到List中,如下所示
@QuarkusTest
public class InjectAllTest {
/**
* 用All註解可以將SayHello類型的bean全部註入到list中,
* 這樣更加直觀
*/
@All
List<SayHello> list;
@Test
public void testAll() {
for (SayHello sayHello : list) {
sayHello.hello();
}
}
}
- 和CDI規範相比,使用All註解可以讓代碼顯得更為直觀,另外還有以下三個特點
-
此list是immutable的(內容不可變)
-
list中的bean是按照priority排序的
-
如果您需要的不僅僅是註入bean,還需要bean的元數據信息(例如bean的scope),可以將List中的類型從SayHello改為InstanceHandle<SayHello>,這樣即可以得到註入bean,也能得到註入bean的元數據(在InjectableBean中),參考代碼如下
@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() );
}
}
}
- 代碼的執行結果如下圖紅框所示,可見註入bean及其作用域都能成功取得(要註意的是註入bean是代理bean)
統一處理非同步事件的異常
-
需要提前說一下,本段落涉及的知識點和AsyncObserverExceptionHandler類有關,而《quarkus依賴註入》系列所用的quarkus-2.7.3.Final版本中並沒有AsyncObserverExceptionHandler類,後來將quarkus版本更新為2.8.2.Final,就可以正常使用AsyncObserverExceptionHandler類了
-
本段落的知識點和非同步事件有關:如果消費非同步事件的過程中發生異常,而開發者有沒有專門寫代碼處理非同步消費結果,那麼此異常就默默無聞的被忽略了,我們也可能因此錯失了及時發現和處理問題的時機
-
來寫一段代碼復現上述問題,首先是事件定義TestEvent.java,就是個普通類,啥都沒有
public class TestEvent {
}
- 然後是事件的生產者TestEventProducer.java,註意其調用fireAsync方法發送了一個非同步事件
@ApplicationScoped
public class TestEventProducer {
@Inject
Event<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 {
@Inject
TestEventProducer testEventProducer;
@Test
public void testAsync() throws InterruptedException {
testEventProducer.asyncProduce();
}
}
- 運行EventExceptionHandlerTest,結果如下圖,DefaultAsyncObserverExceptionHandler處理了這個異常,這是quarkus框架的預設處理邏輯
- DefaultAsyncObserverExceptionHandler只是輸出了日誌,這樣的處理對於真實業務是不夠的(可能需要記錄到特定地方,調用其他告警服務等),所以,我們需要自定義預設的非同步事件異常處理器
- 自定義的全局非同步事件異常處理器如下
package com.bolingcavalry.service.impl;
import io.quarkus.arc.AsyncObserverExceptionHandler;
import io.quarkus.logging.Log;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.spi.EventContext;
import javax.enterprise.inject.spi.ObserverMethod;
@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());
}
}
- 此刻,咱們再執行一次單元測試,如下圖所示,異常已經被NoopAsyncObserverExceptionHandler#handler處理,異常和事件相關的信息都能拿到,您可以按照實際的業務需求來進行定製了
- 另外還要說明一下,自定義的全局非同步事件異常處理器,其作用域只能是ApplicationScoped或者Singleton
- 至此,《quarkus依賴註入》系列全部完成,與bean相關的故事也就此結束了,十三篇文章凝聚了欣宸對quarkus框架bean容器的思考和實踐,希望能幫助您更快的掌握和理解quarkus最核心的領域
- 雖然《quarkus依賴註入》已經終結,但是《quarkus實戰》系列依然還在持續更新中,有了依賴註入的知識作為基礎,接下來的quarkus之旅會更加輕鬆和高效