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

news2024/11/25 6:28:34

一、引言

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

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

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

废话不多说,发车!

二、SPI是什么

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

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

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

果然没错,接下来我们来一下 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流程图

七、总结

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/722993.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数字化转型应该从哪里开始?

数字化转型可能是一个复杂的过程&#xff0c;涉及将数字技术和战略集成到组织的各个方面。虽然具体的起点可能会根据组织的规模、行业和目标而有所不同&#xff0c;但数字化转型计划通常从以下几个共同领域开始&#xff1a; 愿景和战略&#xff1a;转型之旅应从与组织目标一致的…

百万数据SQL优化技巧,看这一篇就够了(实操+详细总结)

前言&#xff1a;这次准备了100W的数据进行SQL性能测试&#xff0c;数据库采用的是MySQL&#xff0c;总共介绍了常见的15种SQL优化方式&#xff0c;每一种优化方式都进行了实打实的测试&#xff0c;逐行讲解&#xff0c;通俗易懂&#xff01; 目录 一、准备数据 1、创建表结构…

Windows环境下安装和配置python环境

Windows环境下安装和配置python环境 1.官网下载&#xff1a;https://www.python.org/downloads/release 2.安装&#xff1a;自定义路径即可无脑下一步 3.cmd打开控制台&#xff0c;输入python&#xff0c;如果页面切换成以下样子就说明安装成功了 4.运行python脚本【步骤3切入…

555、Vue 3 学习笔记 -【常用Composition API(四)】 2023.07.06

目录 一、setup的两个注意点1. setup执行的时机2. setup的参数 二、 计算属性与监视1. computed函数2. watch函数3. watchEffect函数 三、参考链接 一、setup的两个注意点 1. setup执行的时机 在beforeCreate之前执行一次&#xff0c;this是undefined 2. setup的参数 props…

GitLab名词介绍

GitLab名词介绍 分支&#xff1a;active、stale、default、protected IDEA中git面板&#xff1a;本地、远程、HEAD 合并时的选项&#xff1a;Delete、squash 查看Git常用操作 分支&#xff1a;active、stale、default、protected 在分支页面下&#xff0c;有active、stale…

k8s中kubectl陈述式/声明式资源管理

k8s陈述资源管理方法的说明 1.kubernetes 集群管理集群资源的唯一入口是通过相应的方法调用 apiserver 的接口 2.kubectl 是官方的CLI命令行工具&#xff0c;用于与 apiserver 进行通信&#xff0c;将用户在命令行输入的命令&#xff0c;组织并转化为 apiserver 能识别的信息&…

TongWeb8关于内存泄露提示: To prevent a memory leak

原因&#xff1a; 该问题与 https://blog.csdn.net/realwangpu/article/details/109510297 相同&#xff0c;TongWeb7, TongWeb8在卸载应用时&#xff0c;会尝试回收可能存在的内存泄露&#xff0c; 本质应该从应用方面解决。 解决办法&#xff1a; 若无法修改应用&#xff0c…

深度学习神经网络学习笔记-论文研读-transformer

摘要 优势序列转导模型基于复杂的循环或包括一个编码器和一个解码器的卷积神经网络。最好的表现良好的模型还通过attention 连接编码器和解码器机制。我们提出了一种新的简单的网络架构&#xff0c;Transformer&#xff0c; 完全基于注意力机制&#xff0c;省去了递归和卷积完…

【成都理工826】22年真题及解析

哈喽大家好&#xff0c;鉴于真题系列反馈很不错&#xff0c;我决定重启真题系列&#xff01;之前更新的22真题合集点击这里&#xff1a; 成都理工826信号与系统难度不是特别大&#xff0c;但是对计算的要求比较高。掌握好基础&#xff0c;计算细心是可以拿高分的。本套试题内容…

【我们一起60天准备考研算法面试(大全)-第四天 4/60(二叉搜索树与表达式树)】【每天40分钟,我们一起用60天准备 考研408-数据结构(笔试)】

专注 效率 记忆 预习 笔记 复习 做题 欢迎观看我的博客&#xff0c;如有问题交流&#xff0c;欢迎评论区留言&#xff0c;一定尽快回复&#xff01;&#xff08;大家可以去看我的专栏&#xff0c;是所有文章的目录&#xff09;   文章字体风格&#xff1a; 红色文字表示&#…

github搜索技巧笔记

一、了解 GitHub Watch按钮 Watch可以理解为关注的意思&#xff0c;默认情况下是Not watching&#xff0c;当选择Watch后&#xff0c;你会收到这个GitHub项目的所有动态。比如&#xff1a;有人发起pull request或者issue等。接收动态方式包括个人通知中心或者邮箱。 如果某个…

TypeScript - 函数(中)

目录 1、编写良好泛型函数的准则 1.1 向下推送类型参数 1.2 使用较少的类型参数 1.3 类型参数应出现两次 2、可选参数 3、回调中的可选参数 4、函数重载 5、重载签名和实现签名 6、写好重载 1、编写良好泛型函数的准则 编写泛型函数很有趣&#xff0c;并且很容易被类…

Kibana对索引库的操作(3)

这里我们主要是对索引库进行各种查询的操作,所以要提前准备一些数据 POST /leq/goods/3 {"title": "小米手机","images": "3.jpg","price": 4299,"stock": 200,"saleable": true,"subTitle":…

读发布!设计与部署稳定的分布式系统(第2版)笔记20_实例层之代码

1. 术语的定义 1.1. 服务 1.1.1. 指共同协作、以单元的形式对外提供功能的跨机器进程集合 1.1.2. 一个服务可以由多种可执行文件组成 1.1.3. 一个服务可能包含来自多个可执行文件的多个进程 1.1.4. 可能对外呈现单个IP地址&#xff0c;并在后台进行负载均衡 1.1.5. 可能有…

潇洒郎: bat文件与vbs文件, 执行命令行命令以及执行时无console, reg文件注册表

截图.bat start snippingtool 锁屏.bat rundll32.exe user32.dll,LockWorkStation redis.bat cd %~dp0 redis-server.exe redis.windows.conf vbs Set ws CreateObject("Wscript.Shell") ws.run "cmd /c C:/Python27/Scripts/ride.bat",vbhide 在上…

记录好项目D23

记录好项目 你好呀&#xff0c;这里是我专门记录一下从某些地方收集起来的项目&#xff0c;对项目修改&#xff0c;进行添砖加瓦&#xff0c;变成自己的闪亮项目。修修补补也可以成为毕设哦 本次的项目是个基于Springboot教务管理系统 一、系统介绍 这个项目是一个简单的教…

Slowhttptest----DoS攻击工具

Slowhttptest----DoS攻击工具 文章目录 slowhttptestcentos下安装slowhttptestUbuntu 20.04 LTS下安装slowhttptest使用 slowhttptest SlowHTTPTest 是依赖 HTTP 协议的慢速攻击 DoS 攻击工具。 工具的设计基本原理是服务器在请求完全接收后才会进行处理。 如果客户端的 HTT…

香港银行开个人账户应该选择哪个银行,附详细指南

香港现在陆续通关&#xff0c;开户是越来越方便&#xff0c;目前已经很多有账户需求的都过去开户了&#xff0c;那现在个人在香港银行开户的话可以开哪些银行呢&#xff1f;我列举了几个&#xff0c;可以参考下 汇丰个人户&#xff0c;当场开好拿卡使用 恒生个人户&#xff0…

商标注册 | 美国商标注册资料、流程、时间、方式一文详解

中国对外贸易市场中&#xff0c;美国是当仁不让的重要出口市场&#xff0c;这成为众多国内企业产品出海的先选目的地。产品出口也会涉及到一系列知识产权问题&#xff0c;而这个问题之一就是美国商标的注册。 美国的商标保护非常完善&#xff0c;它不仅可以阻止侵权商品进入美…

【ts】变量的类型注解

ts使用类型注解的好处&#xff1a;使用类型注解&#xff0c;可以使代码更加清晰和易于理解&#xff0c;在处理复杂的数据解构的时候&#xff0c;提高代码的可读性和可维护性。 在vue3中&#xff0c;类型注解是可选的&#xff0c;可用可不用&#xff0c;如果不用&#xff0c;vue…