栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JAVA SPI机制简单介绍

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JAVA SPI机制简单介绍

什么是SPI

SpringBoot的自动装配机制中其实就是通过SPI机制去实现的,为了更深入的了解SpringBoot的自动装配机制,故需要对Java的SPI机制作一定的了解。

SPI 全称为 Service Provider Interface,是一种服务发现机制,这里所说的服务发现机制与微服务中所描述的服务发现不是同一个东西,不要将其混淆了。

SPI通过ClassPath路径下的meta-INF/services文件夹查找文件,自动装配文件里所定义的类。这一机制为很多框架提供了扩展的可能,比如在Dubbo、JDBC中都使用到了SPI机制。

SPI 机制图

SPI机制的简单示例 第一步:定义接口

首先建立一个服务用于定义公用接口,之后的服务都将对这些公用接口进行实现。

这个服务模拟的是JDK所定义的公用接口,比如java.sql包下的Driver接口,各个厂商的数据库驱动都是对这个接口的实现

这个项目什么都不用做,只需要定义接口即可。需要注意的是,该项目不需要启动类。

定义好接口后,使用Maven进行打包,即可在其他项目中引用。

用Maven install进行打包

第二步:实现接口

定义好接口并打包后,我们再新建一个新的项目,用于实现刚才定义的接口

这个项目模拟的是各个厂商所提供的驱动,例如MySQL所提供的的mysql-connector-j驱动

在新项目中,引用刚才定义接口的项目

引用完成后新建一个实现类,对刚才定义的接口进行实现

然后在Resource文件夹下创建meta-INF/services文件夹,在文件夹下创建一个文件,文件名称和内容为实现类的全路径名。如图所示

然后对其进行打包

第三步:调用测试

再新建一个项目,这个项目用于调用接口。

这个项目模拟的是平时我们所写的业务项目

在项目中引用实现的依赖,就好像平时引用MySQL驱动依赖一样,只需要引用厂商所提供的驱动即可。

通过ServiceLoader去装载实现类,并循环遍历调用接口。

简化的写法为:

运行后可以发现,其成功的输出了我们在实现类中所定义的输出内容。

SPI机制是如何发现并实例化实现类的 SPI机制发现实现类

在上面的例子中,我们是通过使用ServiceLoader去装载的实现类,这里会引申出一个问题:为什么一定要将文件放在meta-INF/services文件夹下?为什么文件的内容一定要是实现类的全路径名?

首先解答第一个问题,为什么一定要放在meta-INF/services目录下。通过查看ServiceLoader的源码即可得知,原因非常的简单:就是因为其定义了路径前缀。[狗头]

public final class ServiceLoader
    implements Iterable
{

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

    // The class or interface representing the service being loaded
    private final Class 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;

    // Cached providers, in instantiation order
    private linkedHashMap providers = new linkedHashMap<>();

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

然后是第二个问题:为什么文件的内容一定要是实现类的全路径名?谈论到全路径名,一般脑海中就会闪过一个关键的词——反射。没错,SPI就是通过全路径名反射对实现类进行实例化的。

SPI如何实例化实现类

想要知道为什么首先看源码。查看ServiceLoader.load()方法的源码:

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

第一步加载了一个类加载器,然后调用带类加载器的Service加载器,这里不在赘述,直接看源码文档即可:

    
    @CallerSensitive
    public ClassLoader getContextClassLoader() {
        if (contextClassLoader == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                   Reflection.getCallerClass());
        }
        return contextClassLoader;
    }

Service加载器中直接返回了一个ServiceLoader:

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

进一步查看该构造器的源码:

    private ServiceLoader(Class 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();
    }

首先校验Class是否存在,如果不存在则直接抛出了异常;
第二步通过一个三元去判断使用哪一个类加载器,这里涉及到Java类加载的双亲委派机制,这里不做详细说明;
第三步则是调用reload方法,将实现类压入服务的迭代器中。

简单的说,其实构造器这里只是对服务和类加载器做了一个初始化。

reload方法源码和LazyIterator构造器源码:

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

上面的步骤只是将对应的服务实现类和类加载器初始化压入了迭代器中,但最终是怎么实例化的呢?

通过调用ServiceLoader的iterator方法得到一个迭代器对象,这一部分源码不做太多解释,就是个基本的迭代器处理而已,关键是其中的hasNext方法,进一步查看其源码会发现:

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

第一步acc校验了权限,如果为空那么进入hasNextService方法,如果不为空其实做了一些处理后最终还是进到了hasNextService方法,我们直接查看hasNextService的源码:

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

会发现经过一些资源获取和处理后,得到了一个fullName,也就是全路径名。然后通过全路径名获取到资源内容,封装到configs中。再往后对获取到的内容进行一次解析,给到pending中。parse方法就是通过IO操作读取到了我们所配置的文件,其源码不贴了,就是输入输出流而已。关键是pending.next方法,该方法的源码如下,路径为ServiceLoader中的LazyIterator:

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

其跟上述的hasNext方法一样,最终调用到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());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

这一部分可以看到,将nextName也就是我们在文件中所写的全路径名给到cn,通过Class.forName反射的形式得到对象。往下通过newInstance获取实例对象,然后通过cast方法进行类型的转换,转换成一个泛型,也就是SPI服务对象,也就是我们所实现的服务对象:

    
    @SuppressWarnings("unchecked")
    public T cast(Object obj) {
        if (obj != null && !isInstance(obj))
            throw new ClassCastException(cannotCastMsg(obj));
        return (T) obj;
    }

上面的过程如果觉得有点懵逼的话,可以随时回顾刚才的项目三,调用接口实现的时候所做的每一步动作,load、iterator、hasNext和next方法。

最终抛出一个结论:

SPI就是通过IO流的形式读取文件中缩写的全路径名,再利用反射对其进行实例化。

当然这个结论还跳过了很多重要的步骤,比如中间涉及到的类加载器和权限内容。不过这属于类加载和委派机制的范畴了,故不在这里赘述。

收尾

通过了解SPI的加载机制我们可以知道一个结论,SPI虽然不能做到热插拔,但是他对接口实现了插拔机制,我们需要使用怎样的接口只需要使用对应的依赖即可。比如不同的数据库驱动,比如业务系统中想用微信支付、支付宝支付等场景,只需要将对应的依赖“插”进去即可。

同时回忆一下SpringBoot的自动装配机制,里面有一个spring.factories文件,其实也就是通过SPI机制做的实现。略有小小不同,可自行查阅资料。

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号