SPI擴展點在業務中的使用及原理分析

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/11/29/17864703.html
-Advertisement-
Play Games

目前倉儲中台和京喜BP的合作主要通過SPI擴展點的方式。好處就是對修改封閉、對擴展開放,中台不需要關心BP的業務實現細節,通過對不同BP配置擴展點的介面來達到個性化的目的。目前京喜BP主要提供兩種方式的介面實現,一種是jar包的方式,一種是提供jsf介面。 下邊來分別介紹下兩種方式的定義和實現。 ...


1 什麼是SPI

SPI 全稱Service Provider Interface。面向介面編程中,我們會根據不同的業務抽象出不同的介面,然後根據不同的業務實現建立不同規則的類,因此一個介面會實現多個實現類,在具體調用過程中,指定對應的實現類,當業務發生變化時會導致新增一個新的實現類,亦或是導致已經存在的類過時,就需要對調用的代碼進行變更,具有一定的侵入性。
整體機製圖如下:

Java SPI 實際上是“基於介面的編程+策略模式+配置文件”組合實現的動態載入機制。

2 SPI在京喜業務中的使用

2.1 簡介

目前倉儲中台和京喜BP的合作主要通過SPI擴展點的方式。好處就是對修改封閉、對擴展開放,中台不需要關心BP的業務實現細節,通過對不同BP配置擴展點的介面來達到個性化的目的。目前京喜BP主要提供兩種方式的介面實現,一種是jar包的方式,一種是提供jsf介面。
下邊來分別介紹下兩種方式的定義和實現。

2.2 jar包方式

2.2.1 說明及示例

擴展點介面繼承IDomainExtension,這個介面是dddplus包中的一個插件化介面,實現類要使用Extension(io.github.dddplus.annotation)註解,標記BP業務方和介面識別名稱,用來做個性化的區分實現。
以在庫庫存檔點擴展點為例,介面定義在調用方提供的jar中,定義如下:

public interface IProfitLossEnrichExt extends IDomainExtension {
    @Valid
    @Comment({"批量盤盈虧數據豐富擴展", "擴展的屬性請放到對應明細的 extendContent.extendAttr Map欄位中:profitLossBatchDetail.putExtendAttr(key, value)"})
    List<ProfitLossBatchDetailExt> enrich(@NotEmpty List<ProfitLossBatchDetailExt> var1);
}

實現類定義在服務提供方的jar中,如下:

實現類:/**
 * ProfitLossEnrichExtImpl
 * 批量盤盈虧數據豐富擴展
 *
 * @author jiayongqiang6
 * @date 2021-10-15 11:30
 */
@Extension(code = IPartnerIdentity.JX_CODE, value = "jxProfitLossEnrichExt")
@Slf4j
public class ProfitLossEnrichExtImpl implements IProfitLossEnrichExt {
    private SkuInfoQueryService skuInfoQueryService;

    @Override
    public @Valid @Comment({"批量盤盈虧數據豐富擴展", "擴展的屬性請放到對應明細的 extendContent.extendAttr Map欄位中:profitLossBatchDetail" +
            ".putExtendAttr(key, value)"}) List<ProfitLossBatchDetailExt> enrich(@NotEmpty List<ProfitLossBatchDetailExt> list) {
        ...
        return list;
    }

    @Autowired
    public void setSkuInfoQueryService(SkuInfoQueryService skuInfoQueryService) {
        this.skuInfoQueryService = skuInfoQueryService;
    }
}

這個實現類會依賴主數據的jsf服務SkuQueryService,SkuInfoQueryService對SkuQueryService進行rpc封裝調用。通過Autowired的方式註入進來,消費者需要定義在xml文件中,這個跟我們通常引入jsf消費者是一樣的。示例如下:jx/spring-jsf-consumer.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jsf="http://jsf.jd.com/schema/jsf"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
       http://jsf.jd.com/schema/jsf
       http://jsf.jd.com/schema/jsf/jsf.xsd"
       default-lazy-init="false" default-autowire="byName">
    <jsf:consumer id="skuQueryService" interface="com.jdwl.wms.masterdata.api.sku.SkuQueryService"
                  alias="${jsf.consumer.masterdata.alias}" protocol="jsf" check="false" timeout="10000"  retries="3"/>
</beans>

jar包的使用方可以直接載入consumer資源文件,也可以依賴得服務直接手動加到工程目錄下。第一種方式更加方便,但是容易引起衝突,第二種方式雖然麻煩,但能夠避免衝突。

2.2.2 擴展點的測試

因為擴展點依賴傑夫的關係,所以需要在配置文件中添加註冊中心的配置和依賴服務的相關配置。示例如下:application-config.properties

jsf.consumer.masterdata.alias=wms6-test
jsf.registry.index=i.jsf.jd.com

通過在單元測試中載入consumer資源文件和配置文件把相關的依賴都載入進來,就能夠實現對介面的貫穿調用測試。如下代碼所示:

package com.zhongyouex.wms.spi.inventory;

import com.alibaba.fastjson.JSON;
import com.jdwl.wms.inventory.spi.difference.entity.ProfitLossBatchDetailExt;
import com.zhongyouex.wms.spi.inventory.service.SkuInfoQueryService;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.PropertySource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:jx/spring-jsf-consumer.xml"})
@PropertySource(value = {"classpath:application-config.properties"})
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@ComponentScan(basePackages = {"com.zhongyouex.wms"})
public class ProfitLossEnrichExtImplTest {
    @Resource
    SkuInfoQueryService skuInfoQueryService;

    ProfitLossEnrichExtImpl profitLossEnrichExtImpl = new ProfitLossEnrichExtImpl();

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testEnrich() throws Exception {
        profitLossEnrichExtImpl.setSkuInfoQueryService(skuInfoQueryService);
        ProfitLossBatchDetailExt ext = new ProfitLossBatchDetailExt();
        ext.setSku("100008483105");
        ext.setWarehouseNo("6_6_618");
        ProfitLossBatchDetailExt ext1 = new ProfitLossBatchDetailExt();
        ext1.setSku("100009847591");
        ext1.setWarehouseNo("6_6_618");
        List<ProfitLossBatchDetailExt> list = new ArrayList<>();
        list.add(ext);
        list.add(ext1);
        profitLossEnrichExtImpl.enrich(list);
        System.out.write(JSON.toJSONBytes(list));
    }
}

//Generated with love by TestMe :) Please report issues and submit feature requests at: http://weirddev.com/forum#!/testme

2.3 jsf介面方式

jsf方式的擴展點實現和jar包方式是一樣的,區別是這種方式不需要依賴服務提供方實現的jar,無需載入具體的實現類。通過配置jsf介面的傑夫別名來識別擴展點併進行擴展點的調用。

3 SPI原理分析

3.1dddplus

dddplus-runtime包中ExtensionDef主要是用來載入擴展點bean到InternalIndexer:

public void prepare(@NotNull Object bean) {
    this.initialize(bean);
    InternalIndexer.prepare(this);
}

private void initialize(Object bean) {
    Extension extension = (Extension)InternalAopUtils.getAnnotation(bean, Extension.class);
    this.code = extension.code();
    this.name = extension.name();
    if (!(bean instanceof IDomainExtension)) {
        throw BootstrapException.ofMessage(new String[]{bean.getClass().getCanonicalName(), " MUST implement IDomainExtension"});
    } else {
        this.extensionBean = (IDomainExtension)bean;
        Class[] var3 = InternalAopUtils.getTarget(this.extensionBean).getClass().getInterfaces();
        int var4 = var3.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            Class extensionBeanInterfaceClazz = var3[var5];
            if (extensionBeanInterfaceClazz.isInstance(this.extensionBean)) {
                this.extClazz = extensionBeanInterfaceClazz;
                log.debug("{} has ext instance:{}", this.extClazz.getCanonicalName(), this);
                break;
            }
        }

    }
}

3.2 java spi

通過上面簡單的demo,可以看到最關鍵的實現就是ServiceLoader這個類,可以看下這個類的源碼,如下:

public final class ServiceLoader<S> implements Iterable<S> {
 2 3 4     //掃描目錄首碼 5     private static final String PREFIX = "META-INF/services/";
 6 7     // 被載入的類或介面 8     private final Class<S> service;
 910     // 用於定位、載入和實例化實現方實現的類的類載入器11     private final ClassLoader loader;
1213     // 上下文對象14     private final AccessControlContext acc;
1516     // 按照實例化的順序緩存已經實例化的類17     private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
1819     // 懶查找迭代器20     private java.util.ServiceLoader.LazyIterator lookupIterator;
2122     // 私有內部類,提供對所有的service的類的載入與實例化23     private class LazyIterator implements Iterator<S> {
24         Class<S> service;
25         ClassLoader loader;
26         Enumeration<URL> configs = null;
27         String nextName = null;
2829         //...30         private boolean hasNextService() {
31             if (configs == null) {
32                 try {
33                     //獲取目錄下所有的類34                     String fullName = PREFIX + service.getName();
35                     if (loader == null)
36                         configs = ClassLoader.getSystemResources(fullName);
37                     else38                         configs = loader.getResources(fullName);
39                 } catch (IOException x) {
40                     //...41                 }
42                 //....43             }
44         }
4546         private S nextService() {
47             String cn = nextName;
48             nextName = null;
49             Class<?> c = null;
50             try {
51                 //反射載入類52                 c = Class.forName(cn, false, loader);
53             } catch (ClassNotFoundException x) {
54             }
55             try {
56                 //實例化57                 S p = service.cast(c.newInstance());
58                 //放進緩存59                 providers.put(cn, p);
60                 return p;
61             } catch (Throwable x) {
62                 //..63             }
64             //..65         }
66     }
67 }

上面的代碼只貼出了部分關鍵的實現,有興趣的讀者可以自己去研究,下麵貼出比較直觀的spi載入的主要流程供參考:

4 總結

SPI的兩種提供方式各有優缺點,jar包方式部署成本低、依賴多,增加調用方的配置成本;jsf介面方式部署成本高,但調用方依賴少,只需要通過別名識別不同的BP。

總結下spi能帶來的好處:

  • 不需要改動源碼就可以實現擴展,解耦。
  • 實現擴展對原來的代碼幾乎沒有侵入性。
  • 只需要添加配置就可以實現擴展,符合開閉原則。

作者:京東物流 賈永強

來源:京東雲開發者社區 自猿其說Tech 轉載請註明來源


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

-Advertisement-
Play Games
更多相關文章
  • 寫在前面 最近比較迷AI繪圖,那就上個圖吧,我感覺還挺好看的。 可能會有人說,之前不一致分享的是flask嗎,怎麼突然改到django了? 這個問題問得好,開發環境遇到了一些小困難! 不過django,真的是很流行,一點都不過時,這您放心好了!不多說,直接看效果吧! 環境搭建 1、當前環境版本 py ...
  • and dest,src將目標與源做與操作 or dest,src將目標與源做或操作 add 加得數的值超出範圍即會溢出 inc 彙編語言中的自增指令,相當於++ div指令 不會給出被除數 切記提前在預設的寄存器中設置好被除數,且預設寄存器不做別的用處 dup設置記憶體空間,與db、dw、dd等數據 ...
  • 概述 - Overview 在我初學 C++ 時,static、inline、extern 可能是最令我迷惑的 C++ 說明符,原因是它們在不同的語境下會發揮不同的作用,而且某些說明符的含義已經和以前不同,這加劇了我在查詢資料時的困擾。所以今天決定好好總結一下。 首先要介紹 C++ 的兩個概念:存儲 ...
  • 哈嘍大家好,我是鹹魚 當我們在學習 Python 的時候,可能會經常遇到單下劃線 _ 和雙下劃線 __ 這兩種命名方式 單下劃線 _ 和雙下劃線 __ 不僅僅是只是一種簡單的命名習慣,它們在 Python 中有著特殊的含義,對於代碼的可讀性和功能實現有著關鍵的作用。 那麼今天我們來看一看在 Pyth ...
  • LinkedList是Java中的一個雙向鏈表。它實現了List和Deque介面,在使用時可以像List一樣使用元素索引,也可以像Deque一樣使用隊列操作。LinkedList每個節點都包含了前一個和後一個節點的引用,因此可以很方便地在其中進行節點的插入、刪除和移動。相比於ArrayList,Li... ...
  • gRPC(gRPC Remote Procedure Call)是由 Google 開發的開源 RPC 框架,它基於 HTTP/2 標準,使用 Protocol Buffers 作為介面定義語言(IDL)。gRPC 提供了一種高效、跨語言、跨平臺的遠程過程調用(RPC)解決方案,被廣泛應用於構建分佈 ...
  • Zlib是一個開源的數據壓縮庫,提供了一種通用的數據壓縮和解壓縮演算法。它最初由Jean-Loup Gailly和Mark Adler開發,旨在成為一個高效、輕量級的壓縮庫,其被廣泛應用於許多領域,包括網路通信、文件壓縮、資料庫系統等。其壓縮演算法是基於DEFLATE演算法,這是一種無損數據壓縮演算法,通常... ...
  • 大家好,我是棧長。 沒錯,就在昨天,Spring Boot 2.x 停止維護了。。 Spring Boot 最後一個 2.x 的版本 2.7.x 已經停止維護,3.0.x 也停止維護了,商業支持的版本也只有 2.6.x 了,2.5.x 以下的版本徹底退出歷史舞臺。。 從路線圖可以看到每個版本的終止時 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...