### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本文是《quarkus依賴註入》系列的第 ...
歡迎訪問我的GitHub
這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos
本篇概覽
- 本文是《quarkus依賴註入》系列的第五篇,經過前面的學習,咱們熟悉了依賴註入的基本特性,接下來進一步瞭解相關的高級特性,先從本篇的攔截器開始
- 如果您熟悉spring的話,對攔截器應該不會陌生,通過攔截器可以將各種附加功能與被攔截代碼的主體解耦合,例如異常處理、日誌、數據同步等多種場景
- 本篇會演示如何自定義攔截器,以及如何對bean的方法進行進行攔截,由以下章節構成
- 定義和使用攔截器的操作步驟介紹
- 攔截異常
- 攔截構造方法
- 獲取被攔截方法的參數
- 多個攔截器之間傳遞參數
定義和使用攔截器的操作步驟介紹
- 定義和使用攔截器一共要做三件事:
- 定義:新增一個註解(假設名為A),要用@InterceptorBinding修飾該註解
- 實現:攔截器A到底要做什麼事情,需要在一個類中實現,該類要用兩個註解來修飾:A和Interceptor
- 使用:用A來修飾要攔截器的bean
- 整個流程如下圖所示

- 接下來通過實戰掌握攔截器的開發和使用,從最常見的攔截異常開始
攔截異常
-
寫一個攔截器,在程式發生異常的時候可以捕獲到並將異常列印出來
-
首先是定義一個攔截器,這裡的攔截器名為HandleError,註意要用InterceptorBinding修飾
package com.bolingcavalry.interceptor.define;
import javax.interceptor.InterceptorBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
@InterceptorBinding
@Target({TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HandleError {
}
- 其次是實現攔截器的具體功能,下麵代碼有幾處要註意的地方稍後會提到
package com.bolingcavalry.interceptor.impl;
import com.bolingcavalry.interceptor.define.HandleError;
import io.quarkus.arc.Priority;
import io.quarkus.logging.Log;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
@HandleError
@Interceptor
@Priority(Interceptor.Priority.APPLICATION +1)
public class HandleErrorInterceptor {
@AroundInvoke
Object execute(InvocationContext context) {
try {
// 註意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;
}
}
- 上述代碼有以下四點需要註意:
- Priority註解的作用是設定HandleError攔截器的優先順序(值越小優先順序越高),可以同時用多個攔截器攔截同一個方法
- AroundInvoke註解的作用,是表明execute會在攔截bean方法時被調用
- proceed方法的作用,並非是執行被攔截的方法,而是執行下一個攔截器,直到最後一個攔截器才會執行被攔截的方法
- 可以從入參context處取得被攔截實例和方法的信息
- 然後是使用攔截器,這裡創建個bean來演示攔截器如何使用,bean裡面有個業務方法會拋出異常,可見攔截器使用起來很簡單:用HandleError修飾bean即可
@ApplicationScoped
@HandleError
public class HandleErrorDemo {
public void executeThrowError() {
throw new IllegalArgumentException("this is business logic exception");
}
}
- 驗證攔截器的單元測試代碼如下,只要執行HandleErrorDemo的executeThrowError方法就會拋出異常,然後觀察日誌中是否有攔截器日誌信息即可驗證攔截器是否符合預期
@QuarkusTest
public class InterceptorTest {
@Inject
HandleErrorDemo handleErrorDemo;
@Test
public void testHandleError() {
handleErrorDemo.executeThrowError();
}
}
- 執行單元測試,如下圖紅框所示,攔截器捕獲了異常並列印出異常信息

- 至此,攔截異常的操作就完成了,除了用AroundInvoke攔截普通的bean方法,還能用AroundConstruct攔截bean的構造方法,接下里編碼體驗
攔截構造方法
- 攔截器定義
@InterceptorBinding
@Target({TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HandleConstruction {
}
- 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());
}
}
- 上述代碼有兩處要註意的
- 被AroundConstruct註解修飾後,execute方法會在bean的構造方法執行時被調用
- context.getTarget()的返回值,只有在context.proceed執行後才不為空
- 攔截器的使用,用HandleConstruction修飾要攔截的bean,為了調試和分析,還在構造方法中列印了日誌
@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();
}
}
- 運行單元測試,控制台輸出如下,可見構造方法攔截成功
2022-03-27 15:51:03,158 INFO [io.quarkus] (main) Quarkus 2.7.3.Final on JVM started in 0.867s. Listening on: http://localhost:8081
2022-03-27 15:51:03,158 INFO [io.quarkus] (main) Profile test activated.
2022-03-27 15:51:03,158 INFO [io.quarkus] (main) Installed features: [agroal, cdi, narayana-jta, resteasy, smallrye-context-propagation, vertx]
2022-03-27 15:51:03,164 INFO [com.bol.int.dem.HandleonstructionDemo] (main) construction of HandleonstructionDemo
2022-03-27 15:51:03,397 INFO [com.bol.int.imp.HandleConstructionInterceptor] (main) start construction interceptor
2022-03-27 15:51:03,398 INFO [com.bol.int.dem.HandleonstructionDemo] (main) construction of HandleonstructionDemo
2022-03-27 15:51:03,398 INFO [com.bol.int.imp.HandleConstructionInterceptor] (main) bean instance of HandleonstructionDemo
2022-03-27 15:51:03,398 INFO [com.bol.int.dem.HandleonstructionDemo] (main) hello world!
2022-03-27 15:51:03,416 INFO [io.quarkus] (main) Quarkus stopped in 0.015s
獲取被攔截方法的參數
- 攔截方法時,可能需要知道方法入參的值,才好實現具體的攔截功能(如參數校驗),接下來就試試如何取得被攔截方法的參數並列印到日誌中
- 首先是攔截器定義
@InterceptorBinding
@Target({TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackParams {
}
- 攔截器實現,可以用context.getParameters方法取得被攔截方法的入參數組,然後遍歷並列印
@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();
}
}
- 使用攔截器的bean,其hello方法有兩個入參,正常情況下會在攔截器中列印出來
@ApplicationScoped
@TrackParams
public class TrackParamsDemo {
public void hello(String name, int id) {
Log.infov("Hello {0}, your id is {1}", name, id);
}
}
- 測試類,調用了TrackParamsDemo的hello方法
@QuarkusTest
public class InterceptorTest {
@Inject
TrackParamsDemo trackParamsDemo;
@Test
public void testTrackParams() {
trackParamsDemo.hello("Tom", 101);
}
}
- 執行單元測試,控制台輸出如下,可見hello方法的兩個入參的類型和值都被攔截器列印出來了
2022-03-27 17:15:46,582 INFO [io.quarkus] (main) Quarkus 2.7.3.Final on JVM started in 0.905s. Listening on: http://localhost:8081
2022-03-27 17:15:46,582 INFO [io.quarkus] (main) Profile test activated.
2022-03-27 17:15:46,582 INFO [io.quarkus] (main) Installed features: [agroal, cdi, narayana-jta, resteasy, smallrye-context-propagation, vertx]
2022-03-27 17:15:46,587 INFO [com.bol.int.dem.HandleonstructionDemo] (main) construction of HandleonstructionDemo
2022-03-27 17:15:46,827 INFO [com.bol.int.imp.TrackParamsInterceptor] (main) parameter type [String], value [Tom]
2022-03-27 17:15:46,827 INFO [com.bol.int.imp.TrackParamsInterceptor] (main) parameter type [Integer], value [101]
2022-03-27 17:15:46,827 INFO [com.bol.int.dem.TrackParamsDemo] (main) Hello Tom, your id is 101
2022-03-27 17:15:46,845 INFO [io.quarkus] (main) Quarkus stopped in 0.015s
- 以上就是獲取被攔截方法入參的操作了,如果被攔截的構造方法也有入參,也能用此方式全部獲取到
多個攔截器之間傳遞參數
- 多個攔截器攔截同一個方法是很正常的,他們各司其職,根據優先順序按順序執行,如果這些攔截器之間有一定邏輯關係,例如第二個攔截器需要第一個攔截器的執行結果,此時又該如何呢?
- quarkus支持不同攔截器間共用同一個上下文的數據(這讓我想到了數據匯流排),接下來就演示多個攔截器之間是如何共用數據的
- 首先,定義攔截器,這裡增加了一個常量KEY_PROCEED_INTERCEPTORS,後面在攔截器的實現中會用到
@InterceptorBinding
@Target({TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ContextData {
String KEY_PROCEED_INTERCEPTORS = "proceedInterceptors";
}
- 其次,首先攔截器,因為要演示多個攔截器共用數據,這裡會有兩個攔截器,為了簡化開發,先寫個父類,把兩個攔截器的公共代碼寫入父類,可見攔截器之間共用數據的關鍵是context.getContextData()方法的返回值,這是個map,某個攔截器向此map中放入的數據,可以在後面的攔截器中取得,這裡為了演示,將當前實例的類名存入了map中
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();
}
}
- 再新建一個攔截器實現類ContextDataInterceptorA,是BaseContextDataInterceptor的子類:
@ContextData
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 1)
public class ContextDataInterceptorA extends BaseContextDataInterceptor {
@AroundInvoke
Object execute(InvocationContext context) throws Exception {
return super.execute(context);
}
}
- 另一個攔截器實現類ContextDataInterceptorB,註意它的Priority註解的值,表明其優先順序低於ContextDataInterceptorA
@ContextData
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 2)
public class ContextDataInterceptorB extends BaseContextDataInterceptor {
@AroundInvoke
Object execute(InvocationContext context) throws Exception {
return super.execute(context);
}
}
- 然後是被攔截bean
@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();
}
}
- 執行單元測試,控制台輸入如下,可見執行順序分別是ContextDataInterceptorA、ContextDataInterceptorB、被攔截方法,另外,存放在共用數據中的內容也隨著攔截器的執行,越來越多,符合預期
2022-03-27 23:29:27,703 INFO [io.quarkus] (main) Quarkus 2.7.3.Final on JVM started in 0.903s. Listening on: http://localhost:8081
2022-03-27 23:29:27,703 INFO [io.quarkus] (main) Profile test activated.
2022-03-27 23:29:27,703 INFO [io.quarkus] (main) Installed features: [agroal, cdi, narayana-jta, resteasy, smallrye-context-propagation, vertx]
2022-03-27 23:29:27,708 INFO [com.bol.int.dem.HandleonstructionDemo] (main) construction of HandleonstructionDemo
2022-03-27 23:29:27,952 INFO [com.bol.int.imp.BaseContextDataInterceptor] (main) from ContextDataInterceptorA, this is first processor
2022-03-27 23:29:27,953 INFO [com.bol.int.imp.BaseContextDataInterceptor] (main) From ContextDataInterceptorA, all processors ContextDataInterceptorA
2022-03-27 23:29:27,953 INFO [com.bol.int.imp.BaseContextDataInterceptor] (main) From ContextDataInterceptorB, all processors ContextDataInterceptorB
2022-03-27 23:29:27,953 INFO [com.bol.int.dem.ContextDataDemo] (main) Hello world!
2022-03-27 23:29:27,971 INFO [io.quarkus] (main) Quarkus stopped in 0.015s
- 至此,有關攔截器的實戰已經完成,往後不管是自建攔截器還是使用已有攔截器,相信您都能從容應對,信手拈來,有了攔截器,我們在增強應用能力的同時還能保持低耦合性,用好它,打造更完善的應用。
源碼下載
- 本篇實戰的完整源碼可在GitHub下載到,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos)
名稱 | 鏈接 | 備註 |
---|---|---|
項目主頁 | https://github.com/zq2599/blog_demos | 該項目在GitHub上的主頁 |
git倉庫地址(https) | https://github.com/zq2599/blog_demos.git | 該項目源碼的倉庫地址,https協議 |
git倉庫地址(ssh) | [email protected]:zq2599/blog_demos.git | 該項目源碼的倉庫地址,ssh協議 |
- 這個git項目中有多個文件夾,本次實戰的源碼在quarkus-tutorials文件夾下,如下圖紅框
- quarkus-tutorials是個父工程,裡面有多個module,本篇實戰的module是basic-di,如下圖紅框