从源码全面解析 Java SPI 的来龙去脉

这篇具有很好参考价值的文章主要介绍了从源码全面解析 Java SPI 的来龙去脉。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

  • 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
  • 📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列、duubo源码系列
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
  • 📝联系方式:hls1793929520,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

从源码全面解析 Java SPI 的来龙去脉,# dubbo,java,开发语言,面试,后端,SPI,原力计划

从源码全面解析 Java SPI 的来龙去脉

一、引言

对于 Java 开发者而言,关于 dubbo ,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。

但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。

本期 dubbo 源码解析系列文章,将带你领略 dubbo 源码的奥秘

本期源码文章吸收了之前 SpringKakfaJUC源码文章的教训,将不再一行一行的带大家分析源码,我们将一些不重要的部分当做黑盒处理,以便我们更快、更有效的阅读源码。

虽然现在是互联网寒冬,但乾坤未定,你我皆是黑马!

废话不多说,发车!

二、SPI是什么

SPI 全称 Service Provider Interface ,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。

从源码全面解析 Java SPI 的来龙去脉,# dubbo,java,开发语言,面试,后端,SPI,原力计划

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

Java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。

将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

所以 SPI 的核心思想就是解耦

三、使用介绍

我们定义一个接口 Phone

package com.study.spring.spi;
public interface Phone {
    public void getName();
}

实现其三个类:

  • HuaWei
package com.study.spring.spi;

public class HuaWei implements Phone {
    @Override
    public void getName() {
        System.out.println("我是是华为手机");
    }
}
  • IPhone
package com.study.spring.spi;

public class IPhone implements Phone {
    @Override
    public void getName() {
        System.out.println("我是是苹果手机");
    }
}
  • XiaoMi
package com.study.spring.spi;

public class XiaoMi implements Phone {
    @Override
    public void getName() {
        System.out.println("我是是小米手机");
    }
}

重点来了:我们要在 resources 文件夹下面建立一个路径:META-INF/services

然后我们建立一个 txt 名为:com.study.spring.spi.Phone,如下:

我们在这个文件中写上各实现类的路径:

com.study.spring.spi.HuaWei
com.study.spring.spi.IPhone
com.study.spring.spi.XiaoMi

测试类:

package com.study.spring.spi;

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

public class JavaSPITest {
    public static void main(String[] args) {
        // 执行Java SPI的规范
        ServiceLoader<Phone> phoneServiceLoader = ServiceLoader.load(Phone.class);
		// 获取迭代器
        Iterator<Phone> it = phoneServiceLoader.iterator();
        // 迭代遍历,输出集合中的所有元素
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

结果:

我是是华为手机
我是是苹果手机
我是是小米手机

从这里我们可以看到,通过 SPI 的机制,我们可以获取当前接口类的实现

四、生产场景

相信大家在生产上都使用过 JDBC,没错,我们的 JDBC 实际上也使用了 SPI

我们看 DriverManager 的静态方法 loadInitialDrivers

 static {
     // 初始化加载
     loadInitialDrivers();
     println("JDBC DriverManager initialized");
}

我们查看下 loadInitialDrivers 方法的代码:

private static void loadInitialDrivers() {
    String drivers;
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            // SPI的加载机制
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // 迭代
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            }
            return null;
        }
    });   
}

当然,这里我们需要引入下面的 MAVEN 依赖,不然 Driver.class 的实现类为空

 <dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <version>8.0.18</version>
</dependency>

测试类:

package com.study.spring.spi;

import java.sql.Driver;
import java.util.Iterator;
import java.util.ServiceLoader;

public class JDBCTest {
    public static void main(String[] args) {
        ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class);

        for (Iterator<Driver> iterator = serviceLoader.iterator(); iterator.hasNext(); ) {

            Driver driver = iterator.next();

            System.out.println(driver.getClass().getPackage() + " ------> " + driver.getClass().getName());
        }
    }
}

启动:

package com.mysql.cj.jdbc, JDBC, version 4.2 ------> com.mysql.cj.jdbc.Driver

从这里的输出我们可以得出一个假设:我们引入的 JDBC 包里面,存在上述 SPI 机制的 txt 文件名称为: java.sql.Driver 且内容为:com.mysql.cj.jdbc.Driver

我们直接去引入的包里面搜索一下:

从源码全面解析 Java SPI 的来龙去脉,# dubbo,java,开发语言,面试,后端,SPI,原力计划

果然没错,接下来我们来一下 SPI 的原理

五、SPI运行原理剖析

从我们上面的示例中可以发现,SPI 的实现一共两行:

package com.study.spring.spi;

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

public class JavaSPITest {
    public static void main(String[] args) {
        // 执行Java SPI的规范
        ServiceLoader<Phone> phoneServiceLoader = ServiceLoader.load(Phone.class);
		// 获取迭代器
        Iterator<Phone> it = phoneServiceLoader.iterator();
        // 迭代遍历,输出集合中的所有元素
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

我们挨个分析

1、服务初始化加载

ServiceLoader<Phone> phoneServiceLoader = ServiceLoader.load(Phone.class);

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:com.study.spring.spi.Phone
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // AppClassLoader
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    // 安全控制
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    // 重点
    reload();
}

这里的重点还是在我们的 reload 中:

public void reload() {
    // 清空我们的服务提供者缓存
    providers.clear();
    // 延时迭代器,用于延迟加载服务提供者
    // 达到用需加载的目的
    lookupIterator = new LazyIterator(service, loader);
}

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

2、获取迭代器

Iterator<Phone> it = phoneServiceLoader.iterator();

// 返回我们ServiceLoader自定义实现的迭代器
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();
        }
    };
}

3、判断是否有数据

it.hasNext()
public boolean hasNext() {
    // 刚刚初始化knownProviders是空的
    if (knownProviders.hasNext())
        return true;
    // 
    return lookupIterator.hasNext();
}

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

重点还是在 hasNextService 方法中

private boolean hasNextService() {
    // 如果当前的Name不等于空直接返回
    if (nextName != null) {
        return true;
    }
    // 配置是否为空(第一次肯定为空)
    if (configs == null) {
        try {
            // 写死的:META-INF/services 加上接口的名字
            // META-INF/services/com.study.spring.spi.Phone
            String fullName = PREFIX + service.getName();
            // 加载配置
            // configs
            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;
        }
        // 从上述配置文件中读取当前的配置信息
        // com.study.spring.spi.HuaWei
        // com.study.spring.spi.IPhone
        // com.study.spring.spi.XiaoMi
        pending = parse(service, configs.nextElement());
    }
    // 遍历当前的列表,返回第一次的遍历信息(com.study.spring.spi.HuaWei)
    nextName = pending.next();
    return true;
}

4、结果输出

System.out.println(it.next());
public S next() {
    // 刚刚初始化knownProviders是空的
    if (knownProviders.hasNext())
        return knownProviders.next().getValue();
    return lookupIterator.next();
}

public S next() {
    // 是否有控制权限
    // 一般都是空的
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

重点还是在 nextService 方法中

private S nextService() {
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    // cn:com.study.spring.spi.HuaWei
    // 根据类加载器+类的名称得出类
    c = Class.forName(cn, false, loader);
	
    // 创建实例 
    S p = service.cast(c.newInstance());
    // 将实例放到缓存里面
    // key:com.study.spring.spi.HuaWei
    // value:HuaWei@773
    providers.put(cn, p);
    // 返回实例
    return p;
}

六、SPI流程图

高清图可私聊博主获取

从源码全面解析 Java SPI 的来龙去脉,# dubbo,java,开发语言,面试,后端,SPI,原力计划

七、总结

鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。

其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。

后面会继续更新 DubboSPI 以及项目真实落地的 SPI 实现

如果你也对 后端架构中间件源码 有兴趣,欢迎添加博主微信:hls1793929520,一起学习,一起成长

我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,喜欢后端架构和中间件源码。

我们下期再见。

我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。

往期文章推荐:文章来源地址https://www.toymoban.com/news/detail-531023.html

  • 美团二面:聊聊ConcurrentHashMap的存储流程
  • 从源码全面解析Java 线程池的来龙去脉
  • 从源码全面解析LinkedBlockingQueue的来龙去脉
  • 从源码全面解析 ArrayBlockingQueue 的来龙去脉
  • 从源码全面解析ReentrantLock的来龙去脉
  • 阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似
  • 从源码全面解析 ThreadLocal 关键字的来龙去脉
  • 从源码全面解析 synchronized 关键字的来龙去脉
  • 阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试

到了这里,关于从源码全面解析 Java SPI 的来龙去脉的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 从源码全面解析 dubbo 消费端服务调用的来龙去脉

    👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主 📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列、duubo源码系列 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦

    2024年02月09日
    浏览(38)
  • 从源码全面解析Java 线程池的来龙去脉

    👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主 📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦 🍂博主正在努

    2024年02月03日
    浏览(61)
  • 从源码全面解析 ArrayBlockingQueue 的来龙去脉

    👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主 📕系列专栏:Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙、Spring从成神到升仙系列 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一

    2024年02月05日
    浏览(66)
  • 《吊打面试官系列》从源码全面解析 ThreadLocal 关键字的来龙去脉

    👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主 📕系列专栏:Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙、Spring从成神到升仙系列 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一

    2023年04月23日
    浏览(67)
  • 《吊打面试官系列》从源码全面解析 synchronized 关键字的来龙去脉

    👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主 📕系列专栏:Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙、Spring从成神到升仙系列 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一

    2023年04月16日
    浏览(47)
  • 【Spring从成神到升仙系列 四】从源码分析 Spring 事务的来龙去脉

    👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主 📕系列专栏:Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙、Spring从成神到升仙系列 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一

    2024年02月03日
    浏览(34)
  • 二次型的来龙去脉

            在学习二次型的时候没有好好理解概念,导致记住了可以用的结论,但往往遇到题目反应不过来,故这次对二次型进行一个详细剖析。         首先二次型是什么?是一个n元变量的二次齐次多项式,根据二次齐次多项式的定义(所有单项的次数都是2,单项的次数为

    2024年02月09日
    浏览(39)
  • 简单聊聊Https的来龙去脉

    使用明文通信,通信内容可能会被监听 不验证通信双方身份,因此可能会遭遇伪装 无法验证报文完整性,可能会遭到中间人攻击,从而篡改请求和响应报文中的内容 Http 协议直接和TCP进行通信,而 Https 在 Http 和 Tcp 之间加了一层 SSL 实现加密传输 : SSL ( Secure Socket Layer ) 安全

    2024年02月10日
    浏览(44)
  • 一文解释mapState的来龙去脉

    mapState Vuex 提供的辅助函数之一,将 store 中的状态映射到组件的计算属性中,使得在组件中可以轻松地访问 Vuex store 中的状态值 MapState(映射状态) 在我们的 Count.vue 组件中,可以使用 mapState 来更简洁地获取 count 的状态值 首先,导入 mapState : 然后,在 computed 中使用 mapState :

    2024年02月07日
    浏览(41)
  • 你所不知道的 数据在内存中储存 的来龙去脉

            那么好了好了,宝子们,今天给大家介绍一下 “数据在内存中储存” 的来龙去脉,来吧,开始整活!⛳️          一、数据类型的介绍 (1)整型和浮点型:    (2)其他类型:    二、数据在内存中的储存顺序(大端 小端)   (1)引入字节序: 字节序

    2024年02月06日
    浏览(69)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包