Dubbo架構設計與源碼解析(三)責任鏈模式

来源:https://www.cnblogs.com/Jcloud/archive/2022/12/23/16999370.html
-Advertisement-
Play Games

作者:周可強 一、責任鏈模式簡介 1、責任鏈模式定義 責任鏈(Chain of Responsibility)模式的定義:為了避免請求發送者與多個請求處理者耦合在一起,於是將所有請求的處理者通過前一對象記住其下一個對象的引用而連成一條鏈;當有請求發生時,可將請求沿著這條鏈傳遞,直到有對象處理它為止。 ...


作者:周可強

一、責任鏈模式簡介

1、責任鏈模式定義

責任鏈(Chain of Responsibility)模式的定義:為了避免請求發送者與多個請求處理者耦合在一起,於是將所有請求的處理者通過前一對象記住其下一個對象的引用而連成一條鏈;當有請求發生時,可將請求沿著這條鏈傳遞,直到有對象處理它為止。在責任鏈模式中,客戶只需要將請求發送到責任鏈上即可,無須關心請求的處理細節和請求的傳遞過程,請求會自動進行傳遞。所以責任鏈將請求的發送者和請求的處理者解耦了。

2、責任鏈特點

責任鏈模式是一種對象行為型模式,

其主要優點如下。

1).降低了對象之間的耦合度。該模式使得一個對象無須知道到底是哪一個對象處理其請求以及鏈的結構,發送者和接收者也無須擁有對方的明確信息。

2).增強了系統的可擴展性。可以根據需要增加新的請求處理類,滿足開閉原則。

3).增強了給對象指派職責的靈活性。當工作流程發生變化,可以動態地改變鏈內的成員或者調動它們的次序,也可動態地新增或者刪除責任。責任鏈簡化了對象之間的連接。每個對象只需保持一個指向其後繼者的引用,不需保持其他所有處理者的引用,這避免了使用眾多的 if 或者 if···else 語句。

4).責任分擔。每個類只需要處理自己該處理的工作,不該處理的傳遞給下一個對象完成,明確各類的責任範圍,符合類的單一職責原則。

其主要缺點如下。

1).不能保證每個請求一定被處理。由於一個請求沒有明確的接收者,所以不能保證它一定會被處理,該請求可能一直傳到鏈的末端都得不到處理。

2).對比較長的職責鏈,請求的處理可能涉及多個處理對象,系統性能將受到一定影響。

3).職責鏈建立的合理性要靠客戶端來保證,增加了客戶端的複雜性,可能會由於職責鏈的錯誤設置而導致系統出錯,如可能會造成迴圈調用。

3、責任鏈結構圖

二、Dubbo中的責任鏈模式

1、過濾器日誌

通過列印過濾器的日誌,我們可以看到在發佈服務的過程中,會依次經過dubbo的每個過濾器類,以此來保證服務的完善。

2、過濾器簡圖

dubbo通過將每個過濾器類filter封裝成dubbo的核心模型invoker進行組裝,最終形成晚上的過濾器責任鏈filterChain。

3、過濾器類圖

Protocol是核心模型invoker暴露和引用的主功能入口,採用SPI的介面,他的兩個方法export和refer分別對應provider和consumer端的服務功能,ProtocolFilterWapper則是Dubbo的過濾器的主要實現類,通過重寫的export和refer指向buildInvokerChain方法,在buildInvokerChain中進行責任鏈的獲取與組裝,在extensionLoader中通過SPI獲取Filter的各實現類,並通過ActivateComparator進行排序,最終形成完整的責任鏈。

三、Dubbo中各Filter責任介紹

1、provider用到的filter

2、consumer用到的filter

四、源碼探析

進入到核心類ProtocolFilterWrapper中,在實現類中export和refer,都採用相同的構造責任鏈方法buildInvokerChain,只是通過參數group進行區分

在buildInvokerChain中,通過getActivateExtension獲取過濾器數組,併在之後封裝成核心模型invoker並組裝成責任鏈

private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
        Invoker<T> last = invoker;
        // 獲得過濾器數組 (已經排好序的)
        List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
        // 創建帶 Filter 鏈的 Invoker 對象
        if (!filters.isEmpty()) {
            for (int i = filters.size() - 1; i >= 0; i--) {
                final Filter filter = filters.get(i);
                final Invoker<T> next = last;
                last = new Invoker<T>() {

                    @Override
                    public Class<T> getInterface() {
                        return invoker.getInterface();
                    }

                    @Override
                    public URL getUrl() {
                        return invoker.getUrl();
                    }

                    @Override
                    public boolean isAvailable() {
                        return invoker.isAvailable();
                    }

                    @Override
                    public Result invoke(Invocation invocation) throws RpcException {
                        return filter.invoke(next, invocation);
                    }

                    @Override
                    public void destroy() {
                        invoker.destroy();
                    }

                    @Override
                    public String toString() {
                        return invoker.toString();
                    }
                };
            }
        }
        System.out.println("group:" + group);
        for (Filter filter : filters) {
            System.out.println(filter.getClass());
        }
        return last;
    }

getActivateExtension是主要的組裝邏輯,他包含獲取與排序等邏輯

首先進行判斷是否採用系統預設的Filter過濾器,並對每一個系統過濾器進行校驗是否移除,然後對系統過濾器排序,再通過指定的參數,增加用戶自定義的過濾器組裝責任鏈


public List<T> getActivateExtension(URL url, String key, String group) {
        // 從 Dubbo URL 獲得參數值
        String value = url.getParameter(key);
        // 獲得符合自動激活條件的拓展對象數組
        return getActivateExtension(url, value == null || value.length() == 0 ? null : Constants.COMMA_SPLIT_PATTERN.split(value), group);
    }
public List<T> getActivateExtension(URL url, String[] values, String group) {
        List<T> exts = new ArrayList<T>();
        //所有用戶自己配置的filter信息(有些Filter是預設激活的,有些是配置激活的,這裡的names就指的配置激活的filter信息)
        List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
        // 處理自動激活的拓展對象們
        // 判斷不存在配置 `"-name"` 。例如,<dubbo:service filter="-default" /> ,代表移除所有預設過濾器。
        if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
            // 獲得拓展實現類數組
            getExtensionClasses();
            // 迴圈
            for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
                //name指的是SPI讀取的配置文件的key
                String name = entry.getKey();
                Activate activate = entry.getValue();
                if (isMatchGroup(group, activate.group())) { // 匹配分組
                    // 獲得拓展對象
                    T ext = getExtension(name);
                    if (!names.contains(name) // 不包含在自定義配置里。如果包含,會在下麵的代碼處理。
                            && !names.contains(Constants.REMOVE_VALUE_PREFIX + name) // 判斷是否配置移除。例如 <dubbo:service filter="-monitor" />,則 MonitorFilter 會被移除
                            && isActive(activate, url)) { // 判斷是否激活
                        exts.add(ext);
                    }
                }
            }
            // 排序
            Collections.sort(exts, ActivateComparator.COMPARATOR);
        }
        // 處理自定義配置的拓展對象們。例如在 <dubbo:service filter="demo" /> ,代表需要加入 DemoFilter
        List<T> usrs = new ArrayList<T>();
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);
            if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX) && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) { // 判斷非移除的
                // 將配置的自定義在自動激活的拓展對象們前面。例如,<dubbo:service filter="demo,default,demo2" /> ,則 DemoFilter 就會放在預設的過濾器前面。
                if (Constants.DEFAULT_KEY.equals(name)) {
                    if (!usrs.isEmpty()) {
                        exts.addAll(0, usrs);
                        usrs.clear();
                    }
                } else {
                    // 獲得拓展對象
                    T ext = getExtension(name);
                    usrs.add(ext);
                }
            }
        }
        // 添加到結果集
        if (!usrs.isEmpty()) {
            exts.addAll(usrs);
        }
        return exts;
    }

系統預設的過濾器和udf過濾器進行區分

以ContextFilter為例,系統預設過濾器包含Activate註解,用於指定所屬分組與排序權重,用戶自己實現的過濾器則不能添加Activate註解通過發佈時指定所需的過濾器

我們看下具體的排序比較方法,首先判斷Activate註解是否指定before和after參數用來指定排序,若不存在則採用order權重進行排序

ActivateComparator.class
public int compare(Object o1, Object o2) {
        // 基本排序
        if (o1 == null && o2 == null) {
            return 0;
        }
        if (o1 == null) {
            return -1;
        }
        if (o2 == null) {
            return 1;
        }
        if (o1.equals(o2)) {
            return 0;
        }

        Activate a1 = o1.getClass().getAnnotation(Activate.class);
        Activate a2 = o2.getClass().getAnnotation(Activate.class);

        // 使用註解的 `after` 和 `before` 屬性,排序
        if ((a1.before().length > 0 || a1.after().length > 0 || a2.before().length > 0 || a2.after().length > 0) // (a1 或 a2) 存在 (`after` 或 `before`) 屬性。
                && o1.getClass().getInterfaces().length > 0 && o1.getClass().getInterfaces()[0].isAnnotationPresent(SPI.class)) { // 實現的介面,有 @SPI 註解。
            // 獲得拓展載入器
            ExtensionLoader<?> extensionLoader = ExtensionLoader.getExtensionLoader(o1.getClass().getInterfaces()[0]);
            // 以 a1 的視角,進行一次比較
            if (a1.before().length > 0 || a1.after().length > 0) {
                String n2 = extensionLoader.getExtensionName(o2.getClass());
                for (String before : a1.before()) {
                    if (before.equals(n2)) {
                        return -1;
                    }
                }
                for (String after : a1.after()) {
                    if (after.equals(n2)) {
                        return 1;
                    }
                }
            }
            // 以 a2 的視角,進行一次比較。
            if (a2.before().length > 0 || a2.after().length > 0) {
                String n1 = extensionLoader.getExtensionName(o1.getClass());
                for (String before : a2.before()) {
                    if (before.equals(n1)) {
                        return 1;
                    }
                }
                for (String after : a2.after()) {
                    if (after.equals(n1)) {
                        return -1;
                    }
                }
            }
        }

        // 使用註解的 `order` 屬性,排序。
        int n1 = a1 == null ? 0 : a1.order();
        int n2 = a2 == null ? 0 : a2.order();
        // never return 0 even if n1 equals n2, otherwise, o1 and o2 will override each other in collection like HashSet
        return n1 > n2 ? 1 : -1;
    }

總結:
責任鏈模式是設計模式中簡單且常見的設計模式,可能我們日常中也會經常應用責任鏈模式,dubbo中的責任鏈模式將靈活性發揮的很充分,不論是從分組概念、通過註解指定排序的優先順序、每個filter的是否移除 等,將每個filter做成了可插拔的,減少對代碼的侵入性,這點是非常值得我們學習的。


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

-Advertisement-
Play Games
更多相關文章
  • 面向對象有一個特征是繼承,即重用某個已有類的代碼,在其基礎上建立新的類,而無需重新編寫對應的屬性和方法,繼承之後拿來即用; 在其他的面向對象編程語言比如Java中,通常是指,子類繼承父類的屬性和方法; 我們現在來看看,JS是如何實現繼承這一個特征的; 要說明這個,我們首先要看看,每個對象都有的一個隱 ...
  • 案例介紹 歡迎來到我的小院,我是霍大俠,恭喜你今天又要進步一點點了!我們來用JavaScript編程實戰案例,做一個計數器。點擊按鈕數字改變,點擊重置數字歸0。通過實戰我們將學會forEach迴圈、contains方法、textContent屬性。 案例演示 點擊加號按鈕數字增加,點擊減號數字減少, ...
  • VUE常用方法 前言 總結了一些比較常用的方法,不全,僅供參考。 splice 簡介 splice(index,len,item):是vue中數組變異的方法之一,可以用來刪除,更新,和增加數組內容,會改變原數組。 參數說明 | 參數名 | 描述 | | | | | index | 數組下標 | | ...
  • 案例介紹 歡迎來到我的小院,我是霍大俠,恭喜你今天又要進步一點點了!我們來用JavaScript編程實戰案例,做一個背景圖像滾動效果。滾動滑鼠背景圖像縮小,下方滑動出現文字。通過實戰我們將學會obj.opacity方法、pageYOffset屬性、obj.style.backgroundSize方法 ...
  • 問題: 後端返迴文件流,前端使用axios下載或者線上預覽 下載文件流 import axios from 'axios' // 設置響應類型為blob axios.get('/api/app/xxx/downloadExcel', { responseType: 'blob' }).then(re ...
  • 隨著 Vue 3 正式版本的發佈,未來 Vue 3 將會成為前端的主流框架,這個毋庸置疑。Vue 3 在使用方面會相容部分 Vue 2.x 的特性,比如 options API。 所以,究竟是要先學習 Vue 2 打好基礎,還是直接學習 Vue 3 呢? 當 Vue 作者尤大面對這樣的提問時,直接給 ...
  • HTML5 介紹 引用 最全面的前端筆記來啦,包含了入門到入行的筆記,還支持實時效果預覽。小伙伴們不需要在花時間去寫筆記,或者是去網上找筆記了。面試高頻提問和你想要的筆記都幫你寫好了。支持移動端和PC端閱讀,深色和淺色模式。 原文鏈接:https://note.noxussj.top/ 什麼是 HT ...
  • 原型鏈與繼承 new 關鍵字的執行過程 讓我們回顧一下,this 指向里提到的new關鍵字執行過程。 創建一個新的空對象 將構造函數的原型賦給新創建對象(實例)的隱式原型 利用顯式綁定將構造函數的 this 綁定到新創建對象併為其添加屬性 返回這個對象 手寫new關鍵字的執行過程: function ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...