Java spi机制详解

一、spi的概念

 

 

首先放个图:我们在“调用方”和“实现方”之间需要引入“接口”,可以思考一下什么情况应该把接口放入调用方,什么时候可以把接口归为实现方。

先来看看接口属于实现方的情况,这个很容易理解,实现方提供了接口和实现,我们可以引用接口来达到调用某实现类的功能,这就是我们经常说的api,它具有以下特征:

  1. 概念上更接近实现方
  2. 组织上位于实现方所在的包中
  3. 实现和接口在一个包中

当接口属于调用方时,我们就将其称为spi,全称为:service provider interface,spi的规则如下:

  1. 概念上更依赖调用方
  2. 组织上位于调用方所在的包中
  3. 实现位于独立的包中(也可认为在提供方中)

如下图所示:

总结一下SPI的思想:在系统的各个模块中,往往有不同的实现方案,例如日志模块的方案、xml解析的方案等,为了在装载模块的时候不具体指明实现类,我们需要一种服务发现机制,java spi就提供这样一种机制。有点类似于IoC的思想,将服务装配的控制权移到程序之外,在模块化设计时尤其重要。

二、Java SPI使用规范

  1. 定义服务的通用接口,针对通用的服务接口,提供具体的实现类。
  2. 在jar包的META-INF/services/目录中,新建一个文件,文件名为 接口的"全限定名"。 文件内容为该接口的具体实现类的"全限定名"。
  3. 将spi所在jar放在主程序的classpath中
  4. 服务调用方用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实现类,来执行具体业务逻辑

posted @ 2022-09-06 10:37  泉水姐姐。  阅读(1607)  评论(0编辑  收藏  举报