Java spi机制详解
一、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实现类,来执行具体业务逻辑