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

JDK SPI 机制

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

下面我们通过一个简单的示例演示下 JDK SPI 的基本使用方式:

截屏20200925 20.41.24.png

首先我们需要创建一个 Log 接口,来模拟日志打印的功能:

public interface MyLog {
    /**
     * 打印INFO 日志
     * @param info /
     */
    void info(String info);

    /**
     * 打印DEBUG 日志
     * @param debug /
     */
    void debug(String debug);
}

接下来提供两个实现Logback和Log4j,分别代表两个不同日志框架实现:

public class Logback implements MyLog {
    @Override
    public void info(String info) {
        System.out.println("logback输出INFO日志为:"+info);
    }

    @Override
    public void debug(String debug) {
        System.out.println("logback输出DEBUG日志为:"+debug);

    }
}
----------------------------------------------------------
public class Log4j implements MyLog {
    @Override
    public void info(String info) {
        System.out.println("Log4j输出INFO日志为:"+info);
    }

    @Override
    public void debug(String debug) {
        System.out.println("Log4j输出DEBUG日志为:"+debug);
    }
}

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

top.pippen.impl.Log4j
top.pippen.impl.Logback

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

public class LogTest {

    @Test
    public void logTest(){
        ServiceLoader<MyLog> serviceLoader =
           ServiceLoader.load(MyLog.class);
        for (MyLog log : serviceLoader) {
            log.info("JDK SPI");
        }
    }
}

-------------输出结果---------------------

Log4j输出INFO日志为:JDK SPI
logback输出INFO日志为:JDK SPI

JDK SPI 源码分析

通过上述示例,我们可以看到 JDK SPI 的入口方法是 ServiceLoader.load() 方法,接下来我们就对其具体实现进行深入分析。

在 ServiceLoader.load() 方法中,首先会获取当前使用的 ClassLoader,然后调用 reload() 方法,调用关系如下图所示:
截屏20200925 21.11.04.png

在 reload() 方法中,首先会清理 providers 缓存(LinkedHashMap 类型的集合),该缓存用来记录 ServiceLoader 创建的实现对象,其中 Key 为实现类的完整类名,Value 为实现类的对象。之后创建 LazyIterator 迭代器,用于读取 SPI 配置文件并实例化实现类对象。

ServiceLoader.reload() 方法的具体实现,如下所示:

// 缓存,用来缓存 ServiceLoader创建的实现对象
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();


public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);// 迭代器 
    }

在前面示例中,for 循环迭代底层就是调用ServiceLoader.LazyIterator实现的。
iterator 接口有两个关键方法:hasNext() 方法和 next() 方法。这里的 LazyIterator 中的next() 方法最终调用的是其 nextService() 方法,hasNext() 方法最终调用的是 hasNextService() 方法。

首先来看 LazyIterator.hasNextService() 方法,该方法主要负责查找 META-INF/services 目录下的 SPI 配置文件, 并进行遍历,大致实现如下所示:

private static final String PREFIX = "META-INF/services/"; 
Enumeration<URL> configs = null; 
Iterator<String> pending = null; 
String nextName = null; 
private boolean hasNextService() { 
    if (nextName != null) { 
        return true; 
    } 
    if (configs == null) { 
        // PREFIX前缀与服务接口的名称拼接起来,就是META-INF目录下定义的SPI配 
        // 置文件(即示例中的META-INF/services/com.xxx.Log) 
        String fullName = PREFIX + service.getName(); 
        // 加载配置文件 
        if (loader == null) 
            configs = ClassLoader.getSystemResources(fullName); 
        else 
            configs = loader.getResources(fullName); 
    } 
    // 按行SPI遍历配置文件的内容 
    while ((pending == null) || !pending.hasNext()) {  
        if (!configs.hasMoreElements()) { 
            return false; 
        } 
        // 解析配置文件 
        pending = parse(service, configs.nextElement());  
    } 
    nextName = pending.next(); // 更新 nextName字段 
    return true; 
}   

在 hasNextService() 方法中完成 SPI 配置文件的解析之后,再来看 LazyIterator.nextService() 方法,该方法负责实例化 hasNextService() 方法读取到的实现类,其中会将实例化的对象放到 providers 集合中缓存起来,核心实现如下所示:

private S nextService() { 
    String cn = nextName; 
    nextName = null; 
    // 加载 nextName字段指定的类 
    Class<?> c = Class.forName(cn, false, loader); 
    if (!service.isAssignableFrom(c)) { // 检测类型 
        fail(service, "Provider " + cn  + " not a subtype"); 
    } 
    S p = service.cast(c.newInstance()); // 创建实现类的对象 
    providers.put(cn, p); // 将实现类名称以及相应实例对象添加到缓存 
    return p; 
} 

总结

本文我们通过一个示例入手,介绍了 JDK 提供的 SPI 机制的基本使用,然后深入分析了 JDK SPI 的核心原理和底层实现,对其源码进行了深入剖析。

打赏
支付宝 微信
上一篇 下一篇