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 轉載請註明來源