【Java】SPI在Java中的实现与应用-CSDN博客

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

一、SPI的概念

1.1、什么是API

API在我们日常开发工作中是比较直观可以看到的比如在 Spring 项目中我们通常习惯在写 service 层代码前添加一个接口层对于 service 的调用一般也都是基于接口操作通过依赖注入可以使用接口实现类的实例。

如上图所示服务调用方无需关心接口的定义与实现只进行调用即可接口、实现类都是由服务提供方提供。服务提供方提供的接口与其实现方法就可称为APIAPI中所定义的接口无论是在概念上还是具体实现都更接近服务提供方实现方通常接口与实现类在同一包中。

1.2、什么是SPI

如果我们将接口的定义放在调用方服务的调用方定义一个接口规范可以由不同的服务提供者实现。并且调用方能够通过某种机制来发现服务提供方通过调用接口使用服务提供方提供的功能这就是SPI的思想。

SPI 全称 Service Provider InterfaceJava 提供的一套用来被第三方实现或者扩展的 API它可以用来启用框架扩展和替换组件。

服务提供方按接口规范实现服务服务调用方通过某种机制为这个接口寻找到这个服务 SPI的特点很明显接口的定义调用方提供与具体实现是隔离的服务提供方提供使用接口的实现类需要依赖某种服务发现机制。

通过对比我们可以看出接口在APISPI中的含义还是有很大的不同总的来说API 中的接口是更像是服务提供者给调用者的一个功能列表而 SPI 中更多强调的是服务调用者对服务实现的一种约束。

二、为什么要使用SPI

  • 面向接口编程 面向对象的设计与编程中我们经常强调“依赖抽象而不是具体”这样做就是为了实现高内聚、低耦合提供代码灵活性和可维护性等等。

  • 提供标准标准但没有具体实现的业务场景 SPI 机制的使用场景就是没有统一实现标准的业务场景。一般就是服务调用方有定义好的标准接口但是没有统一的实现需要服务提供方提供其具体实现。

  • 解耦 SPI 机制优势就是低耦合。将接口的定义以及具体实现分离可以实现运行时根据业务实际场景启用或者替换具体实现类。

三、Java中如何使用SPI

接口定义、服务实现这些我们都轻车熟路调用方直接依赖接口不依赖具体实现这是依赖倒置原则我们在Spring项目中使用API时会使用Spring的依赖注入DI来实现“服务发现”同样地SPI的重点也是如何让调用方发现接口的具体实现也就是上文提到的某种服务发现机制。

SPI的服务发现机制是由ServiceLoader提供ServiceLoader是Java在JDK 6中引进的新特性它主要是用来发现并加载一系列的service provider。当服务的提供者提供了服务接口的一种实现之后只需要在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件该文件的内容就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名并加载实现类完成依赖的注入这就是Java SPI的服务发现机制。

下面就结合一个示例来具体讲讲。若有这样一个需求需要使用一个接口来完成内容查找服务接口的具体实现交给其他服务提供方实现可能是基于文件系统的查找也可能是基于数据库的查找。

3.1、定义接口

提供一个查找服务标准接口先定义调用方的内容查找方法

// 查找服务接口
public interface Search {
    // 按关键字查询内容方法
     String searchDoc(String keyword);
}

这个接口就是给服务提供方来实现的将它打包发布mvn clean install确保maven仓库中有该jar包之后提供者在项目中就可以引入这个 jar 包了。

3.2、服务实现

制定并发布完标准接口后我们假设第一个服务提供方提供了一种文件查找的实现。新建项目search-file并引入刚才发布的标准接口jar包

<dependency>
    <groupId>com.blblccc.search</groupId>
    <artifactId>search-standard</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

实现定义好的接口

public class FileSearch implements Search {

    @Override
    public String searchDoc(String keyword) {
        return "文件查找" + keyword;
    }
}

并在项目的resources的目录下创建META-INF/services目录然后以前面定义的接口名com.blblccc.spi.learn.Search创建文件并在文件中写入实现类的全限定名。

com.blblccc.file.search.FileSearch

一个服务方的简单实现就完成了用maven打成 jar 包发布到maven之后就可以提供给调用方使用了。

接着按上述实现方式再创建一个项目search-database使用数据库的实现接口

public class DatabaseSearch implements Search {
    @Override
    public String searchDoc(String keyword) {
        return "数据库查找" + keyword;
    }
}

同样打包发布后就可以提供给调用方使用了。

3.1、服务发现

接下来关键的一步就是服务发现服务发现需要依赖ServiceLoader的使用。创建一个新项目search-sever引入上面打好的两个提供方的 jar 包。

<dependencies>
    <dependency>
        <groupId>com.blblccc.search</groupId>
        <artifactId>search-file</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.blblccc.search</groupId>
        <artifactId>search-database</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

虽然每个服务提供者对于接口都有不同的实现但是作为调用者来说它并不需要关心具体的实现类我们要做的是通过接口来调用服务提供者实现的方法。

下面就是关键的服务发现环节使用ServiceLoader来加载具体的实现类调用方只需调用对应接口方法即可。

public class SearchDoc {

    public static void main(String[] args) {
        new SearchDoc().searchDocByKeyWord("hello world");
    }

    public void searchDocByKeyWord(String keyWord) {

        ServiceLoader<Search> searchServiceLoader = ServiceLoader.load(Search.class);

        for (Search search : searchServiceLoader){
            String doc = search.searchDoc(keyWord);
            System.out.println(doc);
        }
    }
}

测试结果

文件查找hello world
数据库查找hello world

可以看到通过定义的Search发现了两个实现类。整段代码中没有出现过具体的服务实现类操作都是通过接口调用。

四、SPI实现原理

4.1、Java的类加载器

首先Java的类加载器可被分为四类

启动类加载器Bootstrap ClassLoader

  • 用于加载Java的核心类由底层的 C++ 实现。启动类加载器不属于 Java 类库无法被 Java 程序直接引用。

  • Bootstrap ClassLoader 的 parent 属性为 null

标准扩展类加载器 Extension ClassLoader

  • 由 sun.misc.Launcher$ExtClassLoader 实现

  • 负责加载 JAVA_HOME 下 libext 目录下的或者被 java.ext.dirs 系统变量所指定的路径中的所有类库

应用类加载器 Application ClassLoader

  • 由 sun.misc.Launcher$AppClassLoader 实现

  • 负责在 JVM 启动时加载用户类路径上的指定类库

用户自定义类加载器 User ClassLoader

  • 当上述 3 种类加载器不能满足开发需求时用户可以自定义加载器

  • 自定义类加载器时需要继承 java.lang.ClassLoader 类。如果不想打破双亲委派模型那么只需要重写 findClass 方法即可如果想打破双亲委派模型则需要重写 loadClass 方法

4.2、双亲委派机制

  • 双亲委派机制的关系链如下

Bootstrap引导类加载器 → Extension拓展类加载器 → Application系统类加载器 → User自定义类加载器

  • 双亲委派模式的好处这种自下而上的层级设计能避免类的重复加载同时防止Java的核心API类在运行时被篡改减少性能开销和安全隐患问题。

  • 双亲委派模式的弊端 无法做到不委派必须首先上浮到最顶层也无法向下委派顶层优先。

4.3、SPI为什么要打破双亲委派机制

这里有一条重要原则类加载器可见性原则

  • 子类加载器能查看父类加载器加载的所有类,但是父类加载器不能查看子类加载器所加载的类

位于rt.jar包中的SPI接口是由Bootstrap类加载器完成加载的而classpath路径下的SPI实现类则是App类加载器进行加载的。但往往在SPI接口中会经常调用实现者的代码所以一般会需要先去加载自己的实现类但实现类并不在Bootstrap类加载器的加载范围内而经过前面的双亲委派机制的分析我们已经得知子类加载器可以将类加载请求委托给父类加载器进行加载但这个过程是不可逆的。也就是父类加载器是不能将类加载请求委派给自己的子类加载器进行加载的所以此时就出现了这个问题如何加载SPI接口的实现类答案是打破双亲委派模型。

以JDBC举例来说

ServiceLoader和DriverManager类以及SPI接口类都是rt核心包的系统类它们都是启动类加载器bootstrap classloader加载的而第三方jar包是在classPath下的启动类加载器加载不了也无法委派给父加载器加载所以我们就需要破坏双亲委派机制指定一个类加载器去加载。

4.4、SPI底层实现原理

要搞清楚这个问题的原因得先确认我们使用SPI的入口

ServiceLoader<Xxxx> serviceLoader = ServiceLoader.load(Xxxx.class);

进入该方法寻找其实现的方式

注意此处获取了当前线程的类加载器而在线程中调用该类方法的是我们用户自己。那么这里就理解为获取到了用户的类加载器。

再往该方法中查找找到该段代码

注意该段代码中cl为上一步获取到的类加载器如果发现类加载器不存在会再次获取系统默认加载器这个系统默认加载器在常规情况下是用于加载启动类的加载器(jdk注释中解释)而启动类则是我们用户自己定义的类这里毋庸置疑也会是应用类加载器。

从上面的代码中我们总结出来ServiceLoader获取了我们的应用类加载器至此load方法入口基本上没有其他内容可以细看。

为减轻文章阅读压力直接跳转到该方法

java.util.ServiceLoader.LazyIterator#nextService

注意这里的loader是我们前面获取到的应用类加载器这个方法中是获取到了具体需要实例化的实现类即将对其进行实例化 在这之前需要先获取到Class这里使用Class.forName(class, false, ClassLoader)方法这个方法的含义是使用指定的类加载器去加载指定的类。既然这里的类加载器是应用类加载器那么类加载顺序自然就又回到了应用类加载器-->扩展类加载器-->BootStrap类加载器-->扩展类加载器-->应用类加载器能加载到我们想要的类也就不奇怪了。

综上Java SPI的实现是依靠ServiceLoaderServiceLoader通过使用线程上下文类加载器来加载SPI接口实现类实现类的全路径名需配置在META-INF/services/目录下以接口名命名的文件内容中ServiceLoader会读取文件中的全路径名通过反射机制实例化接口实现类。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java