4

JavaSPI详解 - bmilk

 1 year ago
source link: https://www.cnblogs.com/bmilk/p/16842249.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
  • 什么是SPI
  • API 与 SPI
  • 一个简单的例子
  • SPI机制的实现
  • Java SPI的问题
  • 为什么SPI机制打破了双亲委派模型

在项目开发中,经常会使用到数据库驱动,我们连接的数据库可能是MySQL也有可能是Oracle,但是不管使用什么数据库都是引入数据库驱动配置相应的地址、用户、密码信息就可以使用而不用修改业务代码。

这是因为在JDK中提供了一个java.sql.Driver接口。各个数据库厂商只需要实现这个接口,当我们引入相应驱动,连接数据库的时候,就会使用厂商提供的实现,那么又是如何知道厂商实现的类路径的呢??

什么是SPI

SPI全名Service Provider interface,翻译过来就是“服务提供接口”,再说简单就是提供某一个服务的接口, 提供给服务开发者或者服务生产商来进行实现。
Java SPIJDK内置的一种动态加载扩展点的实现。
这个机制在一般的业务代码中很少用到(个人接触到的业务没有用到过),但是再底层框架中却被大量使用,包括JDBCDubboSpring框架、日志接口中都有用到,不同的是有的使用Java原生的实现,有的框架则自己实现了一套SPI机制

API 与 SPI

API 全称Application Programming Interface, 翻译为“应用程序接口”,指的是应用程序为外部提供服务的接口,这个接口通常由服务提供者自行开发,定义好接口后很少改动。APISPI示意图如图1,图2所示

1669869-20221030210456102-2123926598.jpg
图1  API示意图
1669869-20221030210444495-1643692188.jpg
图2  SPI示意图

一般应用(模块)之间通过接口进行通讯,服务提供方提供接口并进行实现后,调用方就可以通过调用这个接口拥有服务提供发提供的能力,这个就是API
当接口是由服务调用方提供,并且由服务提供方进行实现时,服务调用方就可以根据自己的需要选择特定实现,而不用更改业务代码以获取相应的功能,这个就是SPI

一个简单的例子

这个功能是向注册中心注册服务的一个示例(过于简单的示例),

首先定义一个接口Registry, 这个接口只有一个功能,就是向注册中心注册一个服务,但是我现在并不确定我选的是什么注册中心,于是提供了一个统一的接口,由各个厂商进行实现

package cn.bmilk.chat.spi;

public interface Registry {
    void  registry(String host, int port);
}

厂商实现好后,需要在其META-INF/services文件夹下新增一个文件,文件名为该接口的全限定名即:cn.bmilk.chat.spi.Registry, 内容为接口实现的全限定名,这里我写了两个

cn.bmilk.chat.spi.EurekaRegistry
cn.bmilk.chat.spi.ZookeeperRegistry

两个类的都是空实现,内容如下

    @Override
    public void registry(String host, int port) {
        System.out.println(this + "registry , host = " + host +"  port = " + port);
    }

下面编写测试主类,通过 ServiceLoader 加载 Registry 实现

public class MainTest {

    public static void main(String[] args) {

        ServiceLoader<Registry> load = ServiceLoader.load(Registry.class);
        Iterator<Registry> iterator = load.iterator();
        while (iterator.hasNext()){
            Registry registry = iterator.next();
            registry.registry("127.0.0.1", 10086);
        }
    }
}
class cn.bmilk.chat.spi.EurekaRegistry
cn.bmilk.chat.spi.EurekaRegistry@12a3a380registry , host = 127.0.0.1  port = 10086
class cn.bmilk.chat.spi.ZookeeperRegistry
cn.bmilk.chat.spi.ZookeeperRegistry@29453f44registry , host = 127.0.0.1  port = 10086

从运行结果中可以看到EurekaRegistryZookeeperRegistry都被实例化并且生成相应的对象。但是我们全程并没有显示的加载和生成EurekaRegistryZookeeperRegistry类对象,那么是怎么来的呢?

SPI机制的实现

SPI机制的核心就是ServiceLoader类。其主要的属性如下:

    // 指出接口配置文件的位置,也就是为什么要在META-INF/services/下创建接口的全限定名文件的原因
    private static final String PREFIX = "META-INF/services/";

    // 正在被加载的类(接口)的class对象
    private final Class<S> service;

    // 加载使用的类加载器
    private final ClassLoader loader;

    // 创建 ServiceLoader 时采用的访问控制上下文
    private final AccessControlContext acc;

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

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

load()方法的实现如下:

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        // 使用当前线程的ClassLoader进行加载待加载的实现类
        return ServiceLoader.load(service, cl);
    }

    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        // load 方法本质是创建一个ServiceLoader对象
        return new ServiceLoader<>(service, loader);
    }

    // new ServiceLoader<>(service, loader)的实现
    private ServiceLoader(Class<S> 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();
    }

    public void reload() {
        providers.clear();
        // 根据接口类型(父类)和类加载器初始化LazyIterator
        lookupIterator = new LazyIterator(service, loader);
    }

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

跟踪load()方法发现其本质是创建了一个ServiceLoader对象,其共有两个参数,分别是代加载的类父类(接口)Class类对象和类加载器。在构造方法中完成了两件事,一个是变量赋值,一个是调用reload()方法。reload()方法则根据接口类型(父类)和类加载器初始化LazyIterator

当执行ServiceLoader#iterator()时,会创建java.util.Iterator匿名内部类实现:

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

        };
    }

当执行hasNext() 方法时,会先去providers 查找已经加载的缓存实现,如果不存在,则会继续调用LazyIterator#hasNext()用于发现尚未加载的实现,最后的实现在LazyIterator#hasNextService()

LazyIterator的关键属性

// 缓存所有需要查找jar包(文件)路径,
Enumeration<URL> configs = null;
// 缓存所有被查找到的实现类全限定名
Iterator<String> pending = null;
// 迭代器使用,下一个需要被加载的类全限定名
String nextName = null;

hasNextService()实现核心如下:

// 获取所由需要扫描的包路径
configs = loader.getResources(fullName);
// 循环扫描configs中所有的包路径,解析META-INF/services中的指定文件(上例中的cn.bmilk.chat.spi.Registry文件)
// 
while ((pending == null) || !pending.hasNext()) {
    if (!configs.hasMoreElements()) {
        return false;
    }
    // pending缓存了所有查找到的类全限定名
    pending = parse(service, configs.nextElement());
}

在知道是否存在接口的实现后,就是通过next()方法获取实现,核心功能由nextService()贡献,核心实现如下:

// 获取一个实现类全限定名
String cn = nextName;
// 加载这个类
Class<?> c = Class.forName(cn, false, loader);
// 使用反射创建对象
c.newInstance()

hasNextService()完成堆配置文件的读取,nextService()完成类的加载和对象的创建,这个一切都没有在ServiceLoader创建时完成,这也是体现了延迟Lazy的一个含义

load()与loadInstalled()

loadInstalled()load()一样,本质都是创建了一个ServiceLoaderd对象,不同点是使用的加载器不同,load()使用的是Thread.currentThread().getContextClassLoader()当前线程的上下文加载器, loadInstalled()使用的是ExtClassLoader加载器来加载

具体实现如下:

    public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        ClassLoader prev = null;
        while (cl != null) {
            prev = cl;
            cl = cl.getParent();
        }
        return ServiceLoader.load(service, prev);
    }


使用这个方法将只扫描JDK安装目录jre/lib/ext下的jar包中指定的实现,我们应用程序类路径下的实现将会被忽略掉

Java SPI的问题

  • Java SPI虽然使用了懒加载机制,但是其获取一个实现时,需要使用迭代器循环加载所有的实现类
  • 当需要某一个实现类时,需要通过循环一遍来获取

这个两个问题,在Dubbo实现自己的SPI机制时进行了增强,可以仅加载自己想要的扩展实现。

为什么SPI机制打破了双亲委派模型 ??

想不明白 说不清楚,想明白再补充


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK