您现在的位置是:首页 > 正文

从源码分析:Java中的SPI是怎样工作的

2024-02-01 06:39:33阅读 2

spi介绍

提到api,大家或多或少地都接触或者使用过,但是如果说到spi呢,可能了解的人就要少一些。

Java SPI的全称为Service Provider Interface,相对于api来讲的话,简单来说,api是提供给用户来进行使用的,而spi是提供给开发者来进行扩展的。也就是说,当我们使用spi的时候,从最基本来说,是基于接口的编程的方式,通过配置文件来实现动态加载,而在编写客户端程序时,可以使用基于策略模式的方式来编写代码,从而使得具体的方法基于同一接口不同的实现类来实现。

也就是说,spi就是为了实现这种解耦的一种服务发现的机制。

spi的使用

在前一篇的博客中有写过一个使用spi的demo:Java中的SPI的使用例子

根据这篇博客,就可以创建出一个完整的使用spi的例子,在客户端中调用同一接口的不同实现。

我们需要注意,当我们在使用SPI时,有如下约定:

  1. 配置文件为在META-INF/services/目录中创建的以接口的全限定名命名的文件,文件内容为需要调用的Api具体实现类的全限定名
  2. 调用时,需要使用java.util.ServiceLoder类动态加载配置文件中所声明的类
  3. 如果SPI的实现类为Jar,则需要放在主程序classPath中
  4. SPI的具体实现类必须有一个不带参数的构造方法

从源码对Java SPI进行分析

首先我们看一下我们在上一节所提到的例子:Java中的SPI的使用例子中的客户端代码:

// testClient.java
import com.lf.API.MyPrinterAPI;
import java.util.ServiceLoader;

public class testClient {
    public static void main(String[] args) {
        ServiceLoader<MyPrinterAPI> printers = ServiceLoader.load(MyPrinterAPI.class);
        for (MyPrinterAPI printer : printers) {
            printer.sayHello("SPI");
        }
    }
}

可以看出,其中调用的关键的一行代码为:

ServiceLoader<MyPrinterAPI> printers = ServiceLoader.load(MyPrinterAPI.class);

在上一节中,提到了在Java中使用SPI的约定,其中第三条所提到的java.util.ServiceLoder就是这里所用的ServiceLoader了,因此,让我们来一起看一看这个类和它的load方法。

首先是ServiceLoader的类的代码:

package java.util;

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

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

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

    // 缓存的providers,以实例化的顺序保存(若实例化,则存入)
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

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

    ... ...

}

首先,我们可以看到这个ServiceLoader类的声明中是有final的,说明这个类是不可继承的,同时,这个类实现了Iterable接口,因此我们可以用迭代器来方便地遍历所需接口的在配置文件中所声明的所有实现类。

紧接着后面是声明的类中的一些成员变量,其中第一个private static final String PREFIX = "META-INF/services/";,这也就是约定中第一条所提到的配置文件需要在META-INF路径的原因了。

接着来看一下testClient中所调用的load方法的具体是实现:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 加载线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

这里为了方便起见,逐个将被调用的方法列出:

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader)
{
    return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
    // 确保svc不为空,并将传入的类名存入到成员变量service中
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 判断传入的加载器是否为空,若为空,则使用ClassLoader.getSystemClassLoader()
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}
public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}
public void reload() {
    // providers为一LinkedHashMap<String,S>类型的成员变量,这里将其清空
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

到这里为止,以上的方法都是ServiceLoader类的方法,而这里reload()方法中new了一个LazyIterator,这个类其实也是ServiceLoader类的内部类。

我们来看一下内部类LazyIterator的完整的代码:

// ServiceLoader.java
private class LazyIterator
    implements Iterator<S>
{

    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

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

    private boolean hasNextService() {
        // 如果已经写入过下一个实现类的类名,说明存在下一个实现类
        // 且可以保证不会在读取nextName之前,重复写nextName,防止空过某个类
        if (nextName != null) {
            return true;
        }
        // 若configs为null,则初始化configs
        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);
            }
        }
        // 若pending为null,则对pending初始化
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            // 逐行将配置中的实现的类名读出,存入ArrayList<String> names中,并返回names的迭代器
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }

    private S nextService() {
        // 判断是否有下一个实现类,并确保下一个实现类的类名被写到nextName
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            // initialize = false 不必须初始化,这样在加载类时并不会立即运行静态区块,而会在使用类建立对象时才运行静态区块。
            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());
            // 将当前的对象放入外部类的map型成员变量providers中
            providers.put(cn, p);
            // 并返回当前的对象
            return p;
        } catch (Throwable x) {
            fail(service,
                    "Provider " + cn + " could not be instantiated",
                    x);
        }
        throw new Error();          // This cannot happen
    }

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

    public S next() {
        if (acc == null) {
            return nextService();
        } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                public S run() { return nextService(); }
            };
            // AccessController.doPrivileged意思是这个是特别的,不用做权限检查
            return AccessController.doPrivileged(action, acc);
        }
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }

}

从代码中我们可以明显地看出来,这是一个实现了Iterator接口的内部类,其中定义了两个方法hasNextService()nextService()来表示是否有下一个实现类与得到下一个实现类,而为了实现接口Iterator,需要实现方法hasNext()next(),于是我们可以看到,这两个方法的实现其实都比较简单,其实仅仅是先判断acc是否为null,若不为null,则直接调用上面所述的两个方法并返回。

看了这么多这个内部类,现在我们把思路再理清一下,我们是在ServiceLoader中的reload()方法中实例化了这个内部类,传入了外部传入的serviceloader,即我们自己定义的API的类与load方法中get的线程类加载器,在实例化这个内部类(即一个迭代器)后,将这个迭代器的实例赋给ServiceLoader类的实例成员lookupIterator,而后,ServiceLoader的遍历便依靠这个迭代器与成员变量providers来实现。

实现的具体方法为:

// ServiceLoader.java
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();
        }

    };
}

可以看到,ServiceLoader的迭代器,会先实例化providers.entrySet()的迭代器作为基本的迭代器,之后遍历这个迭代器的方式为:先遍历providers,遍历完providers之后,再看’lookupIterator’中,是否还有其它的实现类,这个lookupIterator便是我们前面所看过的内部类LazyIterator

至此,我们便把JDK提供的SPI方式遍历并调用服务的所用的代码都过了一边,大家在自己的IDE看这样的代码时要多利用IDE提供的代码追踪、依赖顺序等功能,并多多利用DeBug的方式来分析代码究竟是如何执行的。希望这篇文章能够让大家觉得有所收获。

网站文章

  • 家用 文件服务器,家用文件服务器

    家用 文件服务器,家用文件服务器

    家用文件服务器 内容精选换一换远程桌面协议(Remote Desktop Protocol,RDP),是微软提供的多通道的远程登录协议。本节为您介绍如何使用RDP文件远程登录Windows弹性云服务器...

    2024-02-01 06:39:25
  • SpringCloud-向Eureka注册中心注册微服务(微服务的搭建)

    SpringCloud-向Eureka注册中心注册微服务(微服务的搭建)

    Eureka注册中心注册微服务 注明:此项目为本人学习尚硅谷老师的教学视频然后整理核心的配置文件,所有的项目均在以下地址下载。https://github.com/xwbGithub/microservicecloud下载, 本章讲解请参考microservicecloud-provider-dept-8001,首先微服务的服务中心...

    2024-02-01 06:38:54
  • spring实现aop的步骤

    spring实现aop的步骤

    首先注意导入一个jar包! 在applicationContext.xml中配置扫包 :开启aop的自动代理 切面类 @Component//spri

    2024-02-01 06:38:47
  • 爬虫技术原来可以做这么多牛逼哄哄的事情!

    爬虫技术原来可以做这么多牛逼哄哄的事情!

    对于很多对于不懂编程语言的GGMM来说,爬虫技术高深莫测。但是对于IT工程师来说,爬虫技术可以说信手拈来。虽然熟知爬虫技术,你是否知道它竟然可以做这么多这么牛逼哄哄的事情! 1.利用爬虫技术抓取公司用户信息 公司有15k员工,办公系统的hr模块,只要有部门级的管理人员权限就可以看自己部门的几百名员工资料,包括历年历月的工资条和具体个人信息。关键是,网页地址上有员工编号,如果改一下编号...

    2024-02-01 06:38:41
  • 构建工具webpack与babel使用

    构建工具webpack与babel使用

    Babel入门 一、Bable是什么 Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。 这意味着,你可以现在就用 ES6 编写程序,而不用担心现有环境是否支持。下...

    2024-02-01 06:38:34
  • ASP页面基础知识

    1, 是什么意思呢?是加入样式文件,     href="style.css"   是加入sytle.css样式文件 。rel指定链接link的类型,style指定目标文件href的类型 2,

    2024-02-01 06:37:58
  • Matlab:多输入多输出非线性对象的模型预测控制(MPC, Model Predictive Control)的实现

    Matlab:多输入多输出非线性对象的模型预测控制(MPC, Model Predictive Control)的实现

    [TOC](Matlab:多输入多输出非线性对象的模型预测控制(MPC, Model Predictive Control)的实现)✨ 本文展示了如何在Simulink中设计多输入多输出对象的闭环模型预测控制 (MPC, Model Predictive Control),分析对象具有三个操纵变量(Manipulated Variables)与两个测量输出(Measured Output)。

    2024-02-01 06:37:52
  • cuda 排序算法笔记

    Thrust是cuda自带的c++库,cuda安装好之后,这个库也默认安装好了。 这个库基本是采用类似STL的接口方式,因此对于开发者非常友好,开发者不再需要关注内存与显存相关的问题了。 排序入门例子 #include #include #include #include #incl..

    2024-02-01 06:37:19
  • OkHttp完全解析

    OkHttp完全解析

    网上关于OkHttp的使用教程已经有很多了,先来贴一片网上的使用教程: http://blog.csdn.net/chenzujie/article/details/46994073然后直接进入正题。 看完上面这篇文章,主要理解的几个点:外部通过构造Request,初始化OkHttpClient,并由两者共同构造出Call。访问网络通过Call,Call支持两种模式:同步和异步。同步使用exe

    2024-02-01 06:37:13
  • 一份两百亿阅读的 Git 教程!

    一份两百亿阅读的 Git 教程!

    公众号关注“GitHubPorn”设为 “星标”,每天带你逛 GitHub!大家好,我是小 G。如果你用搜索引擎搜索关键字「Git教程」的话,排在第一的肯定是廖雪峰的网站,毕竟这个教程...

    2024-02-01 06:36:44