您现在的位置是:首页 > 文章详情

SPI扩展点在业务中的使用及原理分析 | 京东物流技术团队

日期:2023-11-29点击:188

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

原文链接:https://my.oschina.net/u/4090830/blog/10313986
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章