編寫可擴充套件程式

曉小麥發表於2019-03-11

痛點

​ 拿差錯系統來說,大體上有核查、差錯提交、貸記調整、例外交易、例外複核、收付調整等差錯交易型別,每個差錯交易型別又分為很多原因碼,比如核查有2001、2201、2301、2502、2102、2401、2402等原因碼,每個原因碼還可能分有不同的子原因碼。在接收到差錯交易請求時,由於每個差錯交易的發起方與接收方的不同,再加上代理清算、原交易狀態、清算狀態等各個維度的判斷,會導致整個差錯業務的校驗部分比較複雜,但是每個差錯交易的整體邏輯是相同的。前期程式碼的開發都是由不同的人員開發,程式碼編寫能力也都不相同,導致整體程式碼校驗部分看起來非常複雜,雖然已經對各個方法進行了封裝,但整體看起來仍然不太滿意,所以需要在業務穩定後,對原有程式碼進行重構,加強模組化與可擴充套件性。

​ 下面截圖為差錯核查的程式碼,雖然已經有很多註釋了,但是並沒有一個很清晰的思路在裡面。

img

dubbo框架的SPI機制

​ 在閱讀dubbo原始碼的時候,會發現整體的結構非常清晰,而且對外提供了很多擴充套件點,它是怎麼做到的呢?以下內容如果看的不太懂,直接看下一章節後再回頭看。

​ dubbo擴充套件都會被放置在下面幾個目錄

  1. META-INF/dubbo/internal/ :用於dubbo內部提供的擴充實現

  2. META-INF/dubbo/ :用於使用者自定義的擴充實現

  3. META-INF/service/ :Java SPI的配置目錄

    下圖為dubbo的Compiler介面的內部實現配置

    img

​ 另外需要知道三個註解及一個物件:@SPI,@Adaptive,@Activate,URL。

@SPI:定義一個介面是一個可被擴充套件的介面,@SPI註解中的值代表其預設的實現類,對應上面配置檔案中的key。下圖為Compile介面,預設實現為javassist。

1552302968617

@Adaptive:分為兩種

  1. 標記在類上,代表手工實現擴充實現類的邏輯,比如下圖Compile的擴充類實現AdaptiveCompiler,如果DEFAULT_COMPILE有值(就是對應的dubbo配置檔案中的key),就使用其對應的實現類實現,如果沒有配置就使用預設的實現(對應SPI註解中配置的值)。

    img

  2. 標記在方法上時,代表自動生成程式碼實現該介面的擴充實現類(這個目前我們可以先不用,因為這塊的動態生成的程式碼和dubbo內部的邏輯綁的很緊,有必要時可以自己重新實現這塊功能)。

@Activate:代表自動啟用,是什麼意思呢?拿Filter介面相關的服務來說,在呼叫ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension相關的方法時,返回一個根據getActivateExtension引數過濾出來的List。比如下圖中組裝過濾器時先根據key和group過濾出對應的過濾器陣列,在進行其他操作。

img

URL:dubbo內部將服務相關的所有變數放到了一個URL物件中,這樣很容易在各個環節進行處理,URL就相當於各個環節間通用的語言。

借鑑dubbo spi機制編寫可擴充套件的程式碼

​ 我是在研究dubbo原始碼的時候,感受到其spi機制在實現可擴充套件程式碼時的便利與強大。然後回想當前的業務場景,是不是也可以藉助這種模式來編寫出優美的可擴充套件性程式碼。我下面拿dubbo spi的這一套實現一個最簡單的差錯交易demo。

​ 假如我們現在要實現一個最簡單的需求:實現核查與差錯提交兩種交易,有2201和2301兩個原因碼。這在目前的程式碼中是核查與差錯提交各用一個介面實現,然後在核查中使用一個Map儲存了2201和2301的校驗實現類,根據引數拿到原因碼的實現類後在呼叫。下面開始借鑑dubbo spi機制實現上述需求。

執行結果

自動擴充執行結果

先看下主要程式碼及程式碼執行結果,注意看紅線部分,我們當前實現的需求是核查的2301原因碼:

1552310720159

好,現在需求變了,我們要新增一個差錯提交的2201的原因碼,怎麼辦?改程式碼結構?不存在的,看下圖,只需要將disputeType和disputeReasonCode的值調整一下即可:

img

自動啟用過濾器執行結果

@Activate的作用是自動啟用,是什麼意思先不要管,先看如下的執行結果,紅線部分是獲取groupName為dispute的Filter實現列表,再組裝成一個Invoker執行鏈。先根據過濾器配置的order順序執行各個過濾器,最後再執行業務實現,也就是i'm last invoker這部分的邏輯。此處過濾器的裝配使用了責任鏈設計模式。

img

程式碼結構介紹

先看整體結構

img

META-INF/dubbo目錄中放的是我們的擴充套件配置:

com.epcc.risk.api.biz.base.spi.disputeReasonCode.DisputeReasonCode檔案:差錯原因碼擴充套件配置

com.epcc.risk.api.biz.base.spi.disputeTran.DisputeTran檔案:差錯交易擴充套件

com.epcc.risk.api.biz.base.spi.Filter檔案:過濾器擴充套件配置

spi包下放的是幾個公共的類:

  • DisputeTranTest:測試類入口
  • Filter:過濾器介面
  • Invoker:業務執行介面
  • Context:差錯業務類,類似於dubbo中的URL
  • MyExtensionLoader:等同dubbo中的ExtensionLoader,將其複製了一份,新增了一個getActivateExtension方法用來做實驗用,因為其他的getActivateExtension都帶有各種引數,同dubbo框架綁的比較死。

filter包下放的是過濾器實現:

  • LimitFilter:模擬限流過濾器
  • LogFilter:模擬日誌過濾器
  • TokenFilter:模擬Token過濾器

disputeTran包下放的差錯交易實現:

  • AdaptiveDisputeTran:差錯交易擴充實現類
  • DisputeTran:差錯交易介面
  • InspectTran:核查介面實現
  • SubmitTran:差錯提交介面實現

disputeReasonCode包下放的是差錯原因碼實現:

  • AdaptiveDisputeTran:差錯原因碼擴充實現類
  • DisputeReasonCode:差錯原因碼介面
  • ReasonCode2201:差錯原因碼2201的實現
  • ReasonCode2301:差錯原因碼2301的實現

程式碼實現

自動擴充程式碼實現

拿差錯交易的實現來說,獲取DisputeTran實現的程式碼是

DisputeTran disputeTran = MyExtensionLoader.getExtensionLoader(DisputeTran.class).getAdaptiveExtension();
複製程式碼

差錯交易配置檔案中配置了核查實現、差錯提交實現及差錯交易的擴充實現類:

inspectTran=com.epcc.risk.api.biz.base.spi.disputeTran.InspectTran
submitTran=com.epcc.risk.api.biz.base.spi.disputeTran.SubmitTran
adptiveDisputeTran=com.epcc.risk.api.biz.base.spi.disputeTran.AdaptiveDisputeTran
複製程式碼

先看一下介面如何定義,介面類上有一個@SPI標記其是一個可擴充套件介面,預設擴充是inspectTran。

@SPI("inspectTran")
public interface DisputeTran {

    /**
     *
     * @param context
     * @return
     */
    public Result process(Context context);

}
複製程式碼

再看下DisputeTran的擴充實現類,類上有一個@Adaptive註解標記其是一個擴充實現類,實現是如果contextType中有值,就使用該值找到實現擴充,如果沒有值,使用預設的擴充:

@Adaptive
public class AdaptiveDisputeTran implements DisputeTran {

    @Override
    public Result process(Context context) {
        DisputeTran disputeTran;
        ExtensionLoader<DisputeTran> loader = ExtensionLoader.getExtensionLoader(DisputeTran.class);
        if (context.getDisputeType() != null && context.getDisputeType() != "") {
            disputeTran = loader.getExtension(context.getDisputeType());
        } else {
            disputeTran = loader.getDefaultExtension();
        }
        return disputeTran.process(context);
    }
}
複製程式碼

再看下核查交易的實現,列印了兩條日誌來代表其要做的事情。另外,差錯交易中用同樣的方式獲取了差錯原因碼的實現,此處就不做講解了,原理相同。

@Slf4j
public class InspectTran implements DisputeTran {

    DisputeReasonCode disputeReasonCode = ExtensionLoader.getExtensionLoader(DisputeReasonCode.class).getAdaptiveExtension();

    @Override
    public Result process(Context context) {
        log.info("deal inspect");
        disputeReasonCode.deal(context);
        log.info("insert db");
        return null;
    }
}
複製程式碼

再回過頭看主流程執行的程式碼,其disputeType的值為inspectTran時,便會根據擴充實現類中的邏輯,取配置檔案中對應的com.epcc.risk.api.biz.base.spi.disputeTran.InspectTran為實現類進行處理,disputeReasonCode值為reasonCode2201時,同樣根據差錯原因碼的擴充實現類邏輯,取com.epcc.risk.api.biz.base.spi.disputeReasonCode.inspect.ReasonCode2201為實現類進行處理。當新增了差錯型別或差錯原因碼時,無需改動主邏輯程式碼,直接在配置檔案中指定新增的差錯型別和差錯原因碼的實現類即可。

    @Test
    public void test1() {
        Context context = new Context();
        context.setDisputeType("submitTran");
        context.setDisputeReasonCode("reasonCode2201");
        disputeTran.process(context);
    }
複製程式碼

自動啟用程式碼實現

Filter配置檔案中配置了各個過濾器對應的過濾器實現:

logFilter=com.epcc.risk.api.biz.base.spi.filter.LogFilter
limitFilter=com.epcc.risk.api.biz.base.spi.filter.LimitFilter
tokenFilter=com.epcc.risk.api.biz.base.spi.filter.TokenFilter
複製程式碼

先看Filter的實現,介面類上有一個@SPI標記其是一個可擴充套件介面。

@SPI
public interface Filter {

    Result invoke(Invoker invoker, Context context);

    default Result onResponse(Result result, Context invocation) {
        return result;
    }

}
複製程式碼

再看其各個實現類的實現,這裡只列出LogFilter和LimitFilter,類上有@Activate註解表名其是一個自動啟用的類,其groupName都是dispute(這裡的groupName是用來給獲取自動啟用的列表提供過濾條件)。

@Slf4j
@Activate(group = "dispute", order = 999)
public class LogFilter implements Filter {

    @Override
    public Result invoke(Invoker invoker, Context context) {
        log.info("i'm log filter, order is 999");
        return invoker.invoke(context);
    }
}
複製程式碼
@Slf4j
@Activate(group = "dispute", order = 1000)
public class LimitFilter implements Filter {

    @Override
    public Result invoke(Invoker invoker, Context context) {
        log.info("i'm limit filter, order is 1000");
        return invoker.invoke(context);
    }
}
複製程式碼

總結

這部分內容是完全模擬dubbo spi的機制寫的一個簡單的可擴充套件程式的小demo,dubbo內部實現可擴充套件程式主要靠的是com.alibaba.dubbo.common.extension.ExtensionLoader,我們完全可以按照其實現一個自己的可擴充套件程式。也可以利用spring等框架實現一個類似的可擴充套件框架,目標是將程式碼模組化管理,外掛式開發,盡力編寫出可擴充套件、可閱讀、可測試的高質量程式碼。

程式碼

github.com/ls960972314… 包下。

相關文章