SPI机制深度解析

176 阅读4分钟

SPI的概念

SPI(Service Provider Interface),是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。SPI是用来给予 拓展的API进行服务发现的。通过SPI,由传统的接口实现方定义接口,变成了接口使用方对于接口的定义,大大有利于框架框架进行拓展的人员的使用。

SPI使用接口编程+配置文件的动态编程+策略模式来实现上面的功能。下面介绍一下这三个东西:

首先是策略模式,定义了一系列算法,每个算法独立在相关的类当中使其能够彼此之间进行替换。下面的例子中的接口可以看做抽象策略而各种实现可以看做具体策略。

接口编程和配置文件动态编程,则是大家熟悉的概念。我们可以随时更改配置文件,因此整个方法和系统的耦合度低,内聚性强,可拔插。

个人理解下将SPI理解成一个本地单机系统的服务注册中心,或者说服务注册中心正是参考了SPI的一个分布式改进版本。

SPI的例子

SPI在Java的开发过程中大量使用,最经典的例子就是JDBC读取不同数据库驱动以及SLF4j的不同的日志实现类的加载。下面我们简单实现一个SPI的例子。 image.png

首先我们定义接口:

package com.example.spidemo.Interface;

public interface ServicesRegistrationCenter {
    void register(String RegistrationCenterName);
} //服务中心接口

接下来我们进行接口的不同实现: ConsulCenter接口

package com.example.spidemo.serviceIml;

public class ConsulCenter implements com.example.spidemo.Interface.ServicesRegistrationCenter{
    @Override
    public void register(String RegistrationCenterName) {
        System.out.println("ConsulCenter register");
    }
}

Nacos接口:


public class NacosCenter implements com.example.spidemo.Interface.ServicesRegistrationCenter{
    @Override
    public void register(String RegistrationCenterName) {
        System.out.println("Nacos register");
    }
}

Zookeeper接口:

package com.example.spidemo.serviceIml;

public class ZookperCenter implements com.example.spidemo.Interface.ServicesRegistrationCenter{
    @Override
    public void register(String RegistrationCenterName) {
        System.out.println("ZookperCenter register");
    }
}

接下来进行配置文件的动态编程:在resources目录下新建META-INF/services目录,并且在这个目录下新建一个与上述接口的全限定名一致的文件,在这个文件中写入接口的实现类的全限定名。

image.png 最后使用ServiceLoader的使用:

package com.example.spidemo;

import com.example.spidemo.Interface.ServicesRegistrationCenter;

import java.util.ServiceLoader;

public class SPItest {
    public static void main(String[] args) {
           ServiceLoader<ServicesRegistrationCenter> servicesRegistrationCenters = ServiceLoader.load(ServicesRegistrationCenter.class);
               for (ServicesRegistrationCenter src :servicesRegistrationCenters) {
                 src.register("filePath");
               }
           }

}

可以看到其整个流程如下:外部程序通过java.util.ServiceLoader类装载这个接口时,就能够通过该Jar包的META/Services/目录里的配置文件找到具体的实现类名,装载实例化,完成注入。同时,SPI的规范规定了接口的实现类必须有一个无参构造方法。

SPI的原理探析

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


    //扫描目录前缀
    private static final String PREFIX = "META-INF/services/";

    // 被加载的类或接口
    private final Class<S> service;

    // 用于定位、加载和实例化实现方实现的类的类加载器
    private final ClassLoader loader;

    // 上下文对象
    private final AccessControlContext acc;

    // 按照实例化的顺序缓存已经实例化的类
    private LinkedHashMap<String, S> providers = new LinkedHashMap<>();

    // 懒查找迭代器
    private java.util.ServiceLoader.LazyIterator lookupIterator;

    // 私有内部类,提供对所有的service的类的加载与实例化
    private class LazyIterator implements Iterator<S> {
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        String nextName = null;

        //...
        private boolean hasNextService() {
            if (configs == null) {
                try {
                    //获取目录下所有的类
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    //...
                }
                //....
            }
        }

        private S nextService() {
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //反射加载类
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
            }
            try {
                //实例化
                S p = service.cast(c.newInstance());
                //放进缓存
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                //..
            }
            //..
        }
    }
}

这里的Prefix是的默认位置是"META-INF/services/",这也是我们为什么要在"META-INF/services/"新建相关文件了。 首先我们传入当前线程的类加载器,和这个class对象,构造一个新的ServiceLoader:对象。在ServiceLoader构造方法中,new一个新的LazyIterator(service,loader); LazyIterator是一个私有的内部类,其作用是扫描(包括所有引用的jar包里的)META INF/services,目录下的配置文件并parse其中的配置的所有的service的名字,配置文件中所有配置的类的全限定名,通过反射加载,实例化并且加载到缓存中去。

下面简单介绍一下懒查找迭代器:所谓懒查找迭代就是不要一次性取得所有数据,这样对于内存的消耗太大了,逐步读取所有的相关文件。

懒查找迭代器

懒查找迭代器(Lazy Iterator)是一种特殊的迭代器,它能够在迭代过程中按需生成元素,而不是一次性生成所有元素。这种迭代器通常用于处理大量数据,因为它能够避免一次性生成大量数据并占用大量内存。

下面举一个简单例子斐波那契数列的例子来更好的理解其概念:

import java.util.Iterator;

public class FibonacciIterator implements Iterator<Integer> {
    private int a = 0;
    private int b = 1;

    @Override
    public boolean hasNext() {
        return true;
    }

    @Override
    public Integer next() {
        int result = a;
        int temp = a + b;
        a = b;
        b = temp;
        return result;
    }
}
public class Main {
    public static void main(String[] args) {
        Iterator<Integer> iterator = new FibonacciIterator();
        for (int i = 0; i < 10; i++) {
            System.out.println(iterator.next());
        }
    }
}

其中每次调用 next() 方法时才会计算下一个元素,而不是一次性生成所有元素,这就是懒查找迭代。

本文所有代码地址:github.com/sumingfirst…

参考文献

一文搞懂Spring的SPI机制(详解与运用实战) - 掘金 (juejin.cn) 结合实战和源码来聊聊Java中的SPI机制? - 冰河团队 - 博客园 (cnblogs.com) Java SPI详解 - jy的blog - 博客园 (cnblogs.com)