Java 基础知识之 SPI

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 什么是 SPI?SPI,全称 Service Provider Interface,即服务提供者接口,是Java中用于提供给第三方实现的接口。

什么是 SPI?

SPI,全称 Service Provider Interface,即服务提供者接口,是Java中用于提供给第三方实现的接口。


如何使用SPI?

SPI 符合面向接口编程的范式,使用接口的用户无需了解底层的实现即可直接使用接口所提供的服务。使用 SPI 需要遵守如下的约定。


服务提供者完成接口的实现,实现类存在一个不带参数的构造器。

服务提供者在 classpath 下的 META-INF/services 目录下创建和接口名称一致的文件,文件的内容为接口的实现类全限定名,如果有多个则每行为一个实现类的全限定名,字符 # 及之后的字符串为注释。

接口的使用者使用 java.util.ServiceLoader#load(java.lang.Class<S>) 获取接口实现类。

SPI 示例

SPI的使用场景有多种,如JDBC,Spring,Dubbo,以JDBC为例,MySQL JDBC 驱动下可以看到 java.sql.Dirver的配置文件。
image.png


那么,驱动如何被加载的呢?当我们调用 java.sql.DriverManager#getConnection(java.lang.String)方法获取java.sql.Connection时会触发 DriverManager 类的加载。DriverManager 类加载时会执行静态代码块的代码。查看 DriverManager 类的相关源码如下。

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    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() {
        //在此处使用 ServiceLoader 加载 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);
            }
        }
    }
}

可以看到 DriverManager 类被加载到 JVM 时会使用 java.util.ServiceLoader#load(java.lang.Class<S>)方法加载SPI Driver的实现类,然后循环使用 java.lang.Class#forName(java.lang.String, boolean, java.lang.ClassLoader) 方法加载驱动。


ServiceLoader 源码浅析

SPI 的实现类使用 java.util.ServiceLoader#load(java.lang.Class<S>)方法获取,ServiceLoader的源码只有500多行,底层使用了java.lang.ClassLoader#getResources或者 java.lang.ClassLoader#getSystemResources 获取 classpath 下的 SPI 资源文件,然后进行逐行解析文件内容获取 SPI 实现类,并且使用了懒加载的方式。获取 classpath 下资源文件的方式参见 Java中如何获取classpath下资源文件?

接下来看 ServiceLoader的实现,先看 ServiceLoader 的成员变量。

//ServiceLoader 实现了接口 Iterable ,说明可以作为迭代器使用
public final class ServiceLoader<S>
    implements Iterable<S>
{
  //classpath 下文件前缀
    private static final String PREFIX = "META-INF/services/";
    //服务的接口或类
    private final Class<S> service;
    //实例化服务提供者的类加载器
    private final ClassLoader loader;
    //ServiceLoader 被创建时的访问控制上下文
    private final AccessControlContext acc;
    // 服务提供者缓存
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 懒加载的迭代器,ServiceLoader使用该迭代器懒加载。
    private LazyIterator lookupIterator;
}

方法java.util.ServiceLoader#load(java.lang.Class<S>)最终调用 ServiceLoader 的构造方法对成员变量进行初始化,当我们循环获取 SPI 实现时会调用方法java.util.ServiceLoader#iterator,跟踪源码如下

    public Iterator<S> iterator() {
        return new Iterator<S>() {
      //已经加载过的Provider
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
            public boolean hasNext() {
              //如果已加载过的Provider中没有更多,则尝试查找
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }
            public S next() {
              如果已加载过的Provider中没有更多,则尝试从查找到的迭代器中获取
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

可以看到,对 ServiceProvider 的迭代最后又会调用 LazyIterator

迭代器的方法,LazyIterator 是 ServiceProvider的静态内部类,前面实例化 ServiceProvider 时会对LazyIterator 进行实例化。LazyItertor 核心源码如下

       private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    //获取类路径下资源文件
                    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;
                }
                //pending 表示某个文件中的 SPI 实现类集合,parse方法用于解析文件,最终循环调用方法java.util.ServiceLoader#parseLine 校验文件中的行是否合法  
                pending = parse(service, configs.nextElement());
            }
            //nextName 表示下一个待获取的实现类的全限定名
            nextName = pending.next();
            return true;
        }
        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            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 {
              //在此调用无参数构造方法实例化 SPI 实现类,因此 SPI 实现类应包含一个无参数的构造方法
                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
        }

LazyIterator 类通过对类路径下SPI 文件的解析获取到实现类,然后进行加载,解析过程最终调用了方法java.util.ServiceLoader#parseLine,该方法主要解析文件中的每一行字符串,主要进行校验字符串是否合法,在此不做分析。


总结

SPI 通过对实现的来源进行约定,可以将接口与实现进行解耦,使得服务的使用者不必关心具体的实现在哪个地方即可获取到实现,但是使用SPI必须一次加载所有的实现类,无法根据条件获取某一个具体的实现。


目录
相关文章
|
2月前
|
Java 数据库连接 数据库
Java服务提供接口(SPI)的设计与应用剖析
Java SPI提供了一种优雅的服务扩展和动态加载机制,使得Java应用程序可以轻松地扩展功能和替换组件。通过合理的设计与应用,SPI可以大大增强Java应用的灵活性和可扩展性。
70 18
|
3月前
|
Dubbo Java 关系型数据库
Java SPI机制分析
文章深入分析了Java SPI机制,以JDBC为例,详细探讨了服务提供者接口的发现、加载过程,并提供了一个序列化服务的实战示例,展示了如何使用ServiceLoader进行服务发现和扩展。
31 3
|
3月前
|
Java 开发者
Java SPI机制大揭秘:动态加载服务提供者,一文让你彻底解锁!
【8月更文挑战第25天】Java SPI(服务提供者接口)是一种强大的扩展机制,允许程序在运行时动态加载服务实现。本文首先介绍SPI的基本原理——定义接口并通过配置文件指定其实现类,随后通过示例演示其实现过程。接着,对比分析了SPI与反射及插件机制的不同之处,强调SPI在灵活性与扩展性方面的优势。最后,基于不同场景推荐合适的选择策略,帮助读者深入理解并有效利用SPI机制。
104 1
|
3月前
|
开发者 C# 自然语言处理
WPF开发者必读:掌握多语言应用程序开发秘籍,带你玩转WPF国际化支持!
【8月更文挑战第31天】随着全球化的加速,开发多语言应用程序成为趋势。WPF作为一种强大的图形界面技术,提供了优秀的国际化支持,包括资源文件存储、本地化处理及用户界面元素本地化。本文将介绍WPF国际化的实现方法,通过示例代码展示如何创建和绑定资源文件,并设置应用程序语言环境,帮助开发者轻松实现多语言应用开发,满足不同地区用户的需求。
70 0
|
4月前
|
Java Spring 容器
Java中套路和实现问题之基于SPI机制的套路有哪些关键点
Java中套路和实现问题之基于SPI机制的套路有哪些关键点
|
3月前
|
Java 数据库连接 API
Java 的 SPI 机制
Java 的 SPI 机制
31 0
|
5月前
|
安全 Java 编译器
杭州 【Java基础知识 11】java泛型方法的定义和使用(学习+改进+自己理解,想法) (借鉴-侵-删)
杭州 【Java基础知识 11】java泛型方法的定义和使用(学习+改进+自己理解,想法) (借鉴-侵-删)
41 1
|
5月前
|
监控 Dubbo Java
深入理解Java SPI:服务发现与扩展的利器(一)
深入理解Java SPI:服务发现与扩展的利器(一)
|
6月前
|
Java 调度 开发者
Java中的多线程编程:基础知识与实践
【5月更文挑战第29天】 在现代软件开发中,多线程编程是一个不可忽视的领域。特别是在Java这种广泛使用的编程语言中,掌握多线程的概念和技术对于开发高效、响应迅速的应用程序至关重要。本文将深入探讨Java多线程的核心概念、实现机制以及常见问题的解决方案。我们将从基础出发,逐步揭示如何通过多线程提升程序性能,并且讨论并发编程中的挑战和解决策略。文章的目的是为开发者提供一个清晰的多线程编程指南,帮助他们在实际项目中有效地应用这些知识。
39 3
|
6月前
|
Java 关系型数据库 MySQL
【Java——SPI机制详解】
SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。 当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的
152 1