quarkus依賴註入之五:攔截器(Interceptor)

来源:https://www.cnblogs.com/bolingcavalry/archive/2023/08/03/17589409.html
-Advertisement-
Play Games

### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本文是《quarkus依賴註入》系列的第 ...


歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 本文是《quarkus依賴註入》系列的第五篇,經過前面的學習,咱們熟悉了依賴註入的基本特性,接下來進一步瞭解相關的高級特性,先從本篇的攔截器開始
  • 如果您熟悉spring的話,對攔截器應該不會陌生,通過攔截器可以將各種附加功能與被攔截代碼的主體解耦合,例如異常處理、日誌、數據同步等多種場景
  • 本篇會演示如何自定義攔截器,以及如何對bean的方法進行進行攔截,由以下章節構成
  1. 定義和使用攔截器的操作步驟介紹
  2. 攔截異常
  3. 攔截構造方法
  4. 獲取被攔截方法的參數
  5. 多個攔截器之間傳遞參數

定義和使用攔截器的操作步驟介紹

  • 定義和使用攔截器一共要做三件事:
  1. 定義:新增一個註解(假設名為A),要用@InterceptorBinding修飾該註解
  2. 實現:攔截器A到底要做什麼事情,需要在一個類中實現,該類要用兩個註解來修飾:A和Interceptor
  3. 使用:用A來修飾要攔截器的bean
  • 整個流程如下圖所示
流程圖 (19)
  • 接下來通過實戰掌握攔截器的開發和使用,從最常見的攔截異常開始

攔截異常

  • 寫一個攔截器,在程式發生異常的時候可以捕獲到並將異常列印出來

  • 首先是定義一個攔截器,這裡的攔截器名為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;
    }
}
  • 上述代碼有以下四點需要註意:
  1. Priority註解的作用是設定HandleError攔截器的優先順序(值越小優先順序越高),可以同時用多個攔截器攔截同一個方法
  2. AroundInvoke註解的作用,是表明execute會在攔截bean方法時被調用
  3. proceed方法的作用,並非是執行被攔截的方法,而是執行下一個攔截器,直到最後一個攔截器才會執行被攔截的方法
  4. 可以從入參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();
    }
}
  • 執行單元測試,如下圖紅框所示,攔截器捕獲了異常並列印出異常信息
image-20220327145313820
  • 至此,攔截異常的操作就完成了,除了用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());
    }
}
  • 上述代碼有兩處要註意的
  1. AroundConstruct註解修飾後,execute方法會在bean的構造方法執行時被調用
  2. 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
  • 至此,有關攔截器的實戰已經完成,往後不管是自建攔截器還是使用已有攔截器,相信您都能從容應對,信手拈來,有了攔截器,我們在增強應用能力的同時還能保持低耦合性,用好它,打造更完善的應用。

源碼下載

名稱 鏈接 備註
項目主頁 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文件夾下,如下圖紅框
    image-20220312091203116
  • quarkus-tutorials是個父工程,裡面有多個module,本篇實戰的module是basic-di,如下圖紅框
    image-20220312091404031

歡迎關註博客園:程式員欣宸

學習路上,你不孤單,欣宸原創一路相伴...


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 通常我們在做項目的時候,要手動搭建項目的結構,如controller,service,mapper,entity,是不是很麻煩,特別是資料庫表特別多時,現在介紹一下使用MybatisPlus時怎麼自動生成這些代碼。 1. 首先要在項目的pom.xml里引入必要的依賴,如下: ~~~xml com.b ...
  • ## Spring MVC Spring MVC是Spring框架的一部分,是一個Web應用程式框架。它旨在使用Model-View-Controller(MVC)設計模式輕鬆構建Web應用程式。 在Spring MVC中,應用程式被分為三個主要組件:Model、View和Controller。Mo ...
  • ## jsp ​ servlet 是無法將後端獲取的數據傳遞給html 頁面的,無法再servlet 中通過轉發或者是重定向的方式,給html 頁面傳遞響應的後端數據,servlet 中由於拼接過於繁瑣,是不適合寫html 的因此引入了 jsp ,既可以編寫 html標簽,也可以寫 Java 代碼, ...
  • # Go 語言入門指南: 環境搭建、基礎語法和常用特性解析 | 青訓營 ## 從零開始 ### Go 語言簡介 ![img](https://img2023.cnblogs.com/blog/2724888/202308/2724888-20230803143447307-285055892.png ...
  • 本文主要從源碼的角度解析了 ThreadLocal,並分析了發生記憶體泄漏的原因及正確用法,最後對它的應用場景進行了簡單介紹。 ...
  • 拆分列是`pandas`中常用的一種數據操作,它可以將一個包含多個值的列按照指定的規則拆分成多個新列,方便進行後續的分析和處理。拆分列的使用場景比較廣泛,以下是一些常見的應用場景: 1. 處理日期數據:在日期數據中,經常會將年、月、日等信息合併成一列,通過拆分列可以將其拆分成多個新列,方便進行時間序 ...
  • ## 教程簡介 JUnit是一個Java語言的單元測試框架。它由Kent Beck和Erich Gamma建立,逐漸成為源於Kent Beck的sUnit的xUnit家族中最為成功的一個。 JUnit有它自己的JUnit擴展生態圈。多數Java的開發環境都已經集成了JUnit作為單元測試的工具。JU ...
  • > 服務發現與負載均衡。 # 一、背景 在微服務架構中,這裡以開發環境「Dev」為基礎來描述,在K8S集群中通常會開放:路由網關、註冊中心、配置中心等相關服務,可以被集群外部訪問; ![](https://img2023.cnblogs.com/blog/1691717/202308/1691717 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...