痛點
拿差錯系統來說,大體上有核查、差錯提交、貸記調整、例外交易、例外複核、收付調整等差錯交易型別,每個差錯交易型別又分為很多原因碼,比如核查有2001、2201、2301、2502、2102、2401、2402等原因碼,每個原因碼還可能分有不同的子原因碼。在接收到差錯交易請求時,由於每個差錯交易的發起方與接收方的不同,再加上代理清算、原交易狀態、清算狀態等各個維度的判斷,會導致整個差錯業務的校驗部分比較複雜,但是每個差錯交易的整體邏輯是相同的。前期程式碼的開發都是由不同的人員開發,程式碼編寫能力也都不相同,導致整體程式碼校驗部分看起來非常複雜,雖然已經對各個方法進行了封裝,但整體看起來仍然不太滿意,所以需要在業務穩定後,對原有程式碼進行重構,加強模組化與可擴充套件性。
下面截圖為差錯核查的程式碼,雖然已經有很多註釋了,但是並沒有一個很清晰的思路在裡面。
dubbo框架的SPI機制
在閱讀dubbo原始碼的時候,會發現整體的結構非常清晰,而且對外提供了很多擴充套件點,它是怎麼做到的呢?以下內容如果看的不太懂,直接看下一章節後再回頭看。
dubbo擴充套件都會被放置在下面幾個目錄
-
META-INF/dubbo/internal/ :用於dubbo內部提供的擴充實現
-
META-INF/dubbo/ :用於使用者自定義的擴充實現
-
META-INF/service/ :Java SPI的配置目錄
下圖為dubbo的Compiler介面的內部實現配置
另外需要知道三個註解及一個物件:@SPI,@Adaptive,@Activate,URL。
@SPI:定義一個介面是一個可被擴充套件的介面,@SPI註解中的值代表其預設的實現類,對應上面配置檔案中的key。下圖為Compile介面,預設實現為javassist。
@Adaptive:分為兩種
-
標記在類上,代表手工實現擴充實現類的邏輯,比如下圖Compile的擴充類實現AdaptiveCompiler,如果DEFAULT_COMPILE有值(就是對應的dubbo配置檔案中的key),就使用其對應的實現類實現,如果沒有配置就使用預設的實現(對應SPI註解中配置的值)。
-
標記在方法上時,代表自動生成程式碼實現該介面的擴充實現類(這個目前我們可以先不用,因為這塊的動態生成的程式碼和dubbo內部的邏輯綁的很緊,有必要時可以自己重新實現這塊功能)。
@Activate:代表自動啟用,是什麼意思呢?拿Filter介面相關的服務來說,在呼叫ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension相關的方法時,返回一個根據getActivateExtension引數過濾出來的List。比如下圖中組裝過濾器時先根據key和group過濾出對應的過濾器陣列,在進行其他操作。
URL:dubbo內部將服務相關的所有變數放到了一個URL物件中,這樣很容易在各個環節進行處理,URL就相當於各個環節間通用的語言。
借鑑dubbo spi機制編寫可擴充套件的程式碼
我是在研究dubbo原始碼的時候,感受到其spi機制在實現可擴充套件程式碼時的便利與強大。然後回想當前的業務場景,是不是也可以藉助這種模式來編寫出優美的可擴充套件性程式碼。我下面拿dubbo spi的這一套實現一個最簡單的差錯交易demo。
假如我們現在要實現一個最簡單的需求:實現核查與差錯提交兩種交易,有2201和2301兩個原因碼。這在目前的程式碼中是核查與差錯提交各用一個介面實現,然後在核查中使用一個Map儲存了2201和2301的校驗實現類,根據引數拿到原因碼的實現類後在呼叫。下面開始借鑑dubbo spi機制實現上述需求。
執行結果
自動擴充執行結果
先看下主要程式碼及程式碼執行結果,注意看紅線部分,我們當前實現的需求是核查的2301原因碼:
好,現在需求變了,我們要新增一個差錯提交的2201的原因碼,怎麼辦?改程式碼結構?不存在的,看下圖,只需要將disputeType和disputeReasonCode的值調整一下即可:
自動啟用過濾器執行結果
@Activate的作用是自動啟用,是什麼意思先不要管,先看如下的執行結果,紅線部分是獲取groupName為dispute的Filter實現列表,再組裝成一個Invoker執行鏈。先根據過濾器配置的order順序執行各個過濾器,最後再執行業務實現,也就是i'm last invoker這部分的邏輯。此處過濾器的裝配使用了責任鏈設計模式。
程式碼結構介紹
先看整體結構
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等框架實現一個類似的可擴充套件框架,目標是將程式碼模組化管理,外掛式開發,盡力編寫出可擴充套件、可閱讀、可測試的高質量程式碼。