浅记一下JAVA SPI 的原理与源码

178 阅读4分钟

介绍: Java SPI 全称 Java Service Provider Interface,是 Java 提供的一种服务提供者发现机制。其核心功能是通过接口找到其实现类。在实际运用中,主要用于在程序启动或运行时,通过 SPI 机制,加载并装配接口实现类,实现组件的替换和动态扩展。

使用步骤:

  1. 创建一个接口,该接口描述了您希望提供的组件的行为。
  2. 创建一个实现该接口的类,该类实现了接口中定义的行为。
  3. 创建一个名为 "META-INF/services" 的文件夹,并在该文件夹中创建一个名为“接口全名”的文件。文件中包含了实现该接口的所有类的完全限定类名(一行一个)。
  4. 将实现类打包为一个 jar 文件,并将其放在应用程序的类路径中。
  5. 在应用程序中,调用 Java 的 ServiceLoader.load 方法来加载实现了指定接口的类。该方法返回一个 Iterable 对象,您可以使用该对象遍历所有已加载的组件并使用它们。

例如,假设您创建了名为 Example 的接口和一个实现该接口的类 ExampleImpl。

我就以JDK本身自带的JDBC接口类 java.sql.Driver 为例子,先看其操作步骤,再来看一下源码。

在我们使用 MySQL 或 Oracle 数据库时,只需要引入 MySQL 驱动 jar 包或 Oracle 驱动 jar 包就可以了。它是怎么做到的呢?关键点是DriverManager

//DriverManager使用SPI加载Driver扩展实现,如com.mysql.jdbc.Driver 
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); 
Iterator<Driver> driversIterator = loadedDrivers.iterator();

这里我们来看看ServiceLoader.load 做了什么事情:


public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
{
      return new ServiceLoader<>(service, loader);
}

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);
}

在ServiceLoader的构造函数里,主要是对各个成员变量做初始化,没有复杂的逻辑。

至此ServiceLoader的初始化就完成了。

通过对ServiceLoader.load的分析发现,ServiceLoader.load其实只是做了初始化,并没有真正的加载实现类.

实现类的解析、加载和实例化

使用 Java SPI 的第二步是遍历。Java SPI 正是在遍历过程中实现的,实现类的解析、加载和实例化。

while(driversIterator.hasNext()) {
    driversIterator.next();
}

因为ServiceLoader实现了Iterable接口,所以它支持了遍历功能,而遍历功能的实现是通过复写iterator()方法实现,跟进去ServiceLoader.iterator()

//ServiceLoader.iterator()
public Iterator<S> iterator() {
    //直接实例化并返回了一个Iterator
    return new Iterator<S>() {
        //实例化遍历器时,将ServiceLoader已经实例化的实现类赋值给了成员变量knownProviders。
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
            
        //iterator.hasNext()会调用这个方法,判断是否还有实现类
        public boolean hasNext() {
            //先判断已加载的实现类中是否存在,存在的话直接返回true
            if (knownProviders.hasNext())
                return true;
            //如果不存在,则调用ServiceLoader中的lookupIterator,看是否存在。
            return lookupIterator.hasNext();
        }

        //iterator.next()会调用这个方法,获取下一个实现类
        public S next() {
            //如果已加载的实现类中存在,则返回已加载的实现类
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            //否则,调用ServiceLoader中的lookupIterator,获取下一个实现类。
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

通过以上代码分析可知:

  • ServiceLoader通过复写iterator()方法实现了遍历功能;
  • ServiceLoader的遍历器提供了一个简单缓存功能knownProviders,用于缓存已经加载并实例化的实现类;
  • ServiceLoader的遍历器非常简单,核心逻辑是通过lookupIterator.hasNext()和lookupIterator.next()实现的; 所以下面重点要分析ServiceLoader的LazyIterator lookupIterator。 先看LazyIterator的几个核心成员变量:
//LazyIterator的成员变量

//要加载的类或接口
Class<S> service;
//类加载器
ClassLoader loader;
//加载的文件路径(META-INF/services/接口或类全限定名)
Enumeration<URL> configs = null;
//从文件路径中加载到的实现类全限定名
Iterator<String> pending = null;
private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            //全路径名=META-INF/services/ + 类或接口的全限定名
            String fullName = PREFIX + service.getName();
            //加载所有的全路径名文件
            //如:META-INF/services/com.yuqiao.deeplearningdubbo.javaspi.protocol.v2.IProtocol
            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;
        }
        //解析文件中所有的实现类名
        //如:com.yuqiao.deeplearningdubbo.javaspi.protocol.v2.tcp.TcpProtocol
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

LazyIterator的hasNextService()方法,有几个关键点:

根据类或接口的全限定名加载配置文件; 解析文件中的所有实现类名; 遍历解析到的实现类名,作为下一个类名返回。 以上内容的核心是完成了实现类的解析!接下来就是实现类的加载和实例化。

我们接着看LazyIterator的nextService()方法:

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());
        //将实例化的类保存到缓存中,key=类全限定名,value=类实例
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

至此 SPI 的所有实现类已经初始化完成。