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

SPI扩展点在业务中的使用及原理分析的更多相关文章

  1. Dubbo源码剖析六之SPI扩展点的实现之Adaptive功能实现原理

    接Dubbo源码剖析六之SPI扩展点的实现之getExtensionLoader - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)继续分析Adaptive功能实现原理.Adaptive的主 ...

  2. 关于boost中enable_shared_from_this类的原理分析

    首先要说明的一个问题是:如何安全地将this指针返回给调用者.一般来说,我们不能直接将this指针返回.想象这样的情况,该函数将this指针返回到外部某个变量保存,然后这个对象自身已经析构了,但外部变 ...

  3. 通过Shell和Redis来实现集群业务中日志的实时收集分析

    http://www.linuxidc.com/Linux/2013-05/83935.htm

  4. TCP中ECN的工作原理分析二(摘自:RFC3168)

    英文源:http://www.icir.org/floyd/ecn.html 发送端和接收端处理: The TCP Sender For a TCP connection using ECN, new ...

  5. Android中线程间通信原理分析:Looper,MessageQueue,Handler

    自问自答的两个问题 在我们去讨论Handler,Looper,MessageQueue的关系之前,我们需要先问两个问题: 1.这一套东西搞出来是为了解决什么问题呢? 2.如果让我们来解决这个问题该怎么 ...

  6. RxJava 中的Map函数原理分析

    首先看一段Map函数的使用代码: Observable.create(new Observable.OnSubscribe<Integer>() { @Override public vo ...

  7. String类中intern方法的原理分析

    一,前言 ​ 昨天简单整理了JVM内存分配和String类常用方法,遇到了String中的intern()方法.本来想一并总结起来,但是intern方法还涉及到JDK版本的问题,内容也相对较多,所以今 ...

  8. Dubbo 中 Zookeeper 注册中心原理分析

    vivo 互联网服务器团队- Li Wanghong 本文通过分析Dubbo中ZooKeeper注册中心的实现ZooKeeperResitry的继承体系结构,自顶向下分析了AbstractRegist ...

  9. Android中Input型输入设备驱动原理分析(一)

    转自:http://blog.csdn.net/eilianlau/article/details/6969361 话说Android中Event输入设备驱动原理分析还不如说Linux输入子系统呢,反 ...

  10. Android中Input型输入设备驱动原理分析<一>

    话说Android中Event输入设备驱动原理分析还不如说Linux输入子系统呢,反正这个是没变的,在android的底层开发中对于Linux的基本驱动程序设计还是没变的,当然Android底层机制也 ...

随机推荐

  1. 二 APPIUM Android自动化 环境搭建(转)

    1.安装JAVA运行环境 2.安装Android开发环境 3.安装nodejs 下载地址:https://nodejs.org/en/ 下载完成之后双击安装. 4.安装APPIUM,Appium服务端 ...

  2. python实现创建一个银行类,这个类实现了两个方法,第一个方法可以将用户信息写入到文件中,第二个方法可以读取文件中的用户信息出来

    class bank: def user_info(self): a=input('请输入用户信息:') # 不写encoding = 'utf-8'中文会乱码 with open('info.txt ...

  3. el-date-picker 在表单中宽度(width)问题

    在使用element-plus的日期选择组件 el-date-picker的时候,发现form表单内的日期选择框并不能跟el-input 一样把宽度撑满.而是要小一圈. 这样在排版中显得不太整齐,但是 ...

  4. [刺客伍六七&黑客] 魔刀千刃

    魔刀千刃的特写 诞生之日:2023.7.29 此后会在此记录如何自己写一个自己的python库以及魔刀千刃的维护过程. 魔刀千刃(evilblade) **只攻不防,天下无双** 实战 (和堆攻击帖子 ...

  5. 渗透小tis

    知己知彼,百战不殆 1.如果提示缺少参数,如{msg:params error},可尝使用字典模糊测试构造参数,进一步攻击. 2.程序溢出,int最大值为2147483647,可尝试使用该值进行整数溢 ...

  6. 你的开发套件已到货「GitHub 热点速览」

    这周的 GitHub 热点榜,撇开上周的介绍过的几个项目,剩下就两字:套件.像是搜罗了大量黑客工具的 hackingtool,还有打算一统米哈游游戏客户端的 Starward,以及好用的 CV 库 s ...

  7. 《Python魔法大冒险》002 编程是什么?

    魔法师:在这个充满魔法和奇迹的数字时代,你是否好奇过计算机是如何运作的?当你用手机玩游戏.在电脑上浏览网页.看动画电影,你是否想过这背后的秘密是什么?别担心,今天我们将揭开这神秘的面纱,一起来探索编程 ...

  8. [初学C#] 第二习题 : 快递跟踪信息查询

    刚学C#, 折腾的一个小玩意. 熟悉和了解C#这门编程语言. 没有啥特殊意义 解锁技能 - System.Net 的 WebRequest等http请求 - Newtonsoft.Json 这个第三方 ...

  9. PHP对关联数组(键值对数组)遍历循环

    PHP对关联数组循环遍历 $arr=array('yxb'=>20,'ylg'=>21,'lgj'=18); foreach($arr as $name=>$value) { ech ...

  10. 试试用Markdown来设计表单

    相信很多后端开发.对于前端知识是比较零碎的,所以很多时候写表单这样的工作,一般就是复制黏贴,然后改改字段.对于HTML格式,一直觉得比较杂乱,不够简洁. 最近TJ发现了一个有趣的小工具:Create ...