美文网首页
Spi简介、实例及加载的源码追踪(JDK9)

Spi简介、实例及加载的源码追踪(JDK9)

作者: 云中人山 | 来源:发表于2020-09-11 23:23 被阅读0次

Spi简介

此文是为了自己做理解整合使用,简书上我发现了一篇比我这个写得更加全面的文章,请移步深入理解SPI机制

笔者的Jdk版本为1.9,ServiceLoader有所不同


概念

Jdk 1.6引入了ServiceLoader,它主要是用来发现和装载一系列的service provider。

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

其包含四个部分:

  • 服务提供者接口(Service Provider Interface): 规定了服务提供者的应该提供的方法

  • 服务提供者(Service Providers):实现了服务提供者接口的具体实现类,也为真正提供服务的类

  • Spi 配置文件: 一个为了便于查找服务实现的指定文件,文件目录必须为 META-INF/services,文件名必须为和服务提供者接口的相对路径完全一致。配置文件中的每一行,为一个具体服务提供者的相对路径

  • ServiceLoader: Java Spi主类,用于为服务提供者接口加载服务。ServiceLoader中有许多实用的方法,可以用于对应的实现,包括进行迭代或者重新加载服务


Spi Vs Api

  • Api 是 类/接口/方法/... 的描述,调用的目的是为了实现某一目标
  • Spi是 类/接口/方法/... 的描述,继承和实现的目的是为了实现某一目标

换句话说, Api 告诉你指定的某个类或者方法的作用,而Spi 告诉你必须实现哪些操作才能符合要求

一般来说 Api和Spi是不同的, 举个例子,在JDBC Driver 这个类是 Spi的一种,如果只是想使用JDBC,你不需要直接使用它,但是实现JDBC驱动的每个人都必须实现该类

但是有时候,他们的概念会重合。 Connection接口既是Spi,又是Api。


示例

首先新建一个项目,项目结构如图所示

1599913238044.png
  1. Spi-base中新建一个接口
public interface SearchSpi {

    /**
     * 查询某一字段
     * @param words
     * @return
     */
    void searchWords(String words);
}
  1. Spi-es中,maven依赖 spi-base,并且实现这个接口
public class EsSearchSpi implements SearchSpi {
    @Override
    public void searchWords(String words) {
        System.out.println("This is es searching " + words);
    }
}

并且在resources目录下新建services文件夹,在文件夹下建立与 SearchSpi相对路径同名的文件

文件中的内容名为 具体实现类的相对路径

在此,文件名为com.wesley.SearchSpi ,文件中内容为com.wesley.EsSearchSpi

此处需要注意一个点,maven 中要加上以下配置,否则无法将resources中的文件打包

<resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*</include>
                </includes>
            </resource>
        </resources>

最后在spi-core中新增启动类

public class App{

    public static void main( String[] args )
    {
        ServiceLoader<SearchSpi> loader = ServiceLoader.load(SearchSpi.class);
        Iterator<SearchSpi> spiIterable = loader.iterator();
        while(spiIterable.hasNext()){
            SearchSpi searchSpi = spiIterable.next();
            searchSpi.searchWords("hello world");
        }
    }

}

启动后,日志为

Connected to the target VM, address: '127.0.0.1:54769', transport: 'socket'
This is es searching hello world
Disconnected from the target VM, address: '127.0.0.1:54769', transport: 'socket'

Process finished with exit code 0

表示Spi被成功装载并调用

这里有个坑,我直接使用 loader.iterator().hasNext()的时候,不知道为什么一直为true,导致程序一直执行,这个留个爪,看后面能不能查出来


过程分析

1 初始化ServiceLoader
ServiceLoader<SearchSpi> loader = ServiceLoader.load(SearchSpi.class);

如文章开头描述,需要SeviceLoader进行加载

其调用的方法及作用为

/**
* 用当前线程的的classLoader 生成一个新的ServiceLoader
**/
    @CallerSensitive
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
    }

ServiceLoader的构造方法中,最主要的是以下部分,初始化了ServiceLoader中的一些成员变量

private ServiceLoader(Class<?> caller, Class<S> svc, ClassLoader cl) {
        Objects.requireNonNull(svc);

        // ....省略部分代码

        this.service = svc;
        this.serviceName = svc.getName();
        this.layer = null;
        this.loader = cl;
        this.acc = (System.getSecurityManager() != null)
                ? AccessController.getContext()
                : null;
    }
  • service : 表示正在加载的服务的类或接口
  • serviceName : 服务类型
  • layer : 类加载器的提供者,初始化时为null
  • loader : 用于查找,加载和实例化提供程序的类加载器。
  • acc : 创建ServiceLoader时采取的访问控制上下文
2 获取迭代器
Iterator<SearchSpi> spiIterable = loader.iterator();

这条语句,作用为: 首先生成了一个包装了ModuleServicesLookupIteratorLazyClassPathLookupIterator组合成的iterator ,这个iterator并不是loader.iterator,而是 lookupIterator1,之后在lookupIterator1的基础上,加入一些校验后才包装成了loader.iterator

其调用方法如下,已加中文注解

public Iterator<S> iterator() {

        // 当 lookupIterator1不存在时,创建吸你的lookUpIterator
        if (lookupIterator1 == null) {
            // 此处的逻辑为 若layer!= null->LayerLookupIterator 
            // 其余为 ModuleServicesLookupIterator 和 LazyClassPathLookupIterator组合成的iterator 
            // 当前程序便是layer == null的情况
            // 为什么使用两个 暂不知道 从命名上看是读取的位置不同
            lookupIterator1 = newLookupIterator();
        }

        return new Iterator<S>() {

            // record reload count
            final int expectedReloadCount = ServiceLoader.this.reloadCount;

            // index into the cached providers list
            int index;

            /**
             * Throws ConcurrentModificationException if the list of cached
             * providers has been cleared by reload.
             */
            private void checkReloadCount() {
                if (ServiceLoader.this.reloadCount != expectedReloadCount)
                    throw new ConcurrentModificationException();
            }

            @Override
            public boolean hasNext() {
                checkReloadCount();
                if (index < instantiatedProviders.size())
                    return true;
                // 程序中进入此处  见下一段源代码
                return lookupIterator1.hasNext();
            }

            @Override
            public S next() {
                checkReloadCount();
                S next;
                if (index < instantiatedProviders.size()) {
                    next = instantiatedProviders.get(index);
                } else {
                    next = lookupIterator1.next().get();
                    instantiatedProviders.add(next);
                }
                index++;
                return next;
            }

        };
    }

lookupIterator1 = newLookupIterator();的具体实现如下

private Iterator<Provider<S>> newLookupIterator() {
        assert layer == null || loader == null;
     // 当前程序便是layer == null的情况
        if (layer != null) {
            return new LayerLookupIterator<>();
        } else {
            Iterator<Provider<S>> first = new ModuleServicesLookupIterator<>();
            Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();
            // 实际程序返回
            return new Iterator<Provider<S>>() {
                // 即分别判断 ModuleServicesLookupIterator 和 LazyClassPathLookupIterator
                @Override
                public boolean hasNext() {
                    return (first.hasNext() || second.hasNext());
                }
                @Override
                public Provider<S> next() {
                    if (first.hasNext()) {
                        return first.next();
                    } else if (second.hasNext()) {
                        return second.next();
                    } else {
                        throw new NoSuchElementException();
                    }
                }
            };
        }
    }
  1. 判断是否有下一元素(真正加载Spi)
while(spiIterable.hasNext())

首先看ServiceLoader中的 ModuleServicesLookupIterator#hasNext

        @Override
        public boolean hasNext() {
            while (nextProvider == null && nextError == null) {
                // get next provider to load
                while (!iterator.hasNext()) {
                    if (currentLoader == null) {
                        return false;
                    } else {
                        
                        currentLoader = currentLoader.getParent();
                        // 入口
                        //返回一个迭代器,获取当前类加载器的模块中的实现
                        // 在本程序中为空的迭代器
                        iterator = iteratorFor(currentLoader);
                    }
                }

                // attempt to load provider
                ServiceProvider provider = iterator.next();
                try {
                    @SuppressWarnings("unchecked")
                    Provider<T> next = (Provider<T>) loadProvider(provider);
                    nextProvider = next;
                } catch (ServiceConfigurationError e) {
                    nextError = e;
                }
            }
            return true;
        }

这里有获取到的迭代器是空的,进入下一步

java.util.ServiceLoader.LazyClassPathLookupIterator#hasNext

接下来我会直接在源代码中标明前后顺序,后续会补上时序图(也许)

@Override
        public boolean hasNext() {
            if (acc == null) {
                // 1.进入此处
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }
private boolean hasNextService() {
            while (nextProvider == null && nextError == null) {
                try {
                    // 2.获取providerClass
                    Class<?> clazz = nextProviderClass();
                    if (clazz == null)
                        return false;

                    if (clazz.getModule().isNamed()) {
                        // ignore class if in named module
                        continue;
                    }

                    if (service.isAssignableFrom(clazz)) {
                        Class<? extends S> type = (Class<? extends S>) clazz;
                        Constructor<? extends S> ctor
                            = (Constructor<? extends S>)getConstructor(clazz);
                        ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);
                        nextProvider = (ProviderImpl<T>) p;
                    } else {
                        fail(service, clazz.getName() + " not a subtype");
                    }
                } catch (ServiceConfigurationError e) {
                    nextError = e;
                }
            }
            return true;
        }
private Class<?> nextProviderClass() {
            if (configs == null) {
                try {
                    // PREFIX = /META-INF/services/   service.getName为Spi的相对路径
                    String fullName = PREFIX + service.getName();
                    if (loader == null) {
                        configs = ClassLoader.getSystemResources(fullName);
                    } else if (loader == ClassLoaders.platformClassLoader()) {
                        // The platform classloader doesn't have a class path,
                        // but the boot loader might.
                        if (BootLoader.hasClassPath()) {
                            configs = BootLoader.findResources(fullName);
                        } else {
                            configs = Collections.emptyEnumeration();
                        }
                    } else {
                        // 3 用类加载器加载资源 获取到一个2位的数组,数组第0位是Enumeration 第1位是BuiltinClassLoader` 
                        configs = loader.getResources(fullName);
                    }
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return null;
                }
                // 4 解析URLx信息
                pending = parse(configs.nextElement());
            }
            String cn = pending.next();
            try {
                return Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service, "Provider " + cn + " not found");
                return null;
            }
        }

java.lang.ClassLoader#getResources源代码如下

public Enumeration<URL> getResources(String name) throws IOException {
        Objects.requireNonNull(name);
        @SuppressWarnings("unchecked")
        Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
        if (parent != null) {
            tmp[0] = parent.getResources(name);
        } else {
            tmp[0] = BootLoader.findResources(name);
        }
        // tmp[1]里保留的是BuiltinClassLoader
        tmp[1] = findResources(name);
        
        return new CompoundEnumeration<>(tmp);
    }

BuiltinClassLoader 是 jdk9 中代替 URLClassLoader 的加载器,是 PlatformClassLoaderAppClassLoader 的父类。其继承了 SecureClassLoader,其核心的方法主要是 loadClassOrNull(...)方法,内部细节不再讨论,否则过深

java.util.ServiceLoader.LazyClassPathLookupIterator#parse的截图及运行时信息如下

1599921799976.png

至此,我们找到了Spi被加载的位置以及解析的位置

剩余细节,不再赘述


Dubbo中的Spi 实现

官方文档

来源:

Dubbo 的扩展点加载从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。

Dubbo 改进了 JDK 标准的 SPI 的以下问题:

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
  • 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
约定:

在扩展类的 jar 包内,放置扩展点配置文件 META-INF/dubbo/接口全限定名,内容为:配置名=扩展实现类全限定名,多个实现类用换行符分隔

从以上的信息,我们可以看到,Dubbo的 Spi基本上参考了Java Spi的思路,使用一个固定路径,使用一个类ExtensionLoader加载对应的Spi。

Dubbo的Spi原理及过程会另外单开一篇文章进行讲解


参考文章

Java Service Provider Interface

Java SPI and ServiceLoader

1599913238044.png

相关文章

网友评论

      本文标题:Spi简介、实例及加载的源码追踪(JDK9)

      本文链接:https://www.haomeiwen.com/subject/aojiektx.html