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中,定义如下:
1
2
3
4
5
|
public interface IProfitLossEnrichExt extends IDomainExtension {
@Valid
@Comment ({ "批量盘盈亏数据丰富扩展" , "扩展的属性请放到对应明细的 extendContent.extendAttr Map字段中:profitLossBatchDetail.putExtendAttr(key, value)" })
List <ProfitLossBatchDetailExt> enrich(@NotEmpty List <ProfitLossBatchDetailExt> var1);
}
|
实现类定义在服务提供方的jar中,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
实现类: / * *
* 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
1
2
3
4
5
6
7
8
9
10
11
|
<?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
1
2
|
jsf.consumer.masterdata.alias = wms6 - test
jsf.registry.index = i.jsf.jd.com
|
通过在单元测试中加载consumer资源文件和配置文件把相关的依赖都加载进来,就能够实现对接口的贯穿调用测试。如下代码所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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这个类,可以看下这个类的源码,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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 转载请注明来源