SPI 机制详解

news2024/12/24 20:44:40

SPI 全称为 Service Provider Interface ,它是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架拓展提供了可能,比如在Dubbo,JDBC中都使用到了SPI机制。

我们先通过如下的简单案例看看它是怎么使用的。

简单案例

首先,我们需要定义一个接口:

public interface SPIService {

    void doSomething();

}

然后,定义两个实现类,随便输出一句话即可:

public class SpiServiceImpl1 implements SPIService{
    @Override
    public void doSomething() {
        System.out.println("一号选手已就位!");
    }
}

public class SpiServiceImpl2 implements SPIService{
    @Override
    public void doSomething() {
        System.out.println("二号选手已就位!");
    }
}

最后需要在 ClassPath路径下配置相关文件。文件名字是接口的全限定类名,内容是实现类的全限定类名,多个实现类用换行符分隔即可。

文件路径如下:
在这里插入图片描述

文件内容如下:

com.example.spidemo.spi.SpiServiceImpl1
com.example.spidemo.spi.SpiServiceImpl2

以上步骤执行完成后,我们就可以通过ServiceLoader.load或者Service.providers方法拿到实现类的实例。其中,ServiceLoader.load包位于java.util.ServiceLoader下,而Service.providers包位于 sun.misc.Service;下。

两种方式的输出结果都是相同的。

public class Test {

    public static void main(String[] args) {
        Iterator<SPIService> providers = Service.providers(SPIService.class);
        ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);

        while(providers.hasNext()) {
            SPIService ser = providers.next();
            ser.doSomething();
        }
        System.out.println("--------------------------------");
        Iterator<SPIService> iterator = load.iterator();
        while(iterator.hasNext()) {
            SPIService ser = iterator.next();
            ser.doSomething();
        }
        for(SPIService db:load){
            db.doSomething();
        }
    }

}

源码分析

我们这里以ServiceLoader.load为例,通过源码看看它立马到底做了什么。

  • ServiceLoader

    首先,我们先来看看 ServiceLoader的类结构:

    从配置文件的路径我们就可以明白为啥我们要在ClassPath路径下创建相关路径,因为这是约定好的,我们要遵从约定。

    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;
    
        // 已加载的服务类集合
        // Cached providers, in instantiation order
        private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    
        // 内部类 真正加载服务类
        // The current lazy-lookup iterator
        private LazyIterator lookupIterator;
    }
    
  • load

    load 方法创建了一些属性,重要的实例化了内部类 LazyIterator 。最后返回 ServiceLoader的实例。

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        // 要加载的接口
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        // 类加载器
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        // 访问控制器
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
    
    
    public void reload() {
       		//	先清空
            providers.clear();
       		// 实例化内部类
            lookupIterator = new LazyIterator(service, loader);
    }
    
  • 查找实现类

    查找实现类和创建实现类的过程都在 LazyIterator 中完成。当我们调用iterator.hasNext() iterator.next()的时候,实际上都是在调用LazyIterator相应的方法。

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

    我们这里直接看 hasnext方法,它最终会调用到hasNextService

    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);
        }
    }
    
    private boolean hasNextService() {
        // 第二次调用的时候已经解析完成了,直接返回即可
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            try {
                // META-INF/services/ 加上接口的全限定名 对应 文件名
                String fullName = PREFIX + service.getName();
                // 将文件路径转换成URL对象
                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;
            }
            // 解析 URL 内容 ,将其返回
            pending = parse(service, configs.nextElement());
        }
        // 拿到第一个实现类的类名
        nextName = pending.next();
        return true;
    }
    
  • 创建实例

    调用next方法的时候实际调用到的是LazyIteratornextService方法。它通过反射的方式创建类的实例并返回。

    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        // 全限定类名
        String cn = nextName;
        nextName = null;
        // 创建类的 class 对象
        Class<?> c = null;
        try {
            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 {
            // 通过 newINstance 实例化
            S p = service.cast(c.newInstance());
            // 放入集合 返回实例
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                 "Provider " + cn + " could not be instantiated",
                 x);
        }
        throw new Error();          // This cannot happen
    }
    

看到这里,相信大家已经搞清楚SPI的机制了。这是 jdk 中的 spi机制,springboot 的spi机制稍有不同,区别在于读取录取以及文件内容不同,这部分在后续自动装配原理的时候在说明。

JDBC中的应用

我们开头说,SPI机制为很多框架的拓展提供了可能,其实JDBC就应用到了这一机制。使用JDBC的步骤无非就是加载驱动程序,然后通过DriverManager获取数据库连接。那么它是如何分别是那种数据库的呢?答案就在SPI中。

  • 加载

    我们来看看DriverManager类,它在静态代码块立马做了一件比较重要的事情。很明显,它以及通过SPI机制把数据库驱动连接初始化了。

    public class DriverManager {
        /* Prevent the DriverManager class from being instantiated. */
        private DriverManager(){}
    
    
        /**
         * Load the initial JDBC drivers by checking the System property
         * jdbc.properties and then use the {@code ServiceLoader} mechanism
         */
        static {
            loadInitialDrivers();
            println("JDBC DriverManager initialized");
        }
    }
    

    具体过程还得看loadInitialDrivers方法,它在里面找的是Driver接口的服务类,所以它的文件路径就是:META-INF/services/java.sql.Driver。

    private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
    
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
    
                // 这里很明显的可以看出它要加载Driver接口的服务,Driver接口的包为:java.sql.Driver
                // 所以这里要找的就是META-INF/services/java.sql.Driver文件
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
    
                try{
                    // 查询之后创建对象
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
    
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
    
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
    

    那么,这个文件在哪呢?我们看看MYSQL的jar包即可,一眼看出就是通过SPI机制进行拓展。

在这里插入图片描述

  • ·创建实例

    上一步已经找到了MySQL中的com.mysql.cj.jdbc.Driver全限定类名,当调用next方法的时候就会创建实例。mysql的实现类里面就完成了一件事,想DriverManager注册自身的实例。

    public class Driver extends NonRegisteringDriver implements java.sql.Driver {
        public Driver() throws SQLException {
        }
    
        static {
            try {
                // 调用注册方法往registerDrivers集合中加入实例
                DriverManager.registerDriver(new Driver());
            } catch (SQLException var1) {
                throw new RuntimeException("Can't register driver!");
            }
        }
    }
    
  • 创建 Connection

    DriverManager.getConnection方法就是创建连接的地方,它通过循环已注册的数据库驱动程序,调用其connect方法,获取连接并返回。

    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }
    
        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }
    
        println("DriverManager.getConnection(\"" + url + "\")");
    
        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;
    
        // 循环已经注册的Driver实例
        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    // 创建连接
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
    
            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }
    
        }
    
        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }
    
        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }
    

手写自己的数据库连接驱动

既然我们已经知道JDBC是怎么创建数据库连接的,那我们是不是可以创建一个我们自己的 Driver 文件。

我们首先创建实现类,继承自MySQL中的 NonRegisteringDriver,还要实现java.sql.Driver接口。这样在调用connect的时候就会调用到此类,但实际的创建过程还是靠MYSQL完成。

public class MyDriver extends NonRegisteringDriver implements Driver {

    // 仿照 mysql 的Driver , 先进行注册
    static {
        try {
            DriverManager.registerDriver(new MyDriver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    public MyDriver() throws SQLException {
    }

    // 重写连接方法
    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        System.out.println("准备创建数据库连接 url:" + url);
        System.out.println("JDBC 配置信息:" + info);

        info.setProperty("user","root");
        Connection connect = super.connect(url, info);
        System.out.println("数据库连接创建完成:" + connect.toString());
        return connect;
    }
}

然后按照 SPI 的配置,我们去classpath下创建相关文件 java.sql.Driver (接口的全限定类名):

内容为实现类的全限定类名。
在这里插入图片描述

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

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

相关文章

go return返回值屏蔽

前言 最近需要写一个云环境的可执行程序&#xff0c;一般使用go语言&#xff0c;毕竟GC原生运行&#xff0c;结合了不需要回收指针的能力和原生运行&#xff0c;但是在程序返回时&#xff0c;笔者看到一个sdk的源码懵了&#xff0c;返回的数据居然可以隐式返回。 准备 go 版本…

机器学习8线性回归法Linear Regression

文章目录一、线性回归算法简介典型的最小二乘法的问题目标&#xff1a;具体怎么推此处省略二、简单线性回归的实现三、向量化运算一、线性回归算法简介 1.解决回归问题&#xff1b; 2.思想简单&#xff0c;实现容易&#xff1b; 3.是许多强大的非线性模型的基础&#xff1b; 4…

ESP32——WEB服务程序测试(基于官方示例restful_server)

一、简介 基于官方示例restful_server创建一个新工程。 参考1&#xff1a; 官方说明 参考2&#xff1a; ES32 RESTful_server实验_NULL_1969的博客-CSDN博客 二、编译下载运行工程 直接编译运行&#xff0c;出现下面两个错误。 2.1 OCD调试错误 esp_semihost: OpenOCD i…

文本生成客观评价指标总结(附Pytorch代码实现)

前言&#xff1a;最近在做文本生成的工作&#xff0c;调研发现针对不同的文本生成场景&#xff08;机器翻译、对话生成、图像描述、data-to-text 等&#xff09;&#xff0c;客观评价指标也不尽相同。虽然网络上已经有很多关于文本生成评价指标的文章&#xff0c;本博客也是基于…

[附源码]JAVA毕业设计计算机组成原理教学演示软件(系统+LW)

[附源码]JAVA毕业设计计算机组成原理教学演示软件&#xff08;系统LW&#xff09; 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。…

PCB元件创建

目录 一&#xff1a;创建元件基本流程 1.1.创建一个原理图库 1. 2.创建元件 1.3绘制 1.4放置管脚 二&#xff1a; 元件创建 2.1电容类元件创建 2.2.电感类元件 2.3.电阻类元件 2.4LED元件 2.5按键元件 2.6芯片类元件创建 2.6.1修改栅格颜色 2.6.2阵列粘贴 2.7接插…

kubernetes Service详解

文章目录Service介绍Service类型Endpoint负载分发策略HeadLiness类型的ServiceNodePort类型的ServiceLoadBalancer类型的ServiceExternalName类型的Servicengress介绍Service介绍 在kubernetes中&#xff0c;pod是应用程序的载体&#xff0c;我们可以通过pod的ip来访问应用程序…

python Threads and ThreadPools

在之前的文章解释了线程和锁的相关事项&#xff0c;这里准备三篇文章分别介绍下线程和线程池&#xff0c;进程和进程池&#xff0c;已经携程的概念 python Threads and ThreadPoolspython Process and ProcessPolls 本文中重点介绍下线程和线程池的概念。每个python程序都是一…

毕业设计-基于机器视觉的安全帽佩戴识别-yolo-python

目录 前言 课题背景和意义 实现技术思路 实现效果图样例 前言 &#x1f4c5;大四是整个大学期间最忙碌的时光,一边要忙着备考或实习为毕业后面临的就业升学做准备,一边要为毕业设计耗费大量精力。近几年各个学校要求的毕设项目越来越难,有不少课题是研究生级别难度的,对本科…

mysql监控sql执行情况

要想进阶针对mysql学习乃至掌握mysql调优的基本技能&#xff0c;监控mysql的执行情况必不可少。就像我们的代码&#xff0c;如果不能debug&#xff0c;想要进行调优排错&#xff0c;难度将会大大增加。 所以今天我们就来讲解如何监控mysql的sql执行情况 show profile指令什么是…

Pinely Round 1 (Div. 1 + Div. 2)

比赛链接&#xff1a;Dashboard - Pinely Round 1 (Div. 1 Div. 2) - Codeforces A&#xff1a;思维 题意&#xff1a;定义了一个序列&#xff0c;给定了三个整数n&#xff0c;a&#xff0c;b。问能否构造两个长度为n的序列&#xff0c;使得它们的最长前缀的长度为a&#xf…

Mongo非关系型数据库

mongo三个概念 如何下载MongoDB 参考(5条消息) mongodb免安装配置_剑客916的博客-CSDN博客 (5条消息) MongoDB的安装配置教程&#xff08;很详细&#xff0c;你想要的都在这里&#xff09;_狮子座的男孩的博客-CSDN博客_mongodb配置 下载地址 Download MongoDB Community Se…

PDF Shaper Pro v12.8 全能PDF工具箱中文版

PDF Shaper 是一款实用的全能PDF工具箱&#xff01;这款PDF转换器包含了很多非常实用的PDF工具&#xff0c;可以轻松的把 PDF 转成 Word&#xff0c;PDF 转图像&#xff0c;PDF 加密等等。它还可以合并&#xff0c;分割&#xff0c;加密和解密 PDF&#xff0c;图像转换为 PDF&a…

Python学习笔记-序列

用于记述python中对于序列的应用&#xff0c;包括列表、元组、字典、集合、字符串等。 1.序列

我看世界杯

目录 写在前面 正文 第一次看世界杯 我看重的球队 写在最后 写在前面 说实话&#xff0c;第一次接触足球还是在小学阶段&#xff0c;现在已经记不清那是在哪里搞来的&#xff0c;反正是挺破烂的&#xff0c;外面的五边形布料都已经脱落&#xff0c;都能露出里面的布料&…

java EE初阶 — volatile关键字保证内存可见性

文章目录1.volatile保证内存可见性1.1 如何保证内存可见性1.2 java 内存模型&#xff08;JMM&#xff09;2.volatile 不保证原子性1.volatile保证内存可见性 先来看一段代码 package thread;import java.util.Scanner;class MyCounter {public int flag 0; }public class Th…

Qt实现编辑框失去焦点隐藏功能

今天来为大家分享一个小功能&#xff0c;首先看实现的效果吧~ 功能讲解&#xff1a; QLineEdit控件进行文本编辑&#xff0c;点击保存按钮后&#xff0c;隐藏编辑框和保存按钮&#xff0c;仅展示编辑内容&#xff0c;当鼠标点击空白处时&#xff0c;同样隐藏编辑框、隐藏保存按…

Dubbo前后端分离监控中心搭建

监控中心&#xff0c;因为监控中心现在前后端分离&#xff0c;所以相比与老版有变动&#xff0c;首先下载压缩包 官网: https://github.com/apache/dubbo-admin/tree/develop 下载安装包&#xff0c;使用IDEA打开解压好的dubbo-admin-develop文件夹&#xff0c;稍等片刻让Mav…

CTFHUB-web-RCE

eval执行 <?php if (isset($_REQUEST[cmd])) {eval($_REQUEST["cmd"]); } else {highlight_file(__FILE__); } ?> 看下当前目录 ?cmdprint_r(getcwd());可以执行命令 print_r(system(ls));查看上级目录?cmdprint_r(system(ls ../../../)); 直接cat flag?c…

使用模拟退火(SA)和Matlab的车辆路径问题(VRP)(Matlab代码实现)

&#x1f352;&#x1f352;&#x1f352;欢迎关注&#x1f308;&#x1f308;&#x1f308; &#x1f4dd;个人主页&#xff1a;我爱Matlab &#x1f44d;点赞➕评论➕收藏 养成习惯&#xff08;一键三连&#xff09;&#x1f33b;&#x1f33b;&#x1f33b; &#x1f34c;希…