浅析Java-SPI机制

Posted 风在哪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅析Java-SPI机制相关的知识,希望对你有一定的参考价值。

Java SPI机制

SPI全称为Service Provider Interface(服务提供接口),是JDK内置的一种服务发现机制,是一种将服务接口与具体实现分离以达到解耦,大大提升了程序可扩展性的机制,引入服务提供者就是引入了spi接口的实现者,通过本地的注册发现获取到具体的实现类。

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

系统设计之初为了各个功能模块之间解耦,一般都是基于接口编程,模块之间不对实现类进行硬编码,一旦代码涉及到具体实现类的耦合,就违反了可插拔、开闭等原则,如果我们希望实现在模块装配的时候能够不在程序编码指定,那就需要一种服务发现机制。Java SPI就是这样一种机制:为某个接口寻找服务实现的机制。SPI的核心思想就是解耦。

这些 SPI 的接口由 Java 核心库来提供(由启动类加载器Bootstrap Classloader负责加载),而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由**启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)**来加载的。启动类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。于是加载SPI实现类的重任就落到了线程上下文类加载器(破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器)的身上。

那么Java SPI具体如何使用呢:

  1. 首先定义一组接口
  2. 写出接口的一个或者多个实现类
  3. 在src/main/resources/下新建/META-INF/services目录,并且新增一个以接口的全限定类名命名的文件,文件的内容就是接口的各个实现类的全限定类名
  4. 通过ServiceLoader加载接口的具体实现类

这就是JDK为我们实现的SPI机制,总体来说就是在配置文件中定义好接口的实现类,然后根据接口从配置文件中加载该接口的所有实现类,供我们的程序使用。

首先来看一个SPI的实际例子。

SPI例子

创建一个简单的maven项目,本例子是在maven项目中实现的

首先定义一组接口,这里定义了一个名为SPIService的接口:

public interface SPIService {
    /**
     * 方法执行
     */
    void execute();
}

然后我们提供这个接口的两个实现类:

public class SpiServiceOne implements SPIService{
    @Override
    public void execute() {
        System.out.println("SpiServiceOne is executed!");
    }
}


public class SpiServiceTwo implements SPIService{
    @Override
    public void execute() {
        System.out.println("SpiServiceTwo is executed!");
    }
}

通过SPI的机制我们如何获得这两个实现类呢?

首先,我们需要在resources文件夹下添加一个META-INF/services目录,在改目录下添加以SPIService接口的全限定类名为名的文件,在该文件中添加该接口的实现类的全限定类名,具体实现如下:

image-20210514150830949

image-20210514150844635

接下来就是测试用例,来看看JDK的SPI机制如何使用:

import java.util.Iterator;
import java.util.ServiceLoader;

public class Test {
    public static void main(String[] args) {
        ServiceLoader<SPIService> serviceLoader = ServiceLoader.load(SPIService.class);
        Iterator<SPIService> iterator = serviceLoader.iterator();
        while (iterator.hasNext()) {
            SPIService next = iterator.next();
            next.execute();
        }
    }
}

运行结果如下所示:

image-20210514150939188

原理分析

假如让我们自己实现SPI机制的话,我们会如何实现呢?这里提供一个简单的思路:

  1. 首先通过指定的文件加载出所有的类名
  2. 然后通过Java的反射机制构造出这些对象

Java的SPI机制也是类似的思路,它是通过接口的全限定类名作为了文件名,并且指定了文件所处的位置:/resources/META-INF/services,然后加载文件中接口对应的实现类的全限定类名,通过反射机制加载这些类。

那么JDK的SPI机制具体是怎么实现的呢,来看看它的源码。

简单源码分析

了解了SPI的简单使用,可以来看看JDK是如何实现的。

我们首先通过ServiceLoader的静态方法load来获取了ServiceLoader类,那么就从这个方法入手,来看看它的具体实现:

	@CallerSensitive
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
    }

该方法首先获得了线程上下文类加载器,然后调用了ServiceLoader的构造函数,其中Reflection.getCallerClass()方法是为了获得load方法的调用者,在本例中就是Test对象,可以在调试过程中发现:

image-20210514152224854

然后就会调用ServiceLoader的私有构造函数ServiceLoader(Class<?> caller, Class< S> svc, ClassLoader cl),构造出ServiceLoader对象并返回给调用者。

接下来看看serviceLoader.iterator()方法是如何实现的:

public Iterator<S> iterator() {

    // 如果查找服务的迭代器为空,那就新建一个
    if (lookupIterator1 == null) {
        lookupIterator1 = newLookupIterator();
    }
	// 创建一个迭代器
    return new Iterator<S>() {

        // 记录ServiceLoader的加载次数
        final int expectedReloadCount = ServiceLoader.this.reloadCount;

        // index into the cached providers list
        int index;

        /**
         * 如果缓存的服务提供者因为重新加载而被清空,那就抛出ConcurrentModificationException异常
         * 通过比较两者的reloadCount值即可
         */
        private void checkReloadCount() {
            if (ServiceLoader.this.reloadCount != expectedReloadCount)
                throw new ConcurrentModificationException();
        }

        @Override
        public boolean hasNext() {
            // 首先判断服务提供者是否已经加载完成,且缓存没有被清空
            checkReloadCount();
           	// 如果下标值小于服务提供者的数量,那么直接返回true
            if (index < instantiatedProviders.size())
                return true;
            // 否则调用lookupIterator1判断是否包含下一个元素
            return lookupIterator1.hasNext();
        }
		// 获得下一个元素
        @Override
        public S next() {
            // 首先判断加载的情况
            checkReloadCount();
            S next;
            /*
            instantiatedProviders用于缓存已经加载过的接口实现类
            所以我们先从这个缓存中获取资源
            如果缓存中没有,再调用迭代器重新进行加载,并将加载后的接口实现类存入缓存中
            */
            if (index < instantiatedProviders.size()) {
                next = instantiatedProviders.get(index);
            } else {
                // get()方法真正对接口实现类实例化的地方
                next = lookupIterator1.next().get();
                instantiatedProviders.add(next);
            }
            // 下标递增
            index++;
            return next;
        }

    };
}

这里采用了一种懒加载的方式,当我们调用iterator.hasNext()方法判断是否存在下一个元素时,ServiceLoader才会去文件中获取全限定类名,并且进行加载,看看调试的调用堆栈:

image-20210514161605334

那么直接来看看nextProviderClass方法:

private Class<?> nextProviderClass() {
    if (configs == null) {
        try {
            // fullName = META-INF/services/com.wyg.spi.SPIService
            String fullName = PREFIX + service.getName();
            // 如果类加载器为空的话,就调用ClassLoader加载资源
            if (loader == null) {
                configs = ClassLoader.getSystemResources(fullName);
            } else if (loader == ClassLoaders.platformClassLoader()) {
                // 如果当前类加载器为platformClassLoader,也就是平台类加载器,那就调用启动类加载器加载资源
                // 如果类加载器不为空,并且拥有当前的ClassPath,那就调用启动类加载器加载资源
                if (BootLoader.hasClassPath()) {
                    configs = BootLoader.findResources(fullName);
                } else {
                    configs = Collections.emptyEnumeration();
                }
            } else {
                // 如果类加载器即不为空,也不是平台类加载器,就调用它加载资源
                // 其实从load()方法我们可以知道,这里的loader类加载器其实是线程上下文类加载器,所以会走到这里
                configs = loader.getResources(fullName);
            }
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    // 解析获取到的资源,其实就是我们的接口全限定类名文件,获取它指定的实现类的全限定类名
    // 每次只会获取一个类,也就是说一次只加载一个类,也就是懒加载
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return null;
        }
        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;
    }
}

当我们调用iterator.next()方法时,ServiceLoader才会创造类的实例,也就是调用newInstance()方法:

image-20210514162917557

newInstance()方法是真正产生实现类实例的地方:

private S newInstance() {
    S p = null;
    Throwable exc = null;
    // 如果访问控制为空的话,可以直接对类进行实例化
    if (acc == null) {
        try {
            // 通过接口实现类的构造函数创建新的实例
            p = ctor.newInstance();
        } catch (Throwable x) {
            exc = x;
        }
    } else {
        /*
        如果存在访问控制的话,使用PrivilegedExceptionAction相当于有了访问构造函数的特权
        通过AccessController.doPrivileged可以在有访问控制的情况下进行对象实例的创建
        */
        PrivilegedExceptionAction<S> pa = new PrivilegedExceptionAction<>() {
            @Override
            public S run() throws Exception {
                return ctor.newInstance();
            }
        };
        // invoke constructor with permissions restricted by acc
        try {
            p = AccessController.doPrivileged(pa, acc);
        } catch (Throwable x) {
            if (x instanceof PrivilegedActionException)
                x = x.getCause();
            exc = x;
        }
    }
    // 如果异常不为空,会调用fail方法,返回失败
    if (exc != null) {
        if (exc instanceof InvocationTargetException)
            exc = exc.getCause();
        String cn = ctor.getDeclaringClass().getName();
        fail(service,
             "Provider " + cn + " could not be instantiated", exc);
    }
    return p;
}

通过上述一系列的函数调用,就可以获得对应的实例了。

实际案例分析

JDBC

在JDBC中就使用到了SPI机制:

image-20210514164708270

image-20210514164822420

我们在使用JDBC时,无需再自己指定驱动,JDBC会自动为我们寻找相关的驱动。

也就是我们在使用JDBC时,即使不写Class.forName(driver),也不会报错,SPI机制会自动帮助我们寻找相关的驱动实现。

Spring Boot

了解Spring Boot的读者应该知道,Spring Boot也有类似的SPI机制,Spring Boot的自动装配机制,就是通过加载META-INF/spring.factories文件中的内容,将对应的类实例化出来,放到Spring IoC容器中,供应用程序使用。

总结

JDK提供的SPI机制还是有一定缺陷的:

  • 它不能按需加载,需要遍历所有的实现,并进行实例化,我们只能在循环中找到自己需要的实现,而且在找到目标类之前,会加载其他的类,造成一定的资源浪费
  • 只能通过Iterator形式获取某个类的具体实现,不够灵活

如果想要扩展JDK的SPI机制,可以看看Dubbo是如何做的,这个我目前还没有研究,以后研究了再和大家分享!

以上是关于浅析Java-SPI机制的主要内容,如果未能解决你的问题,请参考以下文章

学习Java中的SPI机制

webpack模块机制浅析

java-spi

Spark Core任务运行机制和Task源代码浅析1

JavaScript运行机制浅析

安卓binder机制浅析