九、SPI详解

本文详细介绍了JavaSPI的思想,它是服务发现的一种机制,用于框架扩展和组件替换。特别关注了JDBC4.0后如何通过SPI自动加载驱动,以及一个简单的SPI使用示例和源码分析。

1、SPI(Service Provider Interface)

重点是思想

        SPI只是一种思想,不是设计模式,是一种服务发现机制,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。

        API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。

        SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。  从使用人员上来说,SPI 被框架扩展人员使用。

2、介绍

熟悉JDBC的都知道,在 jdbc 4.0之前,在使用 DriverManager 获取 DB 连接之前,我们总是需要显示的实例化 DB 驱动。比如,对 mysql,典型的代码如下:

Connection conn = null;
PreparedStatement stmt = null;
try{
    // 注册 JDBC driver, 加载数据库驱动class
    Class.forName("com.mysql.jdbc.Driver");
    // 获取连接
    conn = DriverManagger.getConnection(DB_URL,USER,PASSWD);
    // 执行一条sql
    stmt = conn.prepareStatement();
    String sql = "select * from t_user";
    ResultSet rs = stmt.executeQuery(sql);
    // 数据解包
    while(ts.next()){
        // 根据列名获取列值
        // ...
    } catch(SQLException se) {
        // ...
    } final {
        try {
            if (stmt!=null) stmt.close();
        } catch(Exception e) {/*ignored*/}
        try {
            if (conn!=null) conn.close();
        } catch(Exception e) {/*ignored*/}
    }
}

        JDBC的开始总是需要通过 Class.forName 显式加载驱动,否则将找不到对应的DB驱动。但是 JDBC4.0 开始,这个显式的初始化不再是必选项了,它存在的意义只是为了向上兼容。

        那么JDBC4.0之后,我们的应用是如何找到对应的驱动呢?

        答案就是 SPI(Service Provider Interface)。Java在语言层面为我们提供了一种方便地创建可扩展应用的途径。SPI提供了一种JVM级别的服务发现机制,我们只需要按照SPI的要求,在jar包中进行适当的配置,jvm就会在运行时通过懒加载,帮我们找到所需的服务并加载。如果我们一直不使用某个服务,那么它不会被加载,一定程度上避免了资源的浪费。

3、一个简单的例子

通过一个ServiceLoader.load(xx.class)简单的例子看看如何最小化构建一个基于SPI的服务。

3.1 、添加一个interface 或 abstract class

        Java SPI 并没有强制必须使用 interface 或 abstract class,完全可以将 class 注册为 SPI 注册服务,但是作为可扩展服务,使用 interface 或 abstract class 是一个好习惯。

        在包 “cn.spi”中定义一个接口Animal:

package cn.spi;

public interface Animal {
    void eat();
    void sleep();
}

3.2、提供实现类

package cn.spi.impl;
import cn.spi.Animal;

public class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog is eating");
    }

    @Override
    public void sleep() {
        System.out.println("Dog is sleeping");
    }
}

3.3、服务注册

在main目录下创建目录 "resources/META-INF/services"

mkdir -p resources/META-INF/services

再在该目录下创建以接口Animal全限定名为名(cn.spi.Animal)的配置文件,文件内容为该接口的实现类的全限定名,即

"cn.spi.impl.Dog"

完成此步骤后,在当前maven项目的 src/main/resources/META-INF/services下有这么一个配置文件:"cn.spi.Animal",并且它的内容为"cn.spi.impl.Dog"。

注意本步骤的要点:

  • 必须放在JAR包或项目的指定路径,即 META-INF/services 下
  • 必须以服务的全限定名命名配置文件,比如本例中,配置文件必须命名为 cn.spi.Animal,java会根据此名进行服务查找
  • 内容必须是一个实现类的全限定名,如果要注册多个实现类,按行分割。注释以#开头。

3.4、增加单元测试:

package cn.spi

import org.junit.Test;
import java.util.ServiceLoader;

public class AnimalTest {
    @Test
    public void animalTest() {
        ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class);
        for(Animal animal: animals) {
            animal.eat();
            animal.sleep();
        }
    }
}

执行结果:

Dog is eating

Dog is sleeping

可见,虽然我们没有显式使用Animal的实现类Dog,但是java帮我们自动加载了该实现类。

4、源码分析

接下来从代码层面看看SPI都为我们做了什么。首先看看 java.util.ServiceLoader 的实现。我们看到 ServiceLoader 使用非常简单,只需要调用一个静态方法 load 并以要加载的服务的父类(通常是一个interface或abstract class)作为参数,jvm就会帮我们构建好当前进程中所有注册到 META-INF/services/[service full qualified class name] 的服务。

//自动从META-INF/services/[service full qualified class name] 构建
ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class);

4.1、创建ServiceLoader实例

        下面是构造ServiceLoader实例的相关代码。ServiceLoader必须通过静态方法load(Class service)的方式加载服务,默认会使用当前线程的上下文class loader对象。构造完ServiceLoader后,ServiceLoader实例并不会立刻扫描当前进程中的服务实例,而是创建一个LazyIterator懒加载迭代器,在实际使用时再扫描所有jar包找到对应的服务。懒加载迭代器被保存在一个内部成员lookupIterator中。

public final class ServiceLoader<S> implements Iterable<S>{
    // 保存懒加载迭代器
    private LazyIterator lookupIterator;
    
    // 通过service的class创建ServiceLoader实例,默认使用上下文classloader
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 从线程上下文中获取类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    
    // 构建ServiceLoader实例 service:接口class, loader:类加载器
    public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
        return new ServiceLoader<>(service, loader);
    }
    
    // ServiceLoader 构造函数,私有类型,必须通过ServiceLoader.load(Class<?>)静态方法来创建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();
    }
    
    // 重新load指定serivice的实现。通过LazyIterator实现懒加载。
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
    ...
}

4.2、服务加载和遍历

        在调用了ServiceLoader animals = ServiceLoader.load(Animal.class)之后,ServiceLoader会返回一个Animal.class类型的迭代器,但此时在ServiceLoader内部只是创建了一个 LazyIterator,而不会真正通过classloader在classpath中寻找相关的服务实现。

        

public final class ServiceLoader<S> implements Iterable<S>
{
    ...
    // 缓存的service provider,按照初始化顺序排列。
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
   
     // 当前的LazyIterator迭代器指针,服务懒加载迭代器
    private LazyIterator lookupIterator;

    ...
    // 创建ServiceLoader迭代器,隐藏了LazyIterator的实现细节
    public Iterator<S> iterator() {
        return new Iterator<S>() {
            // 创建Iterator迭代器时的ServiceLoader.providers快照,
            // 因此在首次迭代时,iterator 总是会通过LazyIterator进行懒加载
            Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();

            public boolean hasNext() {
                // 如果已经扫描过,则对providers进行迭代;
                if (knownProviders.hasNext())
                    return true;
                // 如果没有扫描过,则通过lookupIterator进行扫描和懒加载
                return lookupIterator.hasNext();
            }

            public S next() {
                // 如果已经扫描过,则对providers进行迭代;
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                // 如果没有扫描过,则通过lookupIterator进行扫描和懒加载
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }
    ...
}

ServiceLoader的迭代器很简单:

  • 未进行迭代操作时,不对jar包作任何扫描
  • 首次迭代时,因为ServiceLoader.providers中没有任何缓存,总是会通过LazyIterator进行懒加载,并将service实现的全限定名与加载的service实例作为key-value缓存到ServiceLoader.providers中。
  • 之后再进行迭代时,总是在ServiceLoader.providers中进行。

4.3、懒加载迭代器LazyIterator

懒加载迭代器LazyIterator主要实现以下功能:

  • 首次迭代时,通过ClassLoader.getResources(String)获得指定services文件的URL集合
  • 如果是首次遍历懒加载器,或者对上一个URL内容解析获得的service实现类集合完成了迭代,则从configs中取下一个services文件URL进行解析,按行获得具体的service实现类集合,并进行迭代。
  • 对当前URL中解析得到的实现类集合进行迭代,每次返回一个service实现类。

下面是LazyIterator的源码及注释:

public final class ServiceLoader<S> implements Iterable<S>
{
    private static final String PREFIX = "META-INF/services/";
    ...

    // 懒加载迭代器实现
    private class LazyIterator implements Iterator<S>{
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        // 当前service配置文件的内容迭代器
        // 即对services进行遍历,取出一个services配置文件,再对该文件按行解析,每行代表一个具体的service实现类,pending是某个services配置文件中service实现类的迭代器
        Iterator<String> pending = null;
        String nextName = null;
        
        // 构造懒加载迭代器
        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            // 首次迭代时,configs为空,尝试通过classloader获取名为:
            // "META-INF/services/[服务全限定名]"的所有配置文件
            if (configs == null) {
                try {
                    // 注意fullName的定义:"META-INF/services/[服务全限定名]"
                    String fullName = PREFIX + service.getName();
                    // 通过ClassLoader.getResources()获得资源URL集合
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            // 如果pending为空,或者pending已经迭代到迭代器末尾,则尝试解析下一个services配置文件
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            // 对当前pending内容进行遍历,每一项代表services的一个实现类
            nextName = pending.next();
            return true;
        }
    }

    ...
}

最后,附上parse及parseLine的代码,可以发现,parseLine中会对服务实现类进行去重,所以在一个或多个services配置文件中配置多次的服务实现类只会被处理一次。

public final class ServiceLoader<S> implements Iterable<S>{
    ...
    // 按行解析给定配置文件。如果解析出的服务实现类没有被其他已解析的配置文件配置过,则通过参数nams返回给parse方法
    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names) throws IOException, ServiceConfigurationError {
        String ln = r.readLine();
        if (ln == null) {
            return -1;
        }
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            // 去重,防止重复配置服务,每个服务实现类只会被解析一次
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }

    // 解析指定的作为SPI配置文件的URL的内容
    private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
        return names.iterator();
    }
    ...
}

5、JDBC中对SPI的使用

最后,以JDBC为例,看一个SPI的实际使用场景。在文章开始,我们提到过,JDBC4.0之前,我们总是需要在业务代码中显式地实例化DB驱动实现类:

Class.forName("com.mysql.jdbc.Driver");

为什么JDBC4.0之后不需要了呢?

答案就在下面的代码中。在系统启动时,DriverManager 静态初始化时会通过 ServiceLoader 对所有jar包中被注册为 java.sql.Driver 服务的驱动实现类进行初始化,这样就避免了上面通过Class.forName手动初始化的繁琐工作。DriverManager.getConnection()方法会触发DriverManager的初始化。

public class DriverManager {

    // JDBC驱动注册中心,所有加载的JDBC驱动都注册在该CopyOnWriteArrayList中
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

    ...

    /* 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");
    }

    private static void loadInitialDrivers() {
        // 如果通过jdbc.drivers配置了驱动,则在本方法最后进行实例化
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                    public String run() {
                        return System.getProperty("jdbc.drivers");
                    }
                    });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                // 通过ServiceLoader加载所有通过SPI方式注册的"java.sql.Driver"服务
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                // 遍历ServiceLoader实例进行强制实例化,因此除了遍历不做任何其他操作
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                    // Do nothing
                }
                return null;
                }
            }
        );

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        // 强制加载"jdbc.driver"环境变量中配置的DB驱动
        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驱动为例看看驱动实例化时做了什么:

package com.mysql.jdbc;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            // 向 DriverManager 注册自己
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     *
     * @throws SQLException
     *             if a database error occurs.
     */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

再看看mysql驱动jar包中对service的配置:

因此,只要某个驱动以这种方式被引用并被上下文class loader加载,那么该驱动就会通过SPI的方式被自动发现和加载。实际使用时,Driver.getDriver(url)会通过DB连接url获取到正确的驱动并建立与DB的连接。

<think>首先,用户要求使用中文回答问题。主题是“spi详解”,所以我需要用中文详细解释SPI(Serial Peripheral Interface)。 SPI是一种串行通信协议,常用于微控制器和外设之间的通信,如传感器、存储器等。我需要涵盖其基本概念、工作原理、优缺点等。 关键点: - **全称**:Serial Peripheral Interface(串行外设接口) - **特点**:全双工、同步通信、主从模式 - **信号线**: - SCLK:串行时钟(由主机提供) - MOSI:主机输出,从机输入 - MISO:主机输入,从机输出 - SS/CS:片选信号(选择从机) - **工作方式**:主机控制时钟,数据在时钟边沿传输。 - **模式**:有四种模式,取决于时钟极性和相位(CPOL和CPHA)。 - **优势**:高速、简单、无起始/停止位。 - **劣势**:需要更多引脚、无错误检测。 回答结构: 1. 介绍SPI是什么。 2. 解释核心组件和信号。 3. 描述数据传输过程。 4. 讨论模式(CPOL和CPHA)。 5. 优缺点。 6. 应用场景。 在回答末尾,我必须添加“§§相关问题§§”,然后列出3-5个相关问题。这些问题要基于用户的问题和我的回答。 相关问题示例: 1. SPI与其他协议(如I2C、UART)的区别是什么? 2. 如何在实际项目中使用SPI? 3. SPI的四种模式如何影响通信? 4. SPI在嵌入式系统中的应用案例? 5. 如何解决SPI通信中的常见问题? 确保输出格式: - 回答内容用中文。 - 结束时输出“§§相关问题§§”,然后直接列出问题(1. ... 2. ... 3. ...)。 - 不要换行分散;问题列表要紧凑。 完整输出: - 先写回答。 - 然后“§§相关问题§§” - 接着问题列表。 现在,构建回答。</think>### SPI详解 SPI(Serial Peripheral Interface,串行外设接口)是一种同步串行通信协议,广泛用于嵌入式系统和微控制器中,以实现主设备(如微处理器)与从设备(如传感器、存储器芯片)之间的高速数据交换。它由Motorola公司开发,具有全双工通信能力,支持主从模式。以下是SPI的核心内容: #### 1. **基本组成** - **信号线**: - **SCLK**(Serial Clock):时钟信号,由主机提供,控制数据传输速率。 - **MOSI**(Master Out Slave In):主机输出、从机输入数据线。 - **MISO**(Master In Slave Out):主机输入、从机输出数据线。 - **SS/CS**(Slave Select/Chip Select):片选信号,由主机控制,用于选择特定从机(允许多个从机共享同一总线)。 - 典型连接:一个主机可连接多个从机,但每个从机需要独立的SS线。 #### 2. **工作原理** - **同步通信**:数据传输基于SCLK时钟信号,主机在时钟边沿(上升沿或下降沿)触发数据采样。 - **全双工传输**:数据同时在MOSI和MISO线上传输,主机发送数据给从机的同时接收从机数据。 - **传输过程**: 1. 主机拉低目标从机的SS线,激活通信。 2. 主机生成SCLK信号,并在每个时钟周期通过MOSI发送一位数据。 3. 从机在同步的时钟边沿通过MISO返回数据。 4. 传输完成后,主机拉高SS线释放从机。 - **数据帧**:通常以8位或16位为单位传输,无起始/停止位,效率高。 #### 3. **模式配置** SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)决定: - **CPOL**:时钟空闲状态(0为低电平,1为高电平)。 - **CPHA**:数据采样时机(0为第一个时钟边沿采样,1为第二个时钟边沿采样)。 - **模式表**: | 模式 | CPOL | CPHA | 描述 | |------|------|------|------| | 0 | 0 | 0 | 时钟空闲低电平,数据在上升沿采样 | | 1 | 0 | 1 | 时钟空闲低电平,数据在下降沿采样 | | 2 | 1 | 0 | 时钟空闲高电平,数据在下降沿采样 | | 3 | 1 | 1 | 时钟空闲高电平,数据在上升沿采样 | - 设备必须配置相同模式才能通信。 #### 4. **优缺点** - **优点**: - 高速传输:速率可达MHz级别(如10-100MHz),远高于I2C。 - 简单高效:无复杂协议开销,支持全双工。 - 灵活性:可自定义数据帧长度和时钟频率。 - **缺点**: - 引脚占用多:每个从机需独立SS线,限制多设备扩展。 - 无错误检测:缺乏内置机制(如CRC),易受干扰。 - 主从依赖:从机无法主动发起通信。 #### 5. **应用场景** - 常见于SD卡、OLED显示屏、温度传感器(如DS18B20)、Flash存储器(如W25Q系列)等。 - 示例代码(Python伪代码,模拟SPI通信逻辑): ```python def spi_transfer(host_data, mode=0): # 初始化SPI设置(简化版) cpol = mode >> 1 # 提取CPOL cpha = mode & 1 # 提取CPHA # 模拟数据传输 miso_data = 0 for bit in range(8): # 根据模式生成时钟边沿 if cpha == 0: # 第一个边沿采样 miso_bit = read_miso() # 读取从机数据 # 主机发送数据位 set_mosi((host_data >> bit) & 1) toggle_sclk() # 切换时钟 if cpha == 1: # 第二个边沿采样 miso_bit = read_miso() miso_data |= (miso_bit << bit) return miso_data # 返回从机数据 ``` SPI是嵌入式开发的基础协议,理解其机制有助于高效设计硬件接口。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值