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中,定义如下:
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 转载请注明来源

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Taro:高性能小程序的最佳实践 | 京东云技术团队
前言 作为一个开放式的跨端跨框架解决方案,Taro 在大量的小程序和 H5 应用中得到了广泛应用。我们经常收到开发者的反馈,例如“渲染速度较慢”、“滑动不够流畅”、“性能与原生应用相比有差距” 等。这表明性能问题一直是困扰开发者的一个重要问题。 熟悉 Taro 的开发者应该知道,相比于 Taro 1/2,Taro 3 是一个更加注重运行时而轻量化编译时的框架。它的优势在于提供了更高效的代码编写方式,并拥有更丰富的生态系统。然而,这也意味着在性能方面可能会有一些损耗。 但是,使用 Taro 3 并不意味着我们必须牺牲应用的性能。事实上,Taro 已经提供了一系列的性能优化方法,并且不断探索更加极致的优化方案。 本文将为大家提供一些小程序开发的最佳实践,帮助大家最大程度地提升小程序应用的性能表现。 一、如何提升初次渲染性能 如果初次渲染的数据量非常大,可能会导致页面在加载过程中出现一段时间的白屏。为了解决这个问题,Taro 提供了预渲染功能(Prerender)。 使用 Prerender 非常简单,只需在项目根目录下的 config 文件夹中找到 index.js/dev.js/prod...
- 下一篇
语言模型文本处理基石:Tokenizer简明概述
编者按:近年来,人工智能技术飞速发展,尤其是大型语言模型的问世,让 AI 写作、聊天等能力有了质的飞跃。如何更好地理解和利用这些生成式 AI,成为许多开发者和用户关心的问题。 今天,我们推出的这篇文章有助于读者深入了解大语言模型的工作原理。作者指出,大语言模型的核心在于将文本转化为数字表征,这就需要介绍 tokenizer 的概念。通过 tokenizer ,文本被分词并映射为 token id,这为模型理解文本提供了坚实的基础。作者还比较了基于统计学的文本自动补全和大语言模型的不同之处,说明了上下文窗口大小的重要性。最后,作者建议读者在使用 OpenAI 等平台时观察定价规则与 token 数量的关系,并思考为什么是这种定价规则。 本文通俗易懂地介绍了 tokenizer 在语言模型中的关键作用,让我们更好理解这类模型的工作方式,对使用生成式AI有很好的启发作用。人工智能技术的发展日新月异,理解其基础原理尤为重要。我们将持续关注该领域新进展,为读者呈现有价值的技术分析。 以下是译文,enjoy! 🚢🚢🚢欢迎小伙伴们加入AI技术软件及技术交流群,追踪前沿热点,共探技术难题~ 作者...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS关闭SELinux安全模块
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Hadoop3单机部署,实现最简伪集群
- CentOS6,7,8上安装Nginx,支持https2.0的开启