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

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中,定义如下:

1public interface IProfitLossEnrichExt extends IDomainExtension { 2 @Valid 3 @Comment({"批量盘盈亏数据丰富扩展", "扩展的属性请放到对应明细的 extendContent.extendAttr Map字段中:profitLossBatchDetail.putExtendAttr(key, value)"}) 4 List<ProfitLossBatchDetailExt> enrich(@NotEmpty List<ProfitLossBatchDetailExt> var1); 5}

实现类定义在服务提供方的jar中,如下:

1实现类:/** 2 * ProfitLossEnrichExtImpl 3 * 批量盘盈亏数据丰富扩展 4 * 5 * @author jiayongqiang6 6 * @date 2021-10-15 11:30 7 */ 8@Extension(code = IPartnerIdentity.JX_CODE, value = "jxProfitLossEnrichExt") 9@Slf4j 10public class ProfitLossEnrichExtImpl implements IProfitLossEnrichExt { 11 private SkuInfoQueryService skuInfoQueryService; 12 13 @Override 14 public @Valid @Comment({"批量盘盈亏数据丰富扩展", "扩展的属性请放到对应明细的 extendContent.extendAttr Map字段中:profitLossBatchDetail" + 15 ".putExtendAttr(key, value)"}) List<ProfitLossBatchDetailExt> enrich(@NotEmpty List<ProfitLossBatchDetailExt> list) { 16 ... 17 return list; 18 } 19 20 @Autowired 21 public void setSkuInfoQueryService(SkuInfoQueryService skuInfoQueryService) { 22 this.skuInfoQueryService = skuInfoQueryService; 23 } 24}

这个实现类会依赖主数据的jsf服务SkuQueryService,SkuInfoQueryService对SkuQueryService进行rpc封装调用。通过Autowired的方式注入进来,消费者需要定义在xml文件中,这个跟我们通常引入jsf消费者是一样的。示例如下:jx/spring-jsf-consumer.xml

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

jar包的使用方可以直接加载consumer资源文件,也可以依赖得服务直接手动加到工程目录下。第一种方式更加方便,但是容易引起冲突,第二种方式虽然麻烦,但能够避免冲突。

2.2.2 扩展点的测试

因为扩展点依赖杰夫的关系,所以需要在配置文件中添加注册中心的配置和依赖服务的相关配置。示例如下:application-config.properties

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

通过在单元测试中加载consumer资源文件和配置文件把相关的依赖都加载进来,就能够实现对接口的贯穿调用测试。如下代码所示:

1package com.zhongyouex.wms.spi.inventory; 2 3import com.alibaba.fastjson.JSON; 4import com.jdwl.wms.inventory.spi.difference.entity.ProfitLossBatchDetailExt; 5import com.zhongyouex.wms.spi.inventory.service.SkuInfoQueryService; 6import org.junit.Before; 7import org.junit.Ignore; 8import org.junit.Test; 9import org.junit.runner.RunWith; 10import org.mockito.MockitoAnnotations; 11import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 12import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 13import org.springframework.context.annotation.ComponentScan; 14import org.springframework.context.annotation.PropertySource; 15import org.springframework.test.context.ContextConfiguration; 16import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 17 18import javax.annotation.Resource; 19import java.util.ArrayList; 20import java.util.List; 21 22@RunWith(SpringJUnit4ClassRunner.class) 23@ContextConfiguration({"classpath:jx/spring-jsf-consumer.xml"}) 24@PropertySource(value = {"classpath:application-config.properties"}) 25@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) 26@ComponentScan(basePackages = {"com.zhongyouex.wms"}) 27public class ProfitLossEnrichExtImplTest { 28 @Resource 29 SkuInfoQueryService skuInfoQueryService; 30 31 ProfitLossEnrichExtImpl profitLossEnrichExtImpl = new ProfitLossEnrichExtImpl(); 32 33 @Before 34 public void setUp() { 35 MockitoAnnotations.initMocks(this); 36 } 37 38 @Test 39 public void testEnrich() throws Exception { 40 profitLossEnrichExtImpl.setSkuInfoQueryService(skuInfoQueryService); 41 ProfitLossBatchDetailExt ext = new ProfitLossBatchDetailExt(); 42 ext.setSku("100008483105"); 43 ext.setWarehouseNo("6_6_618"); 44 ProfitLossBatchDetailExt ext1 = new ProfitLossBatchDetailExt(); 45 ext1.setSku("100009847591"); 46 ext1.setWarehouseNo("6_6_618"); 47 List<ProfitLossBatchDetailExt> list = new ArrayList<>(); 48 list.add(ext); 49 list.add(ext1); 50 profitLossEnrichExtImpl.enrich(list); 51 System.out.write(JSON.toJSONBytes(list)); 52 } 53} 54 55//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:

1public void prepare(@NotNull Object bean) { 2 this.initialize(bean); 3 InternalIndexer.prepare(this); 4} 5 6private void initialize(Object bean) { 7 Extension extension = (Extension)InternalAopUtils.getAnnotation(bean, Extension.class); 8 this.code = extension.code(); 9 this.name = extension.name(); 10 if (!(bean instanceof IDomainExtension)) { 11 throw BootstrapException.ofMessage(new String[]{bean.getClass().getCanonicalName(), " MUST implement IDomainExtension"}); 12 } else { 13 this.extensionBean = (IDomainExtension)bean; 14 Class[] var3 = InternalAopUtils.getTarget(this.extensionBean).getClass().getInterfaces(); 15 int var4 = var3.length; 16 17 for(int var5 = 0; var5 < var4; ++var5) { 18 Class extensionBeanInterfaceClazz = var3[var5]; 19 if (extensionBeanInterfaceClazz.isInstance(this.extensionBean)) { 20 this.extClazz = extensionBeanInterfaceClazz; 21 log.debug("{} has ext instance:{}", this.extClazz.getCanonicalName(), this); 22 break; 23 } 24 } 25 26 } 27}

3.2 java spi

通过上面简单的demo,可以看到最关键的实现就是ServiceLoader这个类,可以看下这个类的源码,如下:

1public final class ServiceLoader<S> implements Iterable<S> { 2 2 3 4 //扫描目录前缀 5 private static final String PREFIX = "META-INF/services/"; 3 6 7 // 被加载的类或接口 8 private final Class<S> service; 4 910 // 用于定位、加载和实例化实现方实现的类的类加载器11 private final ClassLoader loader; 51213 // 上下文对象14 private final AccessControlContext acc; 61516 // 按照实例化的顺序缓存已经实例化的类17 private LinkedHashMap<String, S> providers = new LinkedHashMap<>(); 71819 // 懒查找迭代器20 private java.util.ServiceLoader.LazyIterator lookupIterator; 82122 // 私有内部类,提供对所有的service的类的加载与实例化23 private class LazyIterator implements Iterator<S> { 924 Class<S> service; 1025 ClassLoader loader; 1126 Enumeration<URL> configs = null; 1227 String nextName = null; 132829 //...30 private boolean hasNextService() { 1431 if (configs == null) { 1532 try { 1633 //获取目录下所有的类34 String fullName = PREFIX + service.getName(); 1735 if (loader == null) 1836 configs = ClassLoader.getSystemResources(fullName); 1937 else38 configs = loader.getResources(fullName); 2039 } catch (IOException x) { 2140 //...41 } 2242 //....43 } 2344 } 244546 private S nextService() { 2547 String cn = nextName; 2648 nextName = null; 2749 Class<?> c = null; 2850 try { 2951 //反射加载类52 c = Class.forName(cn, false, loader); 3053 } catch (ClassNotFoundException x) { 3154 } 3255 try { 3356 //实例化57 S p = service.cast(c.newInstance()); 3458 //放进缓存59 providers.put(cn, p); 3560 return p; 3661 } catch (Throwable x) { 3762 //..63 } 3864 //..65 } 3966 } 4067 }

上面的代码只贴出了部分关键的实现,有兴趣的读者可以自己去研究,下面贴出比较直观的spi加载的主要流程供参考:

4 总结

SPI的两种提供方式各有优缺点,jar包方式部署成本低、依赖多,增加调用方的配置成本;jsf接口方式部署成本高,但调用方依赖少,只需要通过别名识别不同的BP。

总结下spi能带来的好处:

  • 不需要改动源码就可以实现扩展,解耦。
  • 实现扩展对原来的代码几乎没有侵入性。
  • 只需要添加配置就可以实现扩展,符合开闭原则。

作者:京东物流 贾永强

来源:京东云开发者社区 自猿其说Tech 转载请注明来源

点赞
收藏

评论区

加载中...

相关推荐

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

可插拔组件设计机制—SPI

SPI的全称是ServiceProviderInterface,即提供服务接口;是一种服务发现机制,SPI的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。本篇文章聚焦SPI的使用场景及使用介绍。

Cesium实现文字、点、多段线、多边形的实时绘制

背景知识点、线、面以及文字的实时绘制是GIS很重要的一个功能,是用户对感兴趣区域标注的业务需要。同时Cesium提供了点、线(多段线)、面及文字(label)绘制的接口,绘制方式总共有两种,一种是通过Entity实体的方式,一种是通过Primitives的方式。第一种使用较为简单,是在Primitives

Dubbo实践(七)扩展点

与JDK的SPI机制类似,Dubbo也在METAINF路径下定义了多种扩展接口。只是JDKSPI机制是Java后台帮你实现读取文件并对接具体的实现类,而Dubbo是自己去读文件。扩展点配置扩展点机制有几个要点:1. 根据关键字去读取配置文件,获得具体的实现类比如在dubboprovider.xml文件中配置:<dub

Taro 助力京喜拼拼项目性能体验优化

背景—2020年是社区团购风起云涌的一年,互联网大厂纷纷抓紧一分一秒跑步进场。“京喜拼拼”(微信搜京喜拼拼)是京东旗下的社区团购平台,依托京东供应链体系,精选低价好货,为社区用户提供次日达等优质服务。京喜拼拼团队技术选型使用Taro以便于实现多端需求,因此Taro团队有幸参与到“京喜拼拼”小程序的性能体验优化工作。全面体验

浅析Java - SPI机制 | 京东云技术团队

SPI是什么SPI全称ServiceProviderInterface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。整体机制如下图JavaSPI实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制