• 1

  • 1

[Java]之 SPI [七日打卡]

3天前

一、概述

SPIService Provider Interface)主要是被框架开发人员使用的一种技术。

JDK 中的 SPI 是面向接口编程的,服务规则提供者会在 JRE 的核心 API 里提供服务访问接口。

开发者实现对应的接口,并配置。


(1)实现步骤:

  1. 规范制定者定义接口
  2. 开发者实现接口,并在 META-INF/services 文件夹下建立文件

例如,使用 Java 语言访问数据库时会使用到 java.sql.Driver 接口,不同数据库产品底层的协议不同,提供的 java.sql.Driver 实现也不同,在开发 java.sql.Driver 接口时,开发人员并不清楚用户最终会使用哪个数据库,在这种情况下就可以使用 Java SPI 机制在实际运行过程中,为 java.sql.Driver 接口寻找具体的实现。

例如,如果:

  1. 规范制定者在 rt.jar 包里定义了数据库的驱动接口 java.sql.Driver
  2. 那么, MySQL 实现的开发商则会在 MySQL 的驱动包的 META-INF/services 文件夹下建立名称为 java.sql.Driver 的文件,文件内容就是 MySQLjava.sql.Driver 接口的实现类。

如图:

文件内容:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
复制代码

(2)JDK SPI 自定义实现

当服务的提供者提供了一种接口的实现之后,需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,此文件记录了该 jar 包提供的服务接口的具体实现类。 当某个应用引入了该 jar 包且需要使用该服务时,JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名,进行实现类的加载和实例化,最终使用该实现类完成业务功能。

public interface Log { 

    void log(String info); 

} 
复制代码
public class Logback implements Log { 

    @Override 
    public void log(String info) { 

        System.out.println("Logback:" + info); 
    } 
} 

public class Log4j implements Log { 

    @Override 
    public void log(String info) { 

        System.out.println("Log4j:" + info); 
    } 
} 
复制代码

在项目的 resources/META-INF/services 目录下添加一个名为 com.donaldy.spi.Log 的文件,这是 JDK SPI 需要读取的配置文件,具体内容如下:

com.donaldy.spi.Log4j 
com.donaldy.spi.Logback 
复制代码

样例图:

最后创建 main() 方法,其中会加载上述配置文件,创建全部 Log 接口实现的实例,并执行其 log() 方法,如下所示:

public class Main { 

    public static void main(String[] args) { 

        ServiceLoader<Log> serviceLoader = ServiceLoader.load(Log.class); 
        for (Log log : serviceLoader) {

            log.log("JDK SPI");
        }
    } 
} 

// 输出如下: 
// Log4j:JDK SPI 
// Logback:JDK SPI 
复制代码

二、SPI 的实现原理

重要知识点:

  • Java 核心 API(比如 rt.jar包)是使用 Bootstrap ClassLoader 类加载器加载的。

  • 而用户提供的 Jar 包是由 AppClassLoader 加载的。

如果一个类由类加载器加载,那么这个类依赖的类也是相同的类加载器加载的。

用来搜索开发商提供的 SPI 扩展实现类的 API 类(ServiceLoader)是使用 Bootstrap ClassLoader 加载的,那么 ServiceLoader 里面依赖的类应该也是由 Bootstrap ClassLoader 加载的。

而用户提供的 Jar 包是由 AppClassLoader 加载的,所以这就需要一种违反双亲委派模型的方法,线程上下文类加载器 ContextClassLoader 就是用来解决这个问题的。

测试代码,查看具体是如何实现的:

import java.sql.Driver;
import java.util.Iterator;
import java.util.ServiceLoader;

public class TestSpi {

    public static void main(String[] args) {

        ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);

        for (Driver driver : loader) {

            System.out.println("driver: " + driver.getClass() + ", loader: "
                    + driver.getClass().getClassLoader());

            System.out.println("current thread contextLoader: "
                    + Thread.currentThread().getContextClassLoader());

            System.out.println("ServiceLoader loader: " + ServiceLoader.class.getClassLoader());
        }
    }
}
复制代码

引入 MySQL 驱动的 jar 包,执行结果如下:

driver: class com.mysql.jdbc.Driver, loader: sun.misc.Launcher$AppClassLoader@18b4aac2
current thread contextLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
ServiceLoader loader: null

driver: class com.mysql.fabric.jdbc.FabricMySQLDriver, loader: sun.misc.Launcher$AppClassLoader@18b4aac2
current thread contextLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
ServiceLoader loader: null
复制代码

从执行结果可以知道 ServiceLoader 的加载器为 Bootstarp , 因为这里输出了 null,并且该类在 rt.jar 里面。

ServiceLoaderload() 源码:

    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 获取当前线程上下文加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
复制代码

这里获取了当前线程上下文加载器,指 AppClassLoader

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

java

1

相关文章推荐

未登录头像

暂无评论