一、spi的概念
首先放个图:我们在“调用方”和“实现方”之间需要引入“接口”,可以思考一下什么情况应该把接口放入调用方,什么时候可以把接口归为实现方。
先来看看接口属于实现方的情况,这个很容易理解,实现方提供了接口和实现,我们可以引用接口来达到调用某实现类的功能,这就是我们经常说的api,它具有以下特征:
- 概念上更接近实现方
- 组织上位于实现方所在的包中
- 实现和接口在一个包中
当接口属于调用方时,我们就将其称为spi,全称为:service provider interface,spi的规则如下:
- 概念上更依赖调用方
- 组织上位于调用方所在的包中
- 实现位于独立的包中(也可认为在提供方中)
如下图所示:
总结一下SPI的思想:在系统的各个模块中,往往有不同的实现方案,例如日志模块的方案、xml解析的方案等,为了在装载模块的时候不具体指明实现类,我们需要一种服务发现机制,java spi就提供这样一种机制。有点类似于IoC的思想,将服务装配的控制权移到程序之外,在模块化设计时尤其重要。
二、Java SPI使用规范
- 定义服务的通用接口,针对通用的服务接口,提供具体的实现类。
- 在jar包的META-INF/services/目录中,新建一个文件,文件名为 接口的"全限定名"。 文件内容为该接口的具体实现类的"全限定名"。
- 将spi所在jar放在主程序的classpath中
- 服务调用方用java.util.ServiceLoader,用服务接口为参数,去动态加载具体的实现类到JVM中。
三、API和SPI的区别
- API:提供给调用方,完成某项功能的接口(类、或者方法),你可以使用它完成任务。
- SPI:是一种callback的思想,在一些通用的标准中(即API),为实现厂商提供扩展点。当API被调用时,会动态加载SPI路由到特定的实现中。
四、spi应用
在批处理框架中,暴露很多spi服务
例如
spi捞取服务:com.my.loanbatch.client.core.api.AssetSpiLoadService
spi处理服务:com.my.loanbatch.client.core.api.AssetSpiProcessService
调用方通过实现AssetSpiLoadService与AssetSpiProcessService接口,并且配置服务发布方提供的扩展点,当接口被调用时,可以达到调用方动态调用服务的目的,并且实现自己的业务逻辑
Q1:如何进行配置实现动态发现需要的服务?
在某系统(quick)中,需要调用服务发布方提供的两个spi服务时,我们引入loanbatch的jar包,注册对应bean,并进行xml配置,组装成不同的服务,供调用方实现动态服务发现:
<bean id="quick" class="com.my.loanbatch.client.core.model.config.BatchAppBean">
<property name="domain" value="fc"/>
<property name="mainTable" value="fl_main_job"/>
<property name="branchTable" value="fl_branch_job"/>
<property name="batchMessageConfig" ref="batchMessageConfig"/>
<property name="batchThreadConfig" ref="batchThreadConfig"/>
<!--在这里通过配置的方法定义接口的全限定名,提供服务发现机制-->
<property name="taskResolvers">
<list>
<!--配置两个批处理服务-->
<ref bean="dailyDueNoticeBatch"/>
<ref bean="autoOrderPayBatch"/>
</list>
</property>
</bean>
<!--=============同时实现服务发布方实现的扩展点,将其关联起来======================-->
<!-- 实现spi接口的两个类 -->
<bean id="stOrderCancelLoadService" class="com.mybank.bkfinlease.biz.batch.order.load.StOrderCancelLoadService"/>
<bean id="stOrderCancelProcessService" class="com.mybank.bkfinlease.biz.batch.order.process.StOrderCancelProcessService"/>
<!-- 通过扩展点配置服务下两个spi接口实现类 -->
<sofa:extension bean="batchExtBeanManager" point="task">
<sofa:content>
<!-- 任务名称 与文件配置中的任务名称一致 将实现spi接口的两个类组装成一个服务 -->
<taskDescriptor name="dailyDueNoticeBatch">
<dataSource>flSingleDataSource</dataSource>
<load>stOrderCancelLoadService</load>
<process>stOrderCancelProcessService</process>
<callback>stOrderCancelLoadService</callback>
</taskDescriptor>
</sofa:content>
</sofa:extension>
<bean id="stOrderAutoPayLoadService" class="com.mybank.bkfinlease.biz.batch.order.load.StOrderAutoPayLoadService"/>
<bean id="stOrderAutoPayProcessService" class="com.mybank.bkfinlease.biz.batch.order.process.StOrderAutoPayProcessService"/>
<sofa:extension bean="batchExtBeanManager" point="task">
<sofa:content>
<!-- 任务名称 与文件配置中的任务名称一致 -->
<taskDescriptor name="autoOrderPayBatch">
<dataSource>flSingleDataSource</dataSource>
<load>stOrderAutoPayLoadService</load>
<process>stOrderAutoPayProcessService</process>
<callback>stOrderAutoPayLoadService</callback>
</taskDescriptor>
</sofa:content>
</sofa:extension>
<!--==============具体任务配置,运行模式,分片方法,任务名称,租户id===============-->
<!-- demo-->
<bean id="dailyDueNoticeBatch" class="com.mybank.loanbatch.client.core.helper.node.ForeverTaskDbIndexAverageSplitResolver">
<property name="taskName" value="dailyDueNoticeBatch"/>
<property name="tntInstId" value="${tnt_inst_id}"/>
<property name="taskRunFactory" ref="taskRunForChainFactory"/>
<property name="resolveFactory" ref="resolveWithShardIdFactory"/>
<property name="runConditon" value="FOREVER_SHORT_MODEL"/>
<property name="zoneInfo" value="G"/>
</bean>
<!-- 自动放款-->
<bean id="autoOrderPayBatch" class="com.mybank.loanbatch.client.core.helper.node.ForeverTaskDbIndexAverageSplitResolver">
<property name="taskName" value="autoOrderPayBatch"/>
<property name="tntInstId" value="${tnt_inst_id}"/>
<property name="taskRunFactory" ref="taskRunForChainFactory"/>
<property name="resolveFactory" ref="resolveWithShardIdFactory"/>
<property name="runConditon" value="FOREVER_SHORT_MODEL"/>
<property name="zoneInfo" value="G"/>
</bean>
<!--=====================分片拆分配置========================-->
<!-- 带有shardid的拆分要素 -->
<bean id="resolveWithShardIdFactory"
class="com.mybank.loanbatch.client.core.model.config.factory.ResolveFactory">
<property name="shardIdSize" value="1"/>
<property name="dbSize" value="10"/>
<property name="shardIdLength" value="1"/>
<property name="prodDatacenters">
<list>
<value>MUA</value>
<value>MUB</value>
</list>
</property>
</bean>
<!-- dbsize 设置为1 -->
<bean id="resolveWithShardIdFactorySingle"
class="com.mybank.loanbatch.client.core.model.config.factory.ResolveFactory">
<property name="shardIdSize" value="1"/>
<property name="dbSize" value="1"/>
<property name="shardIdLength" value="1"/>
<property name="prodDatacenters">
<list>
<value>MUA</value>
</list>
</property>
</bean>
Q2:调用spi过程,如何发现动态注册的服务?
调用方在调用spi服务时,通过配置扩展点维护的beanMap,根据执行不同的批处理子任务来动态获取所需要的spi实现类
/** bean列表 */
private static Map<String, AssetSpiLoadService> LOAD_BEANS = new ConcurrentHashMap<String, AssetSpiLoadService>();
/** bean列表 */
private static Map<String, AssetSpiProcessService> PROCESS_BEANS = new ConcurrentHashMap<String, AssetSpiProcessService>();
具体调用逻辑:从beanMap中根据taskName动态获取想要执行的spi实现类,来执行具体业务逻辑