Java SPI 简介

简介

SPI全称为Service Provider Interface。是一种为项目留下扩展点的非常好的方式。我们事先在项目中留下Service的接口。通过配置文件就可以控制生成哪些具体的Service实现类。

一个例子

我们创建一个maven项目。创建一个Service接口:

public interface MyService {
    void service();
}

还有两个MyService的实现类:

public class MyServiceA implements MyService {
    @Override
    public void service() {
        System.out.println("MyService A");
    }
}

public class MyServiceB implements MyService {
    @Override
    public void service() {
        System.out.println("MyService B");
    }
}

根据SPI约定,决定如何生成实现类的配置文件位于classpathMETA-INF/services目录。配置文件的名称为Service接口的全名,包含包名和接口名称。例如包名为com.paul,那么配置文件名为com.paul.MyService。文件的内容只需要将需要创建出的实现类全名列出。比如文件内容为:

com.paul.MyServiceA
com.paul.MyServiceB

程序会创建出MyService两个实现类:MyServiceAMyServiceB

接下来是加载实现类的方法,JDK提供的写法非常的简洁:

public class MyServiceLoader {
    public static void main(String[] args) {
        ServiceLoader myServices = ServiceLoader.load(MyService.class);
        for (MyService myService : myServices) {
            myService.service();
        }
    }
}

通过ServiceLoaderload方法,创建出一个ServiceLoader对象。迭代这个对象就能够获取所有的实现类。

上面的例子中服务接口和实现类在同一个项目中。实际上不同服务的实现类可以在不同的maven子项目中,配合与之对应的配置文件,打成jar包发布。项目部署的时候,在classpath放入不同的jar包,服务调用端不需要修改任何代码,就能够更改服务的行为,从而实现应用功能的扩展,非常的方便。

源代码分析

想明白SPI的实现方式,阅读源代码是免不了的。

我们从上面例子的ServiceLoader.load(MyService.class)调用开始分析源代码。load方法代码如下:

public static  ServiceLoader load(Class service) {
    // 获取当前线程的context classloader
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    // 调用另一个重载方法,传入classloader
    return ServiceLoader.load(service, cl);
}

public static  ServiceLoader load(Class service,
                                        ClassLoader loader)
{
    // 新建一个ServiceLoader
    return new ServiceLoader<>(service, loader);
}

继续查看ServiceLoader的构造方法:

private ServiceLoader(Class svc, ClassLoader cl) {
    // 检查svc不能为null
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 如果没传入classloader,则使用系统的classloader
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    // 获取访问控制器,Java安全机制
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    // 调用reload方法
    reload();
}

reload方法内容如下所示。其中providers变量为一个map,保存了服务具体实现类的名称(包含包名)和实现类这个对象本身。这是ServiceLoader的一种缓冲机制,同时ServiceLoader使用了懒加载,实现类只有第一次迭代ServiceLoader的时候才会创建出,加入到providers缓存中再返回。以后无论再迭代多少次,都是从providers缓存中获取。

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

LazyIterator为懒加载迭代器。这里先不介绍,到调用到它的时候再分析。

ServiceLoader创建完毕后我们开始迭代它了。创建迭代器的逻辑在ServiceLoaderiterator方法:

public Iterator iterator() {
    return new Iterator() {

        // 创建一个providers缓存的key值迭代器
        Iterator> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {
            // 先迭代已缓存的实现类,如果没有,则迭代懒加载迭代器
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }

        public S next() {
            // 同样,如果providers保存的有实现类,优先返回
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            // 否则去懒加载迭代器查找下一个实现类
            return lookupIterator.next();
        }

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

    };
}

到这里我们需要继续分析LazyIteratornext方法了。

public S next() {
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction action = new PrivilegedAction() {
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

先忽略AccessController这块儿,next方法调用的是nextService方法,继续查看。

private S nextService() {
    // 如果没有下一个service,抛出异常
    // 这个方法很重要,稍后分析
    if (!hasNextService())
        throw new NoSuchElementException();
    // nextName变量保存的是下一个要迭代的服务实现类全名
    String cn = nextName;
    nextName = null;
    Class c = null;
    try {
        // 使用反射创建出这个实现类
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    // 如果实现类c不是Service类型,报错退出
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        // 将c转换为Service类型
        S p = service.cast(c.newInstance());
        // 将c放入providers缓存
        providers.put(cn, p);
        // 返回转换为Service类型的实现类
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

hasNextService方法如下所示。

private boolean hasNextService() {
    // 如果有下一个要迭代的实现类名,返回true
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // PREFIX为META-INF/services/,正是配置文件的位置
            // 这里在拼接配置文件路径
            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是获取解析过的配置文件内容的迭代器
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            // 如果pending和configs都没有下一个元素,返回false
            return false;
        }
        // 解析配置文件,返回迭代器
        pending = parse(service, configs.nextElement());
    }
    // 将配置文件中待解析的一行配置放入nextName变量中。
    nextName = pending.next();
    return true;
}

到这里ServiceLoader的实现原理我们已经清楚了。使用的仍然是Java 反射方式,从约定好位置的配置文件中读取各个实现类的名称,通过Class.forname方式创建出实例并返回。

你可能感兴趣的:(Java SPI 简介)