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 转载请注明来源
评论