首页 > 其他分享 >SPI扩展点在业务中的使用及原理分析

SPI扩展点在业务中的使用及原理分析

时间:2023-11-29 14:00:31浏览次数:26  
标签:jsf org 扩展 private SPI 原理 import 加载

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

标签:jsf,org,扩展,private,SPI,原理,import,加载
From: https://www.cnblogs.com/jingdongkeji/p/17864703.html

相关文章

  • 检索增强生成 (RAG)的原理——传统检索+LLM生成相结合
    RAG是一种检索增强生成模型,由信息检索系统和seq2seq生成器组成。它的内部知识可以轻松地随时更改或补充,而无需浪费时间或算力重新训练整个模型。举个例子,假设你正在写一篇关于猫的文章,但你不确定如何描述猫的行为。你可以使用RAG来检索与猫行为相关的文档,然后将这些文档作为上下文......
  • 蛋白质组学原理与数据分析合集
    最近看到微信公众号:“生物信息与育种”的文章阅读量太低了,粉丝量基础也很少。可能是做动植物基因组和育种相关工作的人员基数相对较少,而且我也没有主动去推。于是想着把以前做的的蛋白质组学部分笔记迁移到公众号上,一是为内容备份,二是为增加粉丝和阅读量。不过这些笔记是几年前的......
  • 时间继电器的原理、结构和特点
    时间继电器的原理、结构和特点-工业控制-电子发烧友网https://www.elecfans.com/kongzhijishu/2038328.html时间继电器是一种特殊的继电器,它可以在设定的时间内自动开关电路。其工作原理主要是利用电磁铁的吸合和释放来控制开关的状态。其中,时间继电器一般由计时器和集......
  • 编辑原理总结
    编译原理第一章引言1.从面向机器的语言到面向人类的语言汇编指令:用符号表示的指令被称为汇编指令汇编语言:汇编指令的集合称为汇编语言2.语言之间的翻译转换(也被称为预处理):高级语言之间的翻译,如FORTRAN到ADA的转换编译:高级语言可以直接翻译成机器语言,也可以翻译成汇编语......
  • spine共享骨骼
    项目中遇到使用多个相同spine的问题:我们需要获取骨骼位置的时候,要拿下面这个类的信息 spine工具给的更新方案是:每个spine在Update中每帧更新,根据当前spine更新骨骼信息。这样比较费,比如我们项目场景中有五个角色,每个角色有五个编制,那光友方单位就是25个spine,很难蚌。优化方......
  • SPI
    概述SPI(ServiceProviderInterface)JDK内置的一种服务提供发现机制;用来启用框架扩展和替换组件; 当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类;当其他的程......
  • 计算机组成原理
    目录计算机概述数据总线CPU存储器输入/输出设备计算机的时标系统计算机概述计算机的基本组成:存储器:实现记忆功能的部件用来存放计算程序及参与运算的各种数据运算器:负责数据的算术运算和逻辑运算即数据的加工处理控制器:负责对程......
  • CPU原理学习
    本文是对B站踌躇月光大佬的8位二进制CPU实现教程的学习记录非常感谢这位大大能够提供这么好的教程!!!OvO半加法器半加器是指对输入的两个一位二进制数相加(A与B),输出一个结果位(S)和进位(C),没有进位的输入加法器电路,是一个实现一位二进制数的加法电路。计算公式:S=A^B(A异或B)C=......
  • Redis Sentinel(哨兵)实现原理之领导者Sentinel节点选举和故障转移
    领导者Sentinel节点选举Sentinel节点之间会做一个领导者选举的工作,选出一个Sentinel节点作为领导者进行故障转移的工作。Redis使用了Raft算法实现领导者选举。故障转移领导者选举出的Sentinel节点负责故障转移,过程如下:1.在从节点列表中选出一个节点作为新的主节点,这一步是相对复杂......
  • Linux驱动开发笔记(五):驱动连接用户层与内核层的文件操作集原理和Demo
    前言  驱动写好后,用户层使用系统函数调用操作相关驱动从而实现与系统内核的关联,本篇主要就是理解清楚驱动如何让用户编程来实现与内核的交互。<br>杂项设备文件操作集cd/usr/src/linux-headers-4.18.0-15viinclude/linux/fs.h  搜索到(vi则直接使用“/”):  struct......