文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-08-17 11:16

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

在这里插入图片描述

一、引言

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

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

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

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

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

废话不多说,发车!

二、SPI是什么

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

img

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

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

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

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

三、使用介绍

我们定义一个接口 Phone

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

实现其三个类:

package com.study.spring.spi;public class HuaWei implements Phone {    @Override    public void getName() {        System.out.println("我是是华为手机");    }}
package com.study.spring.spi;public class IPhone implements Phone {    @Override    public void getName() {        System.out.println("我是是苹果手机");    }}
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,如下:

image-20230704221922760

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

com.study.spring.spi.HuaWeicom.study.spring.spi.IPhonecom.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 phoneServiceLoader = ServiceLoader.load(Phone.class);// 获取迭代器        Iterator 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() {        public Void run() {            // SPI的加载机制            ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);            // 迭代            Iterator driversIterator = loadedDrivers.iterator();            try{                while(driversIterator.hasNext()) {                    driversIterator.next();                }            }            return null;        }    });   }

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

      mysql     mysql-connector-java     8.0.18

测试类:

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 serviceLoader = ServiceLoader.load(Driver.class);        for (Iterator 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

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

image-20230704230413850

果然没错,接下来我们来一下 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 phoneServiceLoader = ServiceLoader.load(Phone.class);// 获取迭代器        Iterator it = phoneServiceLoader.iterator();        // 迭代遍历,输出集合中的所有元素        while (it.hasNext()) {            System.out.println(it.next());        }    }}

我们挨个分析

1、服务初始化加载

ServiceLoader phoneServiceLoader = ServiceLoader.load(Phone.class);public static  ServiceLoader load(Class service) {    // 获取当前线程的类加载器    ClassLoader cl = Thread.currentThread().getContextClassLoader();    return ServiceLoader.load(service, cl);}public static  ServiceLoader load(Class service, ClassLoader loader){    return new ServiceLoader<>(service, loader);}private ServiceLoader(Class 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 service, ClassLoader loader) {    this.service = service;    this.loader = loader;}

2、获取迭代器

Iterator it = phoneServiceLoader.iterator();// 返回我们ServiceLoader自定义实现的迭代器public Iterator iterator() {    return new Iterator() {        Iterator> 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 action = new PrivilegedAction() {            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 action = new PrivilegedAction() {            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流程图

高清图可私聊博主获取

在这里插入图片描述

七、总结

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

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

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

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

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

我们下期再见。

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

往期文章推荐:

来源地址:https://blog.csdn.net/qq_40915439/article/details/131566026

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯