第06讲:为何各大开源框架专宠 SPI 技术?

news2024/11/27 14:48:46

在此前的内容中,已经详细介绍了 SkyWalking Agent 用到的多种基础技术,例如,Byte Buddy、Java Agent 以及 OpenTracing 中的核心概念。本课时将深入介绍 SkyWalking Agent 以及 OAP 中都会使用到的 SPI 技术。

JDK SPI 机制

SPI(Service Provider Interface)主要是被框架开发人员使用的一种技术。例如,使用 Java 语言访问数据库时我们会使用到 java.sql.Driver 接口,每个数据库厂商使用的协议不同,提供的 java.sql.Driver 实现也不同,在开发 java.sql.Driver 接口时,开发人员并不清楚用户最终会使用哪个数据库,在这种情况下就可以使用 Java SPI 机制为 java.sql.Driver 接口寻找具体的实现。

当服务的提供者提供了一种接口的实现之后,需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,此文件记录了该 jar 包提供的服务接口的具体实现类。当某个应用引入了该 jar 包且需要使用该服务时,JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名,进行实现类的加载和实例化,最终使用该实现类完成业务功能。

下面通过一个简单的示例演示 JDK SPI 的基本使用方式,示例如下:

首先我们需要创建一个 Log 接口,来模拟日志打印的功能:

public interface Log {
    void log(String info);
}

接下来提供两个实现 —— Logback 和 Log4j,分别代表两个不同日志框架的实现,如下所示:

public class Logback implements Log {
    @Override
    public void log(String info) {
        System.out.println("Logback:" + info);
    }
}

public class Log4j implements Log {
    @Override
    public void log(String info) {
        System.out.println(“Log4j:” + info);
    }
}

在项目的 resources/META-INF/services 目录下添加一个名为 com.xxx.Log 的文件,这是 JDK SPI 需要读取的配置文件,具体内容如下:

com.xxx.impl.Log4j
com.xxx.impl.Logback

最后创建 main() 方法,其中会加载上述配置文件,创建全部 Log 接口实现的实例,并执行其 log() 方法,如下所示:

public class Main {
    public static void main(String[] args) {
        ServiceLoader<Log> serviceLoader = 
                ServiceLoader.load(Log.class);
        Iterator<Log> iterator = serviceLoader.iterator();
        while (iterator.hasNext()) {
            Log log = iterator.next();
            log.log("JDK SPI"); 
        }
    }
}
// 输出如下:
// Log4j:JDK SPI
// Logback:JDK SPI

JDK SPI源码分析

通过上述示例,我们可以看到 JDK SPI 的入口方法是 ServiceLoader.load()  方法,接下来我将对其具体实现进行深入分析。

在 ServiceLoader.load() 方法中,首先会尝试获取当前使用的 ClassLoader(获取当前线程绑定的 ClassLoader,查找失败后使用 SystemClassLoader),然后调用 reload() 方法,调用关系如下图所示:

在 reload() 方法中首先会清理 providers 缓存(LinkedHashMap 类型的集合),该缓存用来记录 ServiceLoader 创建的实现对象,其中 Key 为实现类的完整类名,Value 为实现类的对象。之后创建 LazyIterator 迭代器,该迭代器用于读取 SPI 配置文件并实例化实现类对象。

ServiceLoader.reload() 方法的具体实现,如下所示:

// 缓存,用来缓存 ServiceLoader创建的实现对象
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

public void reload() {
    providers.clear(); // 清空缓存
    lookupIterator = new LazyIterator(service, loader); // 迭代器
}

在前面的示例中,main() 方法中使用的迭代器底层就是调用了 ServiceLoader.LazyIterator  实现的。Iterator 接口有两个关键方法:hasNext() 方法和 next() 方法,这里的 LazyIterator 中的next() 方法最终调用的是其 nextService() 方法,hasNext() 方法最终调用的是 hasNextService() 方法,调用关系如下图所示:

首先来看 LazyIterator.hasNextService() 方法,该方法主要负责查找 META-INF/services 目录下的 SPI 配置文件,并进行遍历,大致实现如下所示:

private static final String PREFIX = "META-INF/services/";
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs  null) {
        // PREFIX前缀与服务接口的名称拼接起来,就是META-INF目录下定义的SPI配
        // 置文件(即示例中的META-INF/services/com.xxx.Log)
        String fullName = PREFIX + service.getName();
        // 加载配置文件
        if (loader 
 null)
            configs = ClassLoader.getSystemResources(fullName);
        else
            configs = loader.getResources(fullName);
    }
    // 按行SPI遍历配置文件的内容
    while ((pending == null) || !pending.hasNext()) { 
        if (!configs.hasMoreElements()) {
            return false;
        }
        // 解析配置文件
        pending = parse(service, configs.nextElement()); 
    }
    nextName = pending.next(); // 更新 nextName字段
    return true;
}

接下来,在 hasNextService() 方法中完成 SPI 配置文件的解析之后,再来看 LazyIterator.nextService() 方法,该方法负责实例化 hasNextService() 方法读取到的实现类,其中会将实例化的对象放到 providers 集合中缓存起来,核心实现如下所示:

private S nextService() {
    String cn = nextName;
    nextName = null;
    // 加载 nextName字段指定的类
    Class<?> c = Class.forName(cn, false, loader);
    if (!service.isAssignableFrom(c)) { // 检测类型
        fail(service, "Provider " + cn  + " not a subtype");
    }
    S p = service.cast(c.newInstance()); // 创建实现类的对象
    providers.put(cn, p); // 将实现类名称以及相应实例对象添加到缓存
    return p;
}

在 main() 方法中使用的迭代器的底层实现介绍完了,我们再来看一下其使用的真正迭代器,核心实现如下:

public Iterator<S> iterator() {
    return new Iterator<S>() {
        // knownProviders用来迭代 providers缓存
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {
            // 先走查询缓存,缓存查询失败,再通过 LazyIterator加载
            if (knownProviders.hasNext()) 
                return true;
            return lookupIterator.hasNext();
        }

        public S next() {
            // 先走查询缓存,缓存查询失败,再通过 LazyIterator加载
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }
        // 省略remove()方法
    };

JDK SPI 在 JDBC 中的应用

了解了 JDK SPI 实现的原理之后,我们来看实践中 JDBC 是如何使用 JDK SPI 机制加载不同数据库厂商的实现类。

JDK 中只定义了一个 java.sql.Driver 接口,具体的实现是由不同数据库厂商来提供的。这里以 MySQL 提供的 JDBC 实现包为例进行分析。

在 mysql-connector-java-*.jar 包中的 META-INF/services 目录下,有一个 java.sql.Driver 文件中只有一行内容,如下所示:

com.mysql.cj.jdbc.Driver

在使用 mysql-connector-java-*.jar 包连接 MySQL 数据库的时候,我们会用到如下语句创建数据库连接:

String url = "jdbc:xxx://xxx:xxx/xxx";
Connection conn = DriverManager.getConnection(url, username, pwd);

DriverManager 是 JDK 提供的数据库驱动管理器,其中的代码片段,如下所示:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

在调用 getConnection() 方法的时候,DriverManager 类会被 Java 虚拟机加载、解析并触发 static 代码块的执行,在 loadInitialDrivers() 方法中通过 JDK SPI 扫描 Classpath 下  java.sql.Driver 接口实现类并实例化,核心实现如下所示:

private static void loadInitialDrivers() {
    String drivers = System.getProperty("jdbc.drivers")
    // 使用 JDK SPI机制加载所有 java.sql.Driver实现类
    ServiceLoader<Driver> loadedDrivers = 
           ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
    String[] driversList = drivers.split(":");
    for (String aDriver : driversList) { // 初始化Driver实现类
        Class.forName(aDriver, true,
            ClassLoader.getSystemClassLoader());
    }
}

在 MySQL 提供的 com.mysql.cj.jdbc.Driver 实现类中,同样有一段 static 静态代码块,这段代码会创建一个 com.mysql.cj.jdbc.Driver 对象并注册到 DriverManager.registeredDrivers 集合中( CopyOnWriteArrayList 类型),如下所示:

static {
   java.sql.DriverManager.registerDriver(new Driver());
}

在 getConnection() 方法中,DriverManager 从该 registeredDrivers 集合中获取对应的 Driver 对象创建 Connection,核心实现如下所示:

private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
    // 省略 try/catch代码块以及权限处理逻辑
    for(DriverInfo aDriver : registeredDrivers) {
        Connection con = aDriver.driver.connect(url, info);
        return con;
    }
}

Dubbo 对 JDK SPI 的改进

通过前面的分析可以发现,JDK SPI 在查找具体实现类的过程中,需要遍历 SPI 配置文件中定义的所有实现类,该过程中会将这些实现类全部实例化。如果 SPI 配置文件中定义了多个实现类,而我们只需要其中一个实现类时,就会生成不必要的对象。

Dubbo 为了解决上述问题,自己设计了一套 SPI 实现,但是思想与 JDK SPI 机制类似。作为思路的扩展,这里简单介绍一下 Dubbo SPI 的实现原理(SkyWalking 使用是 JDK SPI 而不是 Dubbo SPI )。

首先,Dubbo 将 SPI 配置文件改成了 KV 格式,例如:

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

其中 key 就是一个简单的标记,当我们在为一个接口查找具体实现类时,可以指定 key 来选择具体实现,例如,这里指定 key 为 dubbo,Dubbo SPI 就知道我们要的是:org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol 这个实现类。

Dubbo SPI 核心实现是 ExtensionLoader(位于 dubbo-common 模块中的 extension 包中),功能类似于 JDK SPI 中的 java.util.ServiceLoader,其使用方式如下所示:

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class)
      .getExtension("dubbo");
// 很明显,在查找 Protocol这个接口的实现类时,还指定了"dubbo"这个key

ExtensionLoader.getExtensionLoader() 方法会根据接口类型从缓存中查找相应的 ExtensionLoader 实现,核心实现如下:

private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> 
    EXTENSION_LOADERS = new ConcurrentHashMap<>();

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    ExtensionLoader<T> loader = 
         (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) {
        EXTENSION_LOADERS.putIfAbsent(type,
               new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}

查找到接口对应的 ExtensionLoader 对象之后,会调用 getExtension() 方法,再根据传入的 key 查找相应的实现类,最终将其实例化后返回:

// 缓存,记录了 key到实现类对象Holder之间的映射关系
private final ConcurrentMap<String, Holder<Object>> cachedInstances = 
     new ConcurrentHashMap<>();

public T getExtension(String name) {
    Holder<Object> holder = getOrCreateHolder(name);
    Object instance = holder.get();
    if (instance  null) { // double-check防止并发问题
        synchronized (holder) {
            instance = holder.get();
            if (instance 
 null) {
                // createExtension()方法中完成了 SPI配置文件的查找以及实现类
                // 的实例化,具体实现与 JDK SPI原理类似,其中还会处理 Dubbo中
                // 自定义的一些注解,不再展开分析
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

总结

本课时首先介绍了 JDK SPI 机制的原理,并通过 Log 示例演示了 JDK SPI 的使用方式,然后深入到 ServiceLoader 的源码中分析了 JDK SPI 的实现方式,接下来介绍了 JDBC 4.0 如何使用 JDK SPI 机制加载数据库驱动类,最后介绍了 Dubbo 对 JDK SPI 的改进。


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

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

相关文章

Oracle-12c版本之后替换OCR磁盘组步骤

背景: 用户有一套Oracle12.2的RAC集群&#xff0c;在安装配置的时候&#xff0c;OCR磁盘只使用了单块磁盘external的模式&#xff0c;想替换成包含三块磁盘组成员normal模式的磁盘组 OCR磁盘组存储的对象: 在替换OCR磁盘之前&#xff0c;我们先确认需要迁移的OCR磁盘组存储的对…

图扑数字孪生助力智慧冷链园区实现大数据实时监控

前言 近年来&#xff0c;业界学者及企业就智慧冷链物流展开深入研究&#xff0c;2010 年 IBM 发布的《智慧的未来供应链》研究报告中提出智慧供应链概念&#xff0c;并由此延伸出智慧物流概念&#xff0c;即智慧物流是以信息化为依托并广泛应用物联网、人工智能、大数据、云计…

设置rocky Linux ip 与主机服务器处于同一网段内,并且能上网

第一步&#xff1a;查找主机服务器的 了解地址信息 第二步&#xff0c;设置rocky Linux 网络适配器连接&#xff0c;选择桥接模式 第三步&#xff1a;设置rocky Linux ip 第四步&#xff0c;设置完&#xff0c;重启Linux &#xff0c;验证ip是否修改过来&#xff0c;是否在同一…

【python】scikit-learn包:模型评估与优化

模型构建的目的 首先明确&#xff0c;模型拟合的目的&#xff1a; 不是对训练数据进行准确预测&#xff0c;而是对新数据进行准确预测 欠拟合 与 过拟合 欠拟合&#xff1a;可以通过训练数据及时发现&#xff0c;且可通过优化模型结果解决 过拟合&#xff1a;难以发觉&#x…

Golang基础----基于Goland编辑器快速体验Golang

【原文链接】Golang基础----基于Goland编辑器快速体验Golang &#xff08;1&#xff09;打开Goland&#xff0c;点击“New Project” &#xff08;2&#xff09;设置项目存放位置以及项目名&#xff0c;然后点击“Add SDK”&#xff0c;然后点击“Local” &#xff08;3&a…

vue3 的router跳转 - 页面同tab跳转和打开新tab跳转

vue3 的router跳转 - 页面同tab跳转和打开新tab跳转 vue3的路由基本知识 当前页需要的方法 主要是获得Vue Router实例暴露的一些方法&#xff0c;使用这些方法&#xff0c;进行路由操作引入 import { useRouter } from vue-router;调用 const router useRouter();目标页需…

Android jetpack Compose之约束布局

概述 我们都知道ConstraintLayout在构建嵌套层级复杂的视图界面时可以有效降低视图树的高度&#xff0c;使视图树扁平化&#xff0c;约束布局在测量布局耗时上比传统的相对布局具有更好的性能&#xff0c;并且约束布局可以根据百分比自适应各种尺寸的终端设备。因为约束布局确…

simulink simscape传感总结

1. 传感模块概述2. 可观测的传感量3. 传感模块3.1 运动传感模块3.1.1 旋转和平移细分旋转平移 3.2 力传感模块3.2.1 关节力和力矩细分 1. 传感模块概述 Simscape提供传感模块&#xff0c;通过改变模型的输入和输出&#xff0c;可以进行许多分析&#xff0c;比如可以进行机械臂…

中移链控制台对接4A平台功能验证介绍

中移链控制台具备单独的注册登录页面&#xff0c;用户可通过页面注册或者用户管理功能模块进行添加用户&#xff0c;通过个人中心功能模块进行用户信息的修改和密码修改等操作&#xff0c;因业务要求&#xff0c;需要对中移链控制台的用户账号进行集中管理&#xff0c;统一由 4…

2 文件IO

2.1 文件描述符 对于内核而言&#xff0c;所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开 一个现存文件或创建一个新文件时&#xff0c;内核向进程返回一个文件描述符。当读、写一个文件时&#xff0c; 用open或creat返回的文件描述符标识该文件&#xff0…

QT 网络编程之主机信息查询(QHostInfo 和 QNetworkInterface类)简介

Qt 网络模块提供了用于编写 TCP/IP 客户端和服务器端程序的各种类&#xff0c;如用于 TCP 通信的QTcpSocket 和 QTcpServer&#xff0c;用于 UDP 通信的 QUdpSocket&#xff0c;还有用于实现 HTTP、FTP 等普通网络协议的高级类如 QNetworkRequest&#xff0c;QNetworkReply 和Q…

【Redis】Redis中的5种基础数据类型详解

文章目录 1. Redis数据结构2. 基础数据结构详解2.1 String字符串2.2 List列表2.3 Set集合2.4 Hash散列2.5 Zset有序集合 1. Redis数据结构 对于Redis来说&#xff0c;所有的key&#xff08;键&#xff09;都是字符串&#xff0c;我们一般所讨论的Redis基础数据类型指的是存储的…

ref在Vue2、Vue3中的使用

文章目录 前言一、ref在Vue2中的用法二、ref在Vue3中的用法 前言 记录一下ref在Vue2与Vue3中的使用&#xff0c;ref可以获取DOM元素&#xff0c;也可以获取子组件的数据、方法。 一、ref在Vue2中的用法 给元素绑定一个ref&#xff0c;然后在js中通过this.$refs获取DOM。 ref命…

@PostConstruct注解和@PreDestroy注解

前言 Bean注解指定初始化和销毁的方法&#xff0c;也介绍了使用InitializingBean和DisposableBean来处理bean的初始化和销毁。JDK中还提供了两个注解能够在bean创建完成并且属性赋值完成之后执行一些初始化工作和在容器销毁bean之前通知我们进行一些清理工作。 1.PostConstru…

java 获取当前线程的方法

我们知道&#xff0c;线程是程序运行的基本单元&#xff0c;是程序中各进程之间通信的桥梁&#xff0c;一个线程的创建和销毁直接影响整个程序的运行效率。 我们在 Java中经常使用 Runnable接口来获取当前线程&#xff0c;获取线程的主要目的就是为了快速地启动进程。但是&…

一键导出ChatGPT聊天记录:让备份更简单

重要性&#xff1a; 备份ChatGPT的聊天记录同样非常重要&#xff0c;因为这些记录可能包含了您与ChatGPT的交互记录&#xff0c;这些记录可能包含了您的个人信息、兴趣爱好、偏好和其他敏感信息。以下是备份ChatGPT聊天记录的一些重要性&#xff1a; 防止数据丢失&#xff1a;…

FT2000+ qemu kvm 64C64G 通过频繁设置CPU online 状态导致虚拟机红旗操作系统假死测试用例

宿主机配置 虚拟机配置文件 <domain typekvm> //如果是Xen&#xff0c;则type‘xen’<name>redflag1</name> //虚拟机名称&#xff0c;同一物理机唯一<uuid>44748c15-7c00-4817-8724-675a27c3f821</uuid> //同一物理机唯一&#xff0c;可用uu…

北京 Meetup 预告 | 内含六个话题,满满干货,期待见面!

在春之末尾&#xff0c;夏之开始的5月&#xff0c;响应社区小伙伴们的呼唤&#xff0c;我们泽拓科技将在北京举办线下社区技术交流活动。 此次我们泽拓科技邀请了去哪儿网、ScaleFlux、美团、SphereEx的相关资深专家来分享他们的数据库探索与实践。我们真诚邀请数据库技术社区…

Day3_Springboot框架搭建

前面两天介绍了vue前端的主体框架&#xff0c;并完成了相关页面的部分设置&#xff0c;接下来开始介绍springboot后端开发的过程&#xff0c;手把手实践。包括项目的初始构建以及集成mybatis、mybatis-plus实现增删改查&#xff0c;分页查询&#xff0c;集成swagger-ui测试&…

Spring(4) Spring是如何使用三级缓存来解决循环依赖问题?

目录 1.什么是循环依赖&#xff1f;2.什么是Spring的循环依赖&#xff1f;3.三级缓存解决循环依赖3.1 假如只使用一级缓存3.2 假如使用二级缓存3.3 为什么要使用三级缓存 4.三级缓存解决循环依赖的局限性 1.什么是循环依赖&#xff1f; 假设我们有两个类 A 和 B&#xff0c;类…