Java SPI机制的原理和实践

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: SPI 全称 Service Provider Interface,是 Java 提供的,旨在由第三方实现或扩展的 API,它是一种用于动态加载服务的机制。Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。

SPI 全称 Service Provider Interface,是 Java 提供的,旨在由第三方实现或扩展的 API,它是一种用于动态加载服务的机制。Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。

image-20230808213108346

一、概述

Java SPI是SPI的一种重要实现方式,是JDK内置的一种服务发现方法,实现逻辑是:调用方通过调用JDK提供的标准化的服务接口,通过本地服务发现,加载第三方或者本地实现了该接口的类,通过这种方式,服务规范制定者制定接口规范,服务提供者按照接口进行实现。在JDK中实现数据库驱动按需加载就是利用SPI的方式实现的,JDK规定了java.sql.Driver接口,其具体实现可以是MySQL或者PostgreSQL,具体实现是第三方的驱动服务方提供,通过SPI机制加载供调用方使用。

img

1.应用场景

SPI机制应用在服务的规范制定者和服务的实现者需要分离的情况,就是接口中方法的规定和实现是分离的,这种机制经常在可选组件和可选插件的场景中使用,通过SPI的机制将具体的实现加载进来。具体的应用在 Java 的 java.util.spi package 中就约定了很多 SPI 接口。下面,列举一些 SPI 接口:

  1. TimeZoneNameProvider: 为 TimeZone 类提供本地化的时区名称。
  2. DateFormatProvider: 为指定的语言环境提供日期和时间格式。
  3. NumberFormatProvider: 为 NumberFormat 类提供货币、整数和百分比值。
  4. Driver: 从 4.0 版开始,JDBC API 支持 SPI 模式。旧版本使用 Class.forName() 方法加载驱动程序。
  5. PersistenceProvider: 提供 JPA API 的实现。

2.优点和缺点

优点:

SPI机制的优点是灵活性高,可以通过简单地添加或替换实现类来扩展应用程序的功能。同时,SPI机制也具有一定的可扩展性和可维护性,因为它将应用程序和具体实现解耦,实现了高内聚、低耦合的目标。

缺点:

SPI机制的缺点是需要程序员手动编写实现类并在META-INF/services目录下创建配置文件,这样会增加代码量和工作量。同时,SPI机制也存在安全风险,因为实现类是由外部提供的,可能存在恶意实现类的风险。

二、原理

1.主要组成和原理说明

要通过SPI实现动态服务发现,首先需要了解其主要的组成部分:

  1. SPI 接口:为服务提供者实现类约定的的接口或抽象类。
  2. SPI 实现类:实际提供服务的实现类。
  3. SPI 配置:Java SPI 机制约定的配置文件,提供查找服务实现类的逻辑。配置文件必须置于 META-INF/services 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。
  4. ServiceLoader:Java SPI 的核心类,用于加载 SPI 实现类。ServiceLoader 中有各种实用方法来获取特定实现、迭代它们或重新加载服务。

SPI原理说明

实现SPI的动态加载实现类主要是通过ServiceLoader类来实现。首先调用方通过ServiceLoader.load ()静态方法来加载SPI服务,ServiceLoader的成员变量中保存了实现类配置文件位置前缀、实现类信息、实现类ClassLoader信息、实现类实例信息等。

//实现类配置文件位置前缀
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;

ServiceLoader.load ()方法中调用构造方法ServiceLoader(),构造方法中调用reload() 方法,通过lookupIterator = new LazyIterator(service, loader);来初始化懒加载的迭代器,查看其next()方法,其核心就是从META-INF/services/ 目录中读取文件,将其转换为Class类对象加载进来。最后Class类对象通过newInstance()方法实例化,这样就根据SPI中的接口信息加载了实现类。

//ServiceLoader.java

//静态方法load()
public static <S> ServiceLoader<S> load(Class<S> service) {
   
   
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

//将实现类的全限定类名转换为完整路径
        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 = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }
//Class.java

//根据类对象信息生成对象实例
public T newInstance()
    throws InstantiationException, IllegalAccessException
{
   
   
    if (System.getSecurityManager() != null) {
   
   
        checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), false);
    }

    // NOTE: the following code may not be strictly correct under
    // the current Java memory model.

    // Constructor lookup
    if (cachedConstructor == null) {
   
   
        if (this == Class.class) {
   
   
            throw new IllegalAccessException(
                "Can not call newInstance() on the Class for java.lang.Class"
            );
        }
        try {
   
   
            Class<?>[] empty = {
   
   };
            final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
            // Disable accessibility checks on the constructor
            // since we have to do the security check here anyway
            // (the stack depth is wrong for the Constructor's
            // security check to work)
            java.security.AccessController.doPrivileged(
                new java.security.PrivilegedAction<Void>() {
   
   
                    public Void run() {
   
   
                            c.setAccessible(true);
                            return null;
                        }
                    });
            cachedConstructor = c;
        } catch (NoSuchMethodException e) {
   
   
            throw (InstantiationException)
                new InstantiationException(getName()).initCause(e);
        }
    }
    Constructor<T> tmpConstructor = cachedConstructor;
    // Security check (same as in java.lang.reflect.Constructor)
    int modifiers = tmpConstructor.getModifiers();
    if (!Reflection.quickCheckMemberAccess(this, modifiers)) {
   
   
        Class<?> caller = Reflection.getCallerClass();
        if (newInstanceCallerCache != caller) {
   
   
            Reflection.ensureMemberAccess(caller, this, null, modifiers);
            newInstanceCallerCache = caller;
        }
    }
    // Run constructor
    try {
   
   
        return tmpConstructor.newInstance((Object[])null);
    } catch (InvocationTargetException e) {
   
   
        Unsafe.getUnsafe().throwException(e.getTargetException());
        // Not reached
        return null;
    }
}

在代码阅读过程中,有点需要注意,在调用ServiceLoader.load ()方法时,会进行一次重载调用,会多传入了一个新的ClassLoader,这个ClassLoader是ApplicationClassLoader,他的作用是加载目前运行中应用的类对象,需要这样做的原因是JDK的双亲委派机制决定的,即加载ServiceLoader类的ClassLoader是BootstrapClassLoader,所以默认情况通过他去创建的对象也是BootstrapClassLoader,但通过SPI机制需要加载的实现类都在classpath中,无法被加载,所以通过Thread.currentThread().getContextClassLoader();这个方法来获取ApplicationClassLoader。

//ServiceLoader.java

public static <S> ServiceLoader<S> load(Class<S> service) {
   
   
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
//Thread.java

public ClassLoader getContextClassLoader() {
   
   
    if (contextClassLoader == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
   
   
        ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                               Reflection.getCallerClass());
    }
    return contextClassLoader;
}

2.SPI在 JDBC DriverManager上的应用案例分析

在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现。

具体的使用方法是:

  1. JDBC接口定义:首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。
  2. MySQL实现:在mysql的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。
  3. 服务使用:通过以下方法就能直接使用
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);

原理分析

DriverManager.getConnection(url,username,password);执行的代码逻辑。

//DriverManager.java

//通过用户名密码等信息获取连接
public static Connection getConnection(String url,
    String user, String password) throws SQLException {
   
   
    java.util.Properties info = new java.util.Properties();

    if (user != null) {
   
   
        info.put("user", user);
    }
    if (password != null) {
   
   
        info.put("password", password);
    }

    return (getConnection(url, info, Reflection.getCallerClass()));
}



//通过实例化后的驱动连接数据库
    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;

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

DriverManager.java的静态代码段执行loadInitialDrivers();方法,该方法的主要逻辑为:

  1. 从系统变量中获取有关驱动的定义。
  2. 使用SPI来获取驱动的实现。
  3. 遍历使用SPI获取到的具体实现,实例化各个实现类。
  4. 根据第一步获取到的驱动列表来实例化具体实现类。
//DriverManager.java

//静态代码段
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;
        }
        // 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<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                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);
            }
        }
    }

通过以上方式,就能把实现Driver接口的驱动实现类加载进来了。

SPI在slf4j上应用的案例分析

todo

三、实践

1.实现步骤

  1. 定义接口:定义一个接口,声明一些抽象方法。
  2. 创建实现类:创建一个或多个实现该接口的类。
  3. 配置文件:在META-INF/services/目录下创建一个以接口全限定名为命名的文件,内容为实现类的全限定名,每行一个。
  4. 加载配置:使用ServiceLoader类加载配置文件并解析出实现类。

2.代码实现

实现通过SPI加载不同数据库实现方式的功能,数据库的实现方式包括Mysql和redis。

(1)实现接口

public interface DataStorage {
   
   
    String search(String key);
}

(2)创建实现类

创建2个实现类,分别是Mysql和redis的实现方式。

public class MysqlStorage implements DataStorage{
   
   

    @Override
    public String search(String key) {
   
   
        return "【Mysql】搜索" + key + ",结果:No";
    }
}
public class RedisStorage implements DataStorage{
   
   

    @Override
    public String search(String key) {
   
   
        return "【Redis】搜索" + key + ",结果:Yes";
    }
}

(3)配置文件

image-20230808205129264

(4)加载配置

打印出通过SPI加载到的数据库驱动。

import java.util.ServiceLoader;

public class SpiDemo {
   
   
    public static void main(String[] args) {
   
   
        ServiceLoader<DataStorage> serviceLoader = ServiceLoader.load(DataStorage.class);
        System.out.println("============ Java SPI 测试============");
        serviceLoader.forEach(loader -> System.out.println(loader.search("Yes Or No")));
    }

}

(5)验证效果

image-20230808205020924

四、总结

(1)SPI的核心思路是什么?
SPI源码的代码逻辑还是比较复杂,但是核心的思路就是通过ApplicationClassloader加载到配置好的实现类,获取ApplicationClassloader的方法是getContextClassLoader()方法,配置类信息记录在约定好的META-INF/services/目录中。

(2)SPI模式和API模式的区别?

两种模式都区分服务提供者和服务调用者的,API的服务实现放在服务提供者方那边,和接口一起提供;SPI的服务实现可以第三方实现或者自己实现,和接口是分开的。


TODO

  • [ ] 补充SPI在slf4j上应用的案例分析;
  • [ ] 增加Java、Spring、Dubbo三者SPI机制区别的总结;

参考资料

  1. 鲜为人知的Java SPI机制:https://juejin.cn/post/7224756843713036345#heading-3
  2. Java常用机制 - SPI机制详解:https://www.pdai.tech/md/java/advanced/java-advanced-spi.html
  3. 源码级深度理解 Java SPI:https://zhuanlan.zhihu.com/p/580004065?utm_medium=social&utm_oi=26977696219136&utm_psn=1573735461662785536&utm_source=wechat_session
  4. 阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别:https://juejin.cn/post/7210601680554246202
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
17天前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
17天前
|
存储 算法 Java
【JAVA】生成accessToken原理
在Java中,生成accessToken用于身份验证和授权,确保合法用户访问受保护资源。流程包括:1. 身份验证(如用户名密码、OAuth 2.0);2. 生成唯一且安全的令牌;3. 设置令牌有效期并存储;4. 客户端传递令牌,服务器验证其有效性。常见场景为OAuth 2.0协议,涉及客户端注册、用户授权、获取授权码和换取accessToken。示例代码展示了使用Apache HttpClient库模拟OAuth 2.0获取accessToken的过程。
|
1月前
|
Kubernetes Java 持续交付
小团队 CI/CD 实践:无需运维,Java Web应用的自动化部署
本文介绍如何使用GitHub Actions和阿里云Kubernetes(ACK)实现Java Web应用的自动化部署。通过CI/CD流程,开发人员无需手动处理复杂的运维任务,从而提高效率并减少错误。文中详细讲解了Docker与Kubernetes的概念,并演示了从创建Kubernetes集群、配置容器镜像服务到设置GitHub仓库Secrets及编写GitHub Actions工作流的具体步骤。最终实现了代码提交后自动构建、推送镜像并部署到Kubernetes集群的功能。整个过程不仅简化了部署流程,还确保了应用在不同环境中的稳定运行。
82 9
|
2月前
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
60 3
|
2月前
|
安全 算法 Java
Java CAS原理和应用场景大揭秘:你掌握了吗?
CAS(Compare and Swap)是一种乐观锁机制,通过硬件指令实现原子操作,确保多线程环境下对共享变量的安全访问。它避免了传统互斥锁的性能开销和线程阻塞问题。CAS操作包含三个步骤:获取期望值、比较当前值与期望值是否相等、若相等则更新为新值。CAS广泛应用于高并发场景,如数据库事务、分布式锁、无锁数据结构等,但需注意ABA问题。Java中常用`java.util.concurrent.atomic`包下的类支持CAS操作。
93 2
|
2月前
|
Java 程序员
深入理解Java异常处理机制
Java的异常处理是编程中的一块基石,它不仅保障了代码的健壮性,还提升了程序的可读性和可维护性。本文将深入浅出地探讨Java异常处理的核心概念、分类、处理策略以及最佳实践,旨在帮助读者建立正确的异常处理观念,提升编程效率和质量。
154 1
|
2月前
|
Java 开发者 UED
深入探索Java中的异常处理机制##
本文将带你深入了解Java语言中的异常处理机制,包括异常的分类、异常的捕获与处理、自定义异常的创建以及最佳实践。通过具体实例和代码演示,帮助你更好地理解和运用Java中的异常处理,提高程序的健壮性和可维护性。 ##
79 2
|
2月前
|
Java API 开发者
深入理解Java中的异常处理机制
本文探讨了Java编程语言中异常处理的核心概念,包括异常类型、异常捕获与抛出、以及最佳实践。通过分析常见的异常场景和处理策略,旨在帮助开发者更好地理解和运用异常处理机制,提高代码的健壮性和可维护性。文章不仅涵盖了基本的try-catch结构,还深入讨论了自定义异常的创建与使用,以及finally块的重要性和应用。此外,还将介绍一些高级技巧,如多异常捕获和嵌套异常处理,为读者提供全面的技术指导。
120 0
|
存储 Java
【Java 虚拟机原理】线程栈 | 栈帧 | 局部变量表 | 反汇编字节码文件 | Java 虚拟机指令手册 | 程序计数器
【Java 虚拟机原理】线程栈 | 栈帧 | 局部变量表 | 反汇编字节码文件 | Java 虚拟机指令手册 | 程序计数器
143 0
【Java 虚拟机原理】线程栈 | 栈帧 | 局部变量表 | 反汇编字节码文件 | Java 虚拟机指令手册 | 程序计数器
|
1天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
88 60
【Java并发】【线程池】带你从0-1入门线程池

热门文章

最新文章