Java SPI机制

57 阅读3分钟

什么是SPI机制

SPI的全称是Service Provider Interface, 是JDK内置的一种服务提供发现机制,是一套用来实现第三方或者扩展第三方的API。SPI机制的实现是通过接口+配置文件+策略模式的动态加载机制。

SPI机制的约束

当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。

SPI机制的使用场景

  • JDBC加载不同类型的数据库驱动
  • 日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
  • Spring中大量使用了SPI,
  • 对servlet3.0规范
  • 对ServletContainerInitializer的实现
  • 自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
  • Dubbo里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现

SPI示例代码

文件结构如下所示:

代码如下所示:

接口和实现类的定义

// 接口定义
public interface ILogService {
    void warn (String msg);
}

//第一个实现类
public class ConsoleLogServiceImpl  implements ILogService {
    @Override
    public void warn(String msg) {
        System.out.println("Console log:" + msg);
    }
}

//第二个实现类
public class FileLogServiceImpl implements ILogService {
    @Override
    public void warn(String msg) {
        System.out.println("File log:" + msg);
    }
}

META-INF文件的名字就是接口的全限定名。文件的内容就是接口的实现类的全限定名。

com.bobo.spi.test.service.impl.ConsoleLogServiceImpl
com.bobo.spi.test.service.impl.FileLogServiceImpl

测试类如下所示:

public class TestMain {
    private static ServiceLoader<ILogService> services = ServiceLoader.load(ILogService.class);
    public static void main(String[] args) {
        for (ILogService service : services) {
            service.warn("Hello SPI !");
        }
    }
}

SPI源码分析

从测试类中我们可以看出加载接口的实现类的时候使用了ServiceLoader.load(ILogService.class);通过一步一步的点进去我们发现了其实是一个构造函数

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}


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

在reload方法中创建了一个LazyIterator对象。

当我们调用迭代器的时候其实是调用了nextService方法。

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}


通过debug我们可以发现hasNextService方法。首先通过全路径 String fullName = PREFIX + service.getName();获取我们定义在META-INF.services中定义的文件名

然后通过 pending = parse(service, configs.nextElement());方法获取了文件中的内容

在nextService方法中获取了实现类的全路径名字,通过类加载器 c = Class.forName(cn, false, loader);创建了一个实现类的对象。 

这个就是SPI机制的实现原理。