您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
SPI扩展点的使用及原理分析
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
SPI扩展点的使用及原理分析
自猿其说Tech
2022-06-13
IP归属:未知
26240浏览
### 1 什么是SPI SPI 全称Service Provider Interface。面向接口编程中,我们会根据不同的业务抽象出不同的接口,然后根据不同的业务实现建立不同规则的类,因此一个接口会实现多个实现类,在具体调用过程中,指定对应的实现类,当业务发生变化时会导致新增一个新的实现类,亦或是导致已经存在的类过时,就需要对调用的代码进行变更,具有一定的侵入性。 整体机制图如下: ![](//img1.jcloudcs.com/developer.jdcloud.com/cdccb6f7-5584-4170-b311-931b2936f56520220613154920.png) 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中,定义如下: ```java public interface IProfitLossEnrichExt extends IDomainExtension { @Valid @Comment({"批量盘盈亏数据丰富扩展", "扩展的属性请放到对应明细的 extendContent.extendAttr Map字段中:profitLossBatchDetail.putExtendAttr(key, value)"}) List<ProfitLossBatchDetailExt> enrich(@NotEmpty List<ProfitLossBatchDetailExt> var1); } ``` 实现类定义在服务提供方的jar中,如下: ```java 实现类:/** * 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 <?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资源文件和配置文件把相关的依赖都加载进来,就能够实现对接口的贯穿调用测试。如下代码所示: ```java 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: ```java 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这个类,可以看下这个类的源码,如下: ```java 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加载的主要流程供参考: ![](//img1.jcloudcs.com/developer.jdcloud.com/ec18784e-5775-40e4-a934-9add6cd9374220220613155219.png) ### 4 总结 SPI的两种提供方式各有优缺点,jar包方式部署成本低、依赖多,增加调用方的配置成本;jsf接口方式部署成本高,但调用方依赖少,只需要通过别名识别不同的BP。 总结下spi能带来的好处: - 不需要改动源码就可以实现扩展,解耦。 - 实现扩展对原来的代码几乎没有侵入性。 - 只需要添加配置就可以实现扩展,符合开闭原则。 ------------ ###### 自猿其说Tech-JDL京东物流技术与数据智能部 ###### 作者:贾永强
原创文章,需联系作者,授权转载
上一篇:前端监控之性能与异常
下一篇:R2在全渠道业务线的落地
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号