从源码分析:Java中的SPI是怎样工作的

spi介绍

提到api,大家或多或少地都接触或者使用过,但是如果说到spi呢,可能了解的人就要少一些。

Java SPI的全称为Service Provider Interface,相对于api来讲的话,简单来说,api是提供给用户来进行使用的,而spi是提供给开发者来进行扩展的。也就是说,当我们使用spi的时候,从最基本来说,是基于接口的编程的方式,通过配置文件来实现动态加载,而在编写客户端程序时,可以使用基于策略模式的方式来编写代码,从而使得具体的方法基于同一接口不同的实现类来实现。

也就是说,spi就是为了实现这种解耦的一种服务发现的机制。

spi的使用

在前一篇的博客中有写过一个使用spi的demo:Java中的SPI的使用例子

根据这篇博客,就可以创建出一个完整的使用spi的例子,在客户端中调用同一接口的不同实现。

我们需要注意,当我们在使用SPI时,有如下约定:

  1. 配置文件为在META-INF/services/目录中创建的以接口的全限定名命名的文件,文件内容为需要调用的Api具体实现类的全限定名
  2. 调用时,需要使用java.util.ServiceLoder类动态加载配置文件中所声明的类
  3. 如果SPI的实现类为Jar,则需要放在主程序classPath中
  4. SPI的具体实现类必须有一个不带参数的构造方法

从源码对Java SPI进行分析

首先我们看一下我们在上一节所提到的例子:Java中的SPI的使用例子中的客户端代码:

// testClient.java
import com.lf.API.MyPrinterAPI;
import java.util.ServiceLoader;

public class testClient {
    public static void main(String[] args) {
        ServiceLoader<MyPrinterAPI> printers = ServiceLoader.load(MyPrinterAPI.class);
        for (MyPrinterAPI printer : printers) {
            printer.sayHello("SPI");
        }
    }
}

可以看出,其中调用的关键的一行代码为:

ServiceLoader<MyPrinterAPI> printers = ServiceLoader.load(MyPrinterAPI.class);

在上一节中,提到了在Java中使用SPI的约定,其中第三条所提到的java.util.ServiceLoder就是这里所用的ServiceLoader了,因此,让我们来一起看一看这个类和它的load方法。

首先是ServiceLoader的类的代码:

package java.util;

public final class ServiceLoader<S>
    implements Iterable<S>
{

    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;

    // 缓存的providers,以实例化的顺序保存(若实例化,则存入)
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;

    ... ...

}

首先,我们可以看到这个ServiceLoader类的声明中是有final的,说明这个类是不可继承的,同时,这个类实现了Iterable接口,因此我们可以用迭代器来方便地遍历所需接口的在配置文件中所声明的所有实现类。

紧接着后面是声明的类中的一些成员变量,其中第一个private static final String PREFIX = "META-INF/services/";,这也就是约定中第一条所提到的配置文件需要在META-INF路径的原因了。

接着来看一下testClient中所调用的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) {
    // 确保svc不为空,并将传入的类名存入到成员变量service中
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 判断传入的加载器是否为空,若为空,则使用ClassLoader.getSystemClassLoader()
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}
public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}
public void reload() {
    // providers为一LinkedHashMap<String,S>类型的成员变量,这里将其清空
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

到这里为止,以上的方法都是ServiceLoader类的方法,而这里reload()方法中new了一个LazyIterator,这个类其实也是ServiceLoader类的内部类。

我们来看一下内部类LazyIterator的完整的代码:

// ServiceLoader.java
private class LazyIterator
    implements Iterator<S>
{

    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }

    private boolean hasNextService() {
        // 如果已经写入过下一个实现类的类名,说明存在下一个实现类
        // 且可以保证不会在读取nextName之前,重复写nextName,防止空过某个类
        if (nextName != null) {
            return true;
        }
        // 若configs为null,则初始化configs
        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);
            }
        }
        // 若pending为null,则对pending初始化
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            // 逐行将配置中的实现的类名读出,存入ArrayList<String> names中,并返回names的迭代器
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }

    private S nextService() {
        // 判断是否有下一个实现类,并确保下一个实现类的类名被写到nextName
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            // initialize = false 不必须初始化,这样在加载类时并不会立即运行静态区块,而会在使用类建立对象时才运行静态区块。
            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());
            // 将当前的对象放入外部类的map型成员变量providers中
            providers.put(cn, p);
            // 并返回当前的对象
            return p;
        } catch (Throwable x) {
            fail(service,
                    "Provider " + cn + " could not be instantiated",
                    x);
        }
        throw new Error();          // This cannot happen
    }

    public boolean hasNext() {
        if (acc == null) {
            return hasNextService();
        } else {
            PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                public Boolean run() { return hasNextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }

    public S next() {
        if (acc == null) {
            return nextService();
        } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                public S run() { return nextService(); }
            };
            // AccessController.doPrivileged意思是这个是特别的,不用做权限检查
            return AccessController.doPrivileged(action, acc);
        }
    }

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

}

从代码中我们可以明显地看出来,这是一个实现了Iterator接口的内部类,其中定义了两个方法hasNextService()nextService()来表示是否有下一个实现类与得到下一个实现类,而为了实现接口Iterator,需要实现方法hasNext()next(),于是我们可以看到,这两个方法的实现其实都比较简单,其实仅仅是先判断acc是否为null,若不为null,则直接调用上面所述的两个方法并返回。

看了这么多这个内部类,现在我们把思路再理清一下,我们是在ServiceLoader中的reload()方法中实例化了这个内部类,传入了外部传入的serviceloader,即我们自己定义的API的类与load方法中get的线程类加载器,在实例化这个内部类(即一个迭代器)后,将这个迭代器的实例赋给ServiceLoader类的实例成员lookupIterator,而后,ServiceLoader的遍历便依靠这个迭代器与成员变量providers来实现。

实现的具体方法为:

// ServiceLoader.java
public Iterator<S> iterator() {
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }

        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

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

    };
}

可以看到,ServiceLoader的迭代器,会先实例化providers.entrySet()的迭代器作为基本的迭代器,之后遍历这个迭代器的方式为:先遍历providers,遍历完providers之后,再看’lookupIterator’中,是否还有其它的实现类,这个lookupIterator便是我们前面所看过的内部类LazyIterator

至此,我们便把JDK提供的SPI方式遍历并调用服务的所用的代码都过了一边,大家在自己的IDE看这样的代码时要多利用IDE提供的代码追踪、依赖顺序等功能,并多多利用DeBug的方式来分析代码究竟是如何执行的。希望这篇文章能够让大家觉得有所收获。

posted @ 2019-07-15 09:07  点点爱梦  阅读(214)  评论(0编辑  收藏  举报