java中spi有什么作用,spi实现原理

  java中spi有什么作用,spi实现原理

  在面向对象编程中,模块之间的交互是通过接口来编程的。通常调用者不需要知道被调用者的内部实现细节,因为一旦涉及到具体的实现,如果需要另一个实现就需要修改代码,这就违背了编程的‘开闭原则’。所以我们一般有两个选择:一个是使用API(应用编程接口),一个是SPI(服务提供者接口)。API通常由应用程序开发人员使用,而SPI通常由框架扩展人员使用。

  在进入下面的学习之前,我们先加深一下对API和SPI的印象:

  API:实现者制定接口标准,完成接口的不同实现。这种模式的服务接口在概念上更接近于实现者;

  SPI:调用方制定接口标准,实现方为接口提供不同的实现;从前半句可以看出,SPI其实是一种‘为接口找实现’的服务发现机制;在这种模式下,服务接口组织在调用者所在的包中,实现位于单独的包中。

  和SPI:

  看了上面的简单示意图,相信你对API和SPI的区别有了大致的了解。现在我们使用SPI机制来实现一个简单的日志框架:

  第一步是创建一个名为spi-interface的maven项目,并定义一个spi外部服务接口,稍后将提供给调用者。

  包cn.com.wwh;

  /**

  *

  * @文件名Logger.java

  * @版本:1.0

  * @Description:服务提供商接口

  * @作者:wwh

  * @日期:2022年9月19日上午10点31分53秒

  */

  公共接口记录器{

  /**

  *

  * @描述:(功能描述)

  * @param msg

  */

  公共void信息(字符串消息);

  /**

  *

  * @描述:(功能描述)

  * @param msg

  */

  public void debug(字符串消息);

  }包cn.com.wwh;

  导入Java . util . ArrayList;

  导入Java . util . iterator;

  导入Java . util . list;

  导入Java . util . service loader;

  /**

  *

  * @文件名LoggerService.java

  * @版本:1.0

  * @Description:为服务调用方提供特定的功能是SPI的核心功能。

  * @作者:wwh

  * @日期:2022年9月19日上午10:33:30

  */

  公共类记录器服务{

  private static final LoggerService INSTANCE=new LoggerService();

  私有最终记录器记录器;

  private final List loggers=new ArrayList();

  私有LoggerService(){

  //ServiceLoader是实现SPI的核心类。

  service loader Logger sl=service loader . load(Logger . class);

  迭代器记录器it=sl . iterator();

  while (it.hasNext()) {

  loggers . add(it . next());

  }

  如果(!loggers.isEmpty()) {

  logger=loggers . get(0);

  }否则{

  logger=null

  }

  }

  /**

  * @描述:(功能描述)

  * @返回

  */

  公共静态LoggerService getLoggerService(){

  返回实例;

  }

  /**

  *

  * @描述:(功能描述)

  * @param msg

  */

  公共无效信息(字符串消息){

  if (logger==null) {

  System.err.println(在info方法中找不到Logger的实现类.);

  }否则{

  logger . info(msg);

  }

  }

  /**

  *

  * @描述:(功能描述)

  * @param msg

  */

  公共void调试(字符串消息){

  if (logger==null) {

  System.err.println(在调试方法中找不到记录器的实现类.);

  }否则{

  logger . info(msg);

  }

  }

  }把上面的项目做成spi-interface.jar包。

  第二步。创建一个新的maven项目,并导入在步骤1中键入的spi-interface.jar包。这个项目用于提供服务的实现,定义一个类,实现步骤1中定义的cn.com.wwh.Logger接口。示例代码如下:

  包cn.com.wwh;

  导入cn . com . pep . logger;

  /**

  *

  * @文件名Logback.java

  * @版本:1.0

  * @Description:服务接口实现类

  * @作者:wwh

  * @日期:2022年9月19日上午10点50分31秒

  */

  公共类回退实现记录器{

  @覆盖

  公共void调试(字符串消息){

  System.err.println(调用Logback的调试方法,输出日志为: msg );

  }

  @覆盖

  公共无效信息(字符串消息){

  System.err.println(调用Logback的info方法,输出日志为: msg );

  }

  }同时,在当前项目的类路径下建立META-INF/services/folder(至于为什么要这样建立目录,后面会解释),新建一个名为cn.com.wwh.Logger的文件,内容为cn.com.wwh.Logback这一步是关键(具体功能后面会详细解释)。然后,将上面第二步中的这个项目制作成spi-provider.jar包,供以后使用。我目前的开发工具是Eclipse,目录结构如下图所示:

  第三步:编写一个测试类,创建一个新的maven项目,将其命名为spi-test,导入前两步中制作的两个jar包spi-interface.jar和spi-provider.jar,编写测试代码。例子如下:

  包cn.com.wwh;

  导入cn . com . pep . loggerservice;

  /**

  *

  * @文件名SpiTest.java

  * @版本:1.0

  * @描述:

  * @作者:wwh

  * @日期:2022年9月19日上午10点56分31秒

  */

  公共类spite {

  公共静态void main(String[] args){

  logger service logger=logger service . getloggerservice();

  Logger.info(我是中国人);

  Logger.debug(白菜多少钱一斤);

  }

  }通过SPI,我们可以轻松地将服务与服务提供者分离。如果未来某一天我们需要将日志保存到数据库或者通过网络发送,只需要为服务接口替换实现类,其他什么都不需要修改,更符合编程中的“开闭原则”。

  spi的一般原理是:应用启动时,扫描classpath下的所有jar包,将jar包下的/META-INF/services/目录下的文件加载到内存中,进行一系列的分析(文件名是spi接口的全路径名,文件内容应该是SPI接口实现类的全路径名,可以用多个实现类保存,然后判断当前类和当前接口是否是同一类型)。如果结果为真,则通过反射生成指定类的实例对象,保存在映射集中,可以通过遍历或迭代取出。

  SPI的本质是一个加载服务实现的工具。核心类是ServiceLoader。事实上,知道了SPI的原理,我们在JDK探索源代码并不那么费力。让我们开始源代码分析。

  loader类是在java.util包下定义的。最终定义用来禁止子类的继承和修改,实现Iterable接口,通过迭代或遍历得到SPI接口的不同实现。

  从上面的例子我们知道SPI的入口是service loader . load(class s service)方法。让我们看看它做了什么。

  一般来说,以上四个步骤是用指定的类型和绑定到当前线程的classLoader实例化一个LazyIterator对象,赋给引用lookupIterator,清除原来providers列表中缓存的服务的实现。接下来,我们调用ServiceLoader实例的iterator()方法,用下面的代码获得一个迭代器:

  1公共迭代器S iterator(){

  2 //迭代器由匿名内部类提供。

  3返回新的迭代器S () {

  4 //获取缓存服务实现者的迭代器

  5迭代器映射。条目字符串,S known providers=providers . entry set()。迭代器();

  六

  7 //确定迭代器中是否还有其他元素

  8 public boolean hasNext(){

  9 //缓存的服务实现者的迭代器中没有元素。

  10 if (knownProviders.hasNext())

  11返回true

  12返回lookupiterator . has next();//确定延迟加载的迭代器中是否有元素。

  13 }

  14

  15 //获取迭代中的下一个元素

  16 public S next(){

  17 if (knownProviders.hasNext())

  18返回knownProviders.next()。getValue();

  19返回lookupiterator . next();//获取迭代器中延迟加载的下一个元素

  20 }

  21

  22公共void remove(){

  23抛出新的UnsupportedOperationException();

  24 }

  25 };

  20}然后我们调用上一步中获得的迭代器的hasNext()方法。因为我们实际上在ServiceLoader.load()的过程中清除了providers列表中的缓存服务实现,所以我们实际上调用了lookupIterator.hasNext()方法,如下所示:

  1个公共布尔hasNext(){

  2 if (nextName!=null) {//下一个元素存在

  3返回true

  4 }

  5(configs==null){//配置文件为空。

  6尝试{

  7 String fullName=前缀service . getname();//获取配置文件路径

  8 if (loader==null)

  9 configs=class loader . get system resources(全名);

  10其他

  11 configs=loader.getResources(全名);//加载配置文件

  12 } catch (IOException x) {

  13失败(服务,“定位配置文件时出错”,x);

  14 }

  15 }

  16 //遍历配置文件的内容

  17 while ((pending==null) !pending.hasNext()) {

  18如果(!configs.hasMoreElements()) {

  19返回假;

  20 }

  21 pending=parse(service,configs . nextelement());//配置文件的内容解析

  22 }

  23 next name=pending . next();//获取服务实现类的完整路径名

  24返回true

  25 }

  26如果上面的判断为真,那么我们调用迭代器it的next()方法。同样,我们也调用lookupIterator.next()方法。源代码如下:

  1 public S next(){

  2如果(!hasNext()) {

  3抛出新的NoSuchElementException();

  4 }

  5字符串cn=nextName//文件中保存的服务接口实现类的完整路径名

  6 nextName=null

  7类?c=空;

  8尝试{

  9 //获取完全限定名的类对象

  10 c=Class.forName(cn,false,loader);

  11 } catch(ClassNotFoundException x){

  12失败(找不到服务、“提供商”cn);

  13 }

  14 //判断实现类和服务接口是否属于同一类型。

  15如果(!service.isAssignableFrom(c)) {

  16失败(服务,“提供者”cn不是子类型);

  17 }

  18试{

  9//通过反射生成服务接口的实现类,判断这个实例是否是接口的实现。

  20s p=service . cast(c . new instance());

  1//缓存服务接口的实现并返回

  22 providers.put(cn,p);

  23返回p;

  24 } catch(可投掷x) {

  25失败(服务,提供程序 cn 无法实例化,x);

  26 }

  27 throw new Error();//这不可能发生

  28}其实spi实现的主要过程是:扫描类路径下所有jar包下的/META-INF/services/目录(也就是我们需要将服务接口的具体实现类暴露给这个目录。我们之前提到过需要在实现类的类路径下建立一个/META-INF/services/文件夹,这就是原因。),找到对应的文件,读取文件名找到对应的SPI接口,然后通过InputStream流读出文件内容,得到实现类的全路径名,并得到这个全路径名所代表的类对象,判断是否与服务接口是同一类型,然后通过反射生成服务接口的实现,保存在providers列表中备用。

  SPI的这种设计方法为我们的应用扩展提供了极大的便利,但其缺点也是显而易见的。Java SPI遍历SPI的配置文件,并在查找扩展实现类时实例化所有实现类。假设实现类的初始化过程是消耗资源和时间的,但是你不能在你的代码中使用它,这导致了资源的浪费。所以Java SPI不能按需加载实现类。

  另外,SPI机制在很多框架中都有应用:slf4j log框架和Spring框架的基本原理都是类似的反映。还有Dubbo框架也提供了同样的SPI扩展机制,但是Dubbo和spring框架中SPI机制的具体实现和我们今天所学的略有不同(**Dubbo可以实现按需加载实现类* *),但是总体原理是一样的。今天先简单了解一下SPI,相信以今天的基础也不难理解剩下的。

  版权归作者所有:原创作品来自博主小二上九8,转载请联系作者取得转载授权,否则将追究法律责任。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: